| 
17 | 17 | module Mongo  | 
18 | 18 |   module Tracing  | 
19 | 19 |     module OpenTelemetry  | 
 | 20 | +      # CommandTracer is responsible for tracing MongoDB server commands using OpenTelemetry.  | 
 | 21 | +      #  | 
 | 22 | +      # @api private  | 
20 | 23 |       class CommandTracer  | 
21 |  | -        def initialize(otel_tracer, query_text_max_length: 0)  | 
 | 24 | +        extend Forwardable  | 
 | 25 | + | 
 | 26 | +        def_delegators :@parent_tracer,  | 
 | 27 | +          :cursor_context_map,  | 
 | 28 | +          :parent_context_for,  | 
 | 29 | +          :transaction_context_map,  | 
 | 30 | +          :transaction_map_key  | 
 | 31 | + | 
 | 32 | +        def initialize(otel_tracer, parent_tracer, query_text_max_length: 0)  | 
22 | 33 |           @otel_tracer = otel_tracer  | 
 | 34 | +          @parent_tracer = parent_tracer  | 
23 | 35 |           @query_text_max_length = query_text_max_length  | 
24 | 36 |         end  | 
25 | 37 | 
 
  | 
26 |  | -        def trace_command(message, _operation_context, connection)  | 
27 |  | -          @otel_tracer.in_span(  | 
 | 38 | +        def trace_command(message, operation_context, connection)  | 
 | 39 | +          parent_context = parent_context_for(operation_context, cursor_id(message))  | 
 | 40 | +          span = @otel_tracer.start_span(  | 
28 | 41 |             command_span_name(message),  | 
29 | 42 |             attributes: span_attributes(message, connection),  | 
 | 43 | +            with_parent: parent_context,  | 
30 | 44 |             kind: :client  | 
31 |  | -          ) do |span, _context|  | 
 | 45 | +          )  | 
 | 46 | +          ::OpenTelemetry::Trace.with_span(span) do |s, c|  | 
 | 47 | +            # TODO: process cursor context if applicable  | 
32 | 48 |             yield.tap do |result|  | 
33 |  | -              if result.respond_to?(:cursor_id) && result.cursor_id.positive?  | 
34 |  | -                span.set_attribute('db.mongodb.cursor_id', result.cursor_id)  | 
35 |  | -              end  | 
 | 49 | +              process_cursor_context(result, cursor_id(message), c, s)  | 
36 | 50 |             end  | 
37 | 51 |           end  | 
 | 52 | +        rescue Exception => e  | 
 | 53 | +          span&.record_exception(e)  | 
 | 54 | +          span&.status = ::OpenTelemetry::Trace::Status.error("Unhandled exception of type: #{e.class}")  | 
 | 55 | +          raise e  | 
 | 56 | +        ensure  | 
 | 57 | +          span&.finish  | 
38 | 58 |         end  | 
39 | 59 | 
 
  | 
40 | 60 |         private  | 
41 | 61 | 
 
  | 
42 | 62 |         def span_attributes(message, connection)  | 
43 | 63 |           {  | 
44 | 64 |             'db.system' => 'mongodb',  | 
45 |  | -            'db.namespace' => message.documents.first['$db'],  | 
 | 65 | +            'db.namespace' => database(message),  | 
46 | 66 |             'db.collection.name' => collection_name(message),  | 
47 |  | -            'db.operation.name' => message.documents.first.keys.first,  | 
 | 67 | +            'db.command.name' => command_name(message),  | 
48 | 68 |             'server.port' => connection.address.port,  | 
49 | 69 |             'server.address' => connection.address.host,  | 
50 | 70 |             'network.transport' => connection.transport.to_s,  | 
51 | 71 |             'db.mongodb.server_connection_id' => connection.server.description.server_connection_id,  | 
52 | 72 |             'db.mongodb.driver_connection_id' => connection.id,  | 
 | 73 | +            'db.mongodb.cursor_id' => cursor_id(message),  | 
53 | 74 |             'db.query.text' => query_text(message)  | 
54 | 75 |           }.compact  | 
55 | 76 |         end  | 
56 | 77 | 
 
  | 
 | 78 | +        def process_cursor_context(result, cursor_id, context, span)  | 
 | 79 | +          if result.respond_to?(:cursor_id) && result.cursor_id.positive?  | 
 | 80 | +            span.set_attribute('db.mongodb.cursor_id', result.cursor_id)  | 
 | 81 | +          end  | 
 | 82 | +        end  | 
 | 83 | + | 
57 | 84 |         def command_span_name(message)  | 
58 |  | -          message.documents.first.keys.first  | 
 | 85 | +          if (coll_name = collection_name(message))  | 
 | 86 | +            "#{command_name(message)} #{database(message)}.#{coll_name}"  | 
 | 87 | +          else  | 
 | 88 | +            "#{command_name(message)} #{database(message)}"  | 
 | 89 | +          end  | 
59 | 90 |         end  | 
60 | 91 | 
 
  | 
61 | 92 |         def collection_name(message)  | 
62 | 93 |           case message.documents.first.keys.first  | 
63 | 94 |           when 'getMore'  | 
64 |  | -            message.documents.first['collection']  | 
 | 95 | +            message.documents.first['collection'].to_s  | 
65 | 96 |           else  | 
66 |  | -            message.documents.first.values.first  | 
 | 97 | +            message.documents.first.values.first.to_s  | 
67 | 98 |           end  | 
68 | 99 |         end  | 
69 | 100 | 
 
  | 
 | 101 | +        def command_name(message)  | 
 | 102 | +          message.documents.first.keys.first.to_s  | 
 | 103 | +        end  | 
 | 104 | + | 
 | 105 | +        def database(message)  | 
 | 106 | +          message.documents.first['$db'].to_s  | 
 | 107 | +        end  | 
 | 108 | + | 
70 | 109 |         def query_text?  | 
71 | 110 |           @query_text_max_length.positive?  | 
72 | 111 |         end  | 
73 | 112 | 
 
  | 
 | 113 | +        def cursor_id(message)  | 
 | 114 | +          if command_name(message) == 'getMore'  | 
 | 115 | +            message.documents.first['getMore'].value  | 
 | 116 | +          end  | 
 | 117 | +        end  | 
 | 118 | + | 
74 | 119 |         EXCLUDED_KEYS = %w[lsid $db $clusterTime signature].freeze  | 
75 | 120 |         ELLIPSES = '...'  | 
76 | 121 | 
 
  | 
 | 
0 commit comments