123456789_123456789_123456789_123456789_123456789_

Class: Mongo::Tracing::OpenTelemetry::CommandTracer Private

Relationships & Source Files
Super Chains via Extension / Inclusion / Inheritance
Instance Chain:
Inherits: Object
Defined in: lib/mongo/tracing/open_telemetry/command_tracer.rb

Overview

CommandTracer is responsible for tracing MongoDB server commands using ::Mongo::Tracing::OpenTelemetry.

Constant Summary

::Mongo::Monitoring::Event::Secure - Included

REDACTED_COMMANDS

Class Method Summary

Instance Attribute Summary

Instance Method Summary

::Mongo::Monitoring::Event::Secure - Included

#compression_allowed?

Is compression allowed for a given command message.

#redacted

Redact secure information from the document if:

#sensitive?

Check whether the command is sensitive in terms of command monitoring spec.

Constructor Details

.new(otel_tracer, parent_tracer, query_text_max_length: 0) ⇒ CommandTracer

Initializes a new CommandTracer.

Parameters:

[ GitHub ]

  
# File 'lib/mongo/tracing/open_telemetry/command_tracer.rb', line 40

def initialize(otel_tracer, parent_tracer, query_text_max_length: 0)
  @otel_tracer = otel_tracer
  @parent_tracer = parent_tracer
  @query_text_max_length = query_text_max_length
end

Instance Attribute Details

#query_text? ⇒ Boolean (readonly, private)

Checks if query text capture is enabled.

Returns:

  • (Boolean) —

    true if query text should be captured.

[ GitHub ]

  
# File 'lib/mongo/tracing/open_telemetry/command_tracer.rb', line 305

def query_text?
  @query_text_max_length.positive?
end

Instance Method Details

#apply_deferred_attributes(span, message) (private)

Sets the remaining attributes on a span that is recording. They are not visible to the sampler; only custom samplers keying on them are affected.

Parameters:

[ GitHub ]

  
# File 'lib/mongo/tracing/open_telemetry/command_tracer.rb', line 187

def apply_deferred_attributes(span, message)
  {
    'db.query.summary' => query_summary(message),
    'db.query.text' => query_text(message),
    'db.mongodb.cursor_id' => cursor_id(message),
    'db.mongodb.lsid' => lsid(message),
    'db.mongodb.txn_number' => txn_number(message)
  }.each do |key, value|
    span.set_attribute(key, value) unless value.nil?
  end
end

#collection_name(message) ⇒ String | nil (private)

Extracts the collection name from the command message.

Parameters:

Returns:

  • (String | nil) —

    the collection name, or nil if not applicable.

[ GitHub ]

  
# File 'lib/mongo/tracing/open_telemetry/command_tracer.rb', line 271

def collection_name(message)
  case command_name(message)
  when 'getMore'
    message.documents.first['collection'].to_s
  when 'listCollections', 'listDatabases', 'commitTransaction', 'abortTransaction'
    nil
  else
    value = message.documents.first.values.first
    # Return nil if the value is not a string (e.g., for admin commands that have numeric values)
    value.is_a?(String) ? value : nil
  end
end

#command_name(message) ⇒ String (private)

Extracts the command name from the message.

Parameters:

Returns:

  • (String) —

    the command name.

[ GitHub ]

  
# File 'lib/mongo/tracing/open_telemetry/command_tracer.rb', line 289

def command_name(message)
  message.documents.first.keys.first.to_s
end

#connection_attributes(connection) ⇒ Hash (private)

Returns connection-related attributes.

Parameters:

Returns:

  • (Hash) —

    connection span attributes.

[ GitHub ]

  
# File 'lib/mongo/tracing/open_telemetry/command_tracer.rb', line 204

def connection_attributes(connection)
  {
    'server.port' => connection.address.port,
    'server.address' => connection.address.host,
    'network.transport' => connection.transport.to_s,
    'db.mongodb.server_connection_id' => connection.server.description.server_connection_id,
    'db.mongodb.driver_connection_id' => connection.id
  }
end

#create_command_span(message, connection) ⇒ OpenTelemetry::Trace::Span (private)

Creates a span for a command.

Parameters:

Returns:

  • (OpenTelemetry::Trace::Span) —

    the created span.

[ GitHub ]

  
# File 'lib/mongo/tracing/open_telemetry/command_tracer.rb', line 133

def create_command_span(message, connection)
  @otel_tracer.start_span(
    command_name(message),
    attributes: span_attributes(message, connection),
    kind: :client
  )
end

#cursor_id(message) ⇒ Integer | nil (private)

Extracts the cursor ID from getMore commands.

Parameters:

Returns:

  • (Integer | nil) —

    the cursor ID, or nil if not a getMore command.

[ GitHub ]

  
# File 'lib/mongo/tracing/open_telemetry/command_tracer.rb', line 314

def cursor_id(message)
  return unless command_name(message) == 'getMore'

  message.documents.first['getMore'].value
end

#database(message) ⇒ String (private)

Extracts the database name from the message.

Parameters:

Returns:

  • (String) —

    the database name.

[ GitHub ]

  
# File 'lib/mongo/tracing/open_telemetry/command_tracer.rb', line 298

def database(message)
  message.documents.first['$db'].to_s
end

#handle_command_exception(span, exception) (private)

Handles exceptions that occur during command execution.

Parameters:

  • span (OpenTelemetry::Trace::Span) —

    the span.

  • exception (Exception) —

    the exception that occurred.

[ GitHub ]

  
# File 'lib/mongo/tracing/open_telemetry/command_tracer.rb', line 154

def handle_command_exception(span, exception)
  if exception.is_a?(Mongo::Error::OperationFailure)
    span.set_attribute('db.response.status_code', exception.code.to_s)
  end
  span.record_exception(exception)
  span.status = ::OpenTelemetry::Trace::Status.error("Unhandled exception of type: #{exception.class}")
end

#lsid(message) ⇒ BSON::Binary | nil (private)

Extracts the logical session ID from the command.

Parameters:

Returns:

  • (BSON::Binary | nil) —

    the session ID, or nil if not present.

[ GitHub ]

  
# File 'lib/mongo/tracing/open_telemetry/command_tracer.rb', line 325

def lsid(message)
  lsid_doc = message.documents.first['lsid']
  return unless lsid_doc

  lsid_doc['id'].to_uuid
end

#maybe_trace_error(result, span) (private)

Records error status code if the command failed.

Parameters:

  • result (Object) —

    the command result.

  • span (OpenTelemetry::Trace::Span) —

    the current span.

[ GitHub ]

  
# File 'lib/mongo/tracing/open_telemetry/command_tracer.rb', line 242

def maybe_trace_error(result, span)
  return if result.successful?

  span.set_attribute('db.response.status_code', result.error.code.to_s)
  begin
    result.validate!
  rescue Mongo::Error::OperationFailure => e
    span.record_exception(e)
  end
end

#normalize_cursor_id(cursor_id) ⇒ Integer (private)

Normalizes a cursor id to a plain Integer.

OP_MSG replies deserialized in :bson mode expose the cursor id as a BSON::Int64, which does not implement Numeric#positive? and is not a valid ::Mongo::Tracing::OpenTelemetry attribute type.

Parameters:

  • cursor_id (Integer | BSON::Int64) —

    the raw cursor id.

Returns:

  • (Integer) —

    the cursor id as an Integer.

[ GitHub ]

  
# File 'lib/mongo/tracing/open_telemetry/command_tracer.rb', line 234

def normalize_cursor_id(cursor_id)
  cursor_id.is_a?(BSON::Int64) ? cursor_id.value : cursor_id
end

#process_command_result(result, span) (private)

Processes the command result and updates span attributes.

Parameters:

  • result (Object) —

    the command result.

  • span (OpenTelemetry::Trace::Span) —

    the current span.

[ GitHub ]

  
# File 'lib/mongo/tracing/open_telemetry/command_tracer.rb', line 145

def process_command_result(result, span)
  process_cursor_context(result, span)
  maybe_trace_error(result, span)
end

#process_cursor_context(result, span) (private)

Processes cursor context from the command result.

Parameters:

  • result (Object) —

    the command result.

  • span (OpenTelemetry::Trace::Span) —

    the current span.

[ GitHub ]

  
# File 'lib/mongo/tracing/open_telemetry/command_tracer.rb', line 218

def process_cursor_context(result, span)
  cursor_id = normalize_cursor_id(result.cursor_id)
  return unless cursor_id.positive?

  span.set_attribute('db.mongodb.cursor_id', cursor_id)
end

#query_summary(message) ⇒ String (private)

Generates a summary string for the query.

Parameters:

Returns:

  • (String) —

    summary in format "command_name db.collection" or "command_name db".

[ GitHub ]

  
# File 'lib/mongo/tracing/open_telemetry/command_tracer.rb', line 258

def query_summary(message)
  if (coll_name = collection_name(message))
    "#{command_name(message)} #{database(message)}.#{coll_name}"
  else
    "#{command_name(message)} #{database(message)}"
  end
end

#query_text(message) ⇒ String | nil (readonly, private)

Extracts and formats the query text from the command.

Parameters:

Returns:

  • (String | nil) —

    JSON representation of the command, truncated if necessary, or nil if disabled.

[ GitHub ]

  
# File 'lib/mongo/tracing/open_telemetry/command_tracer.rb', line 355

def query_text(message)
  return unless query_text?

  text = message
         .payload['command']
         .reject { |key, _| EXCLUDED_KEYS.include?(key) }
         .to_json
  if text.length > @query_text_max_length
    "#{text[0...@query_text_max_length]}#{ELLIPSIS}"
  else
    text
  end
end

#skip_tracing?(message) ⇒ Boolean (private)

Determines whether the command must not be traced. Sensitive auth commands carry credentials in their payloads (SCRAM proofs, cleartext passwords, etc.) and the ::Mongo::Tracing::OpenTelemetry spec requires drivers to skip command spans for them. Hello / legacy hello are also skipped to keep handshake traffic out of traces.

Parameters:

Returns:

  • (Boolean) —

    true when no command span should be created.

[ GitHub ]

  
# File 'lib/mongo/tracing/open_telemetry/command_tracer.rb', line 120

def skip_tracing?(message)
  name = command_name(message)
  return true if HELLO_COMMANDS.include?(name)

  sensitive?(command_name: name, document: message.documents.first)
end

#span_attributes(message, connection) ⇒ Hash (private)

Builds the attributes passed at span creation: the cheap set the ::Mongo::Tracing::OpenTelemetry spec recommends. They are built for every span, recorded or not, and are the only ones the sampler sees. The rest are set by apply_deferred_attributes once the span is known to record.

Parameters:

Returns:

[ GitHub ]

  
# File 'lib/mongo/tracing/open_telemetry/command_tracer.rb', line 172

def span_attributes(message, connection)
  {
    'db.system.name' => 'mongodb',
    'db.namespace' => database(message),
    'db.collection.name' => collection_name(message),
    'db.command.name' => command_name(message)
  }.compact.merge(connection_attributes(connection))
end

#start_span(message, operation_context, connection)

Starts a span for a MongoDB command.

Parameters:

[ GitHub ]

  
# File 'lib/mongo/tracing/open_telemetry/command_tracer.rb', line 51

def start_span(message, operation_context, connection); end

#trace_command(message, _operation_context, connection) { ... } ⇒ Object

Trace a MongoDB command.

Creates an ::Mongo::Tracing::OpenTelemetry span for the command, capturing attributes such as command name, database name, collection name, server address, connection IDs, and optionally query text. The span is automatically nested under the current operation span and is finished when the command completes or fails.

Parameters:

Yields:

  • the block representing the command to be traced.

Returns:

  • (Object) —

    the result of the command.

[ GitHub ]

  
# File 'lib/mongo/tracing/open_telemetry/command_tracer.rb', line 67

def trace_command(message, _operation_context, connection, &block)
  return yield if skip_tracing?(message)

  # Commands should always be nested under their operation span, not directly under
  # the transaction span. Don't pass with_parent to use automatic parent resolution
  # from the currently active span (the operation span).
  span = create_command_span(message, connection)
  # With no SDK and no valid parent, the API returns a span with an
  # invalid context. It cannot be made current, recorded or
  # propagated, so it gets no work at all, not even finishing it.
  return yield unless span.context.valid?

  trace_with_span(span, message, &block)
end

#trace_with_span(span, message) ⇒ Object (private)

Runs the command with the span made current, and finishes the span. A span that is not recording is still made current, so that its context is propagated, but gets no further attributes and no outcome: the SDK would discard them.

Parameters:

  • span (OpenTelemetry::Trace::Span) —

    a span with a valid context.

  • message (Mongo::Protocol::Message) —

    the command message.

Returns:

  • (Object) —

    the result of the command.

[ GitHub ]

  
# File 'lib/mongo/tracing/open_telemetry/command_tracer.rb', line 95

def trace_with_span(span, message)
  recording = span.recording?
  apply_deferred_attributes(span, message) if recording
  ::OpenTelemetry::Trace.with_span(span) do |s, _c|
    yield.tap do |result|
      process_command_result(result, s) if recording
    end
  end
rescue Exception => e
  handle_command_exception(span, e) if recording
  raise e
ensure
  span.finish
end

#txn_number(message) ⇒ Integer | nil (private)

Extracts the transaction number from the command.

Parameters:

Returns:

  • (Integer | nil) —

    the transaction number, or nil if not present.

[ GitHub ]

  
# File 'lib/mongo/tracing/open_telemetry/command_tracer.rb', line 337

def txn_number(message)
  txn_num = message.documents.first['txnNumber']
  return unless txn_num

  txn_num.value
end