File: tracer.rb

package info (click to toggle)
ruby-mongo 2.23.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 15,020 kB
  • sloc: ruby: 110,810; makefile: 5
file content (236 lines) | stat: -rw-r--r-- 9,582 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# frozen_string_literal: true

# Copyright (C) 2025-present MongoDB Inc.
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

module Mongo
  module Tracing
    module OpenTelemetry
      # OpenTelemetry tracer for MongoDB operations and commands.
      # @api private
      class Tracer
        # @return [ OpenTelemetry::Trace::Tracer ] the OpenTelemetry tracer implementation
        #   used to create spans for MongoDB operations and commands.
        #
        # @api private
        attr_reader :otel_tracer

        # Initializes a new OpenTelemetry tracer.
        #
        # @param enabled [ Boolean | nil ] whether OpenTelemetry is enabled or not.
        #   Defaults to nil, which means it will check the environment variable
        #   OTEL_RUBY_INSTRUMENTATION_MONGODB_ENABLED (values: true/1/yes). If the
        #   environment variable is not set, OpenTelemetry will be disabled by default.
        # @param query_text_max_length [ Integer | nil ] maximum length for captured query text.
        #   Defaults to nil, which means it will check the environment variable
        #   OTEL_RUBY_INSTRUMENTATION_MONGODB_QUERY_TEXT_MAX_LENGTH. If the environment variable is not set,
        #   the query text will not be captured.
        # @param otel_tracer [ OpenTelemetry::Trace::Tracer | nil ] the OpenTelemetry tracer
        #   implementation to use. Defaults to nil, which means it will use the default tracer
        #   from OpenTelemetry's tracer provider.
        def initialize(enabled: nil, query_text_max_length: nil, otel_tracer: nil)
          @enabled = if enabled.nil?
                       %w[true 1 yes].include?(ENV['OTEL_RUBY_INSTRUMENTATION_MONGODB_ENABLED']&.downcase)
                     else
                       enabled
                     end
          check_opentelemetry_loaded
          @query_text_max_length = if query_text_max_length.nil?
                                     ENV['OTEL_RUBY_INSTRUMENTATION_MONGODB_QUERY_TEXT_MAX_LENGTH'].to_i
                                   else
                                     query_text_max_length
                                   end
          @otel_tracer = otel_tracer || initialize_tracer
          @operation_tracer = OperationTracer.new(@otel_tracer, self)
          @command_tracer = CommandTracer.new(@otel_tracer, self, query_text_max_length: @query_text_max_length)
        end

        # Whether OpenTelemetry is enabled or not.
        #
        # @return [Boolean] true if OpenTelemetry is enabled, false otherwise.
        def enabled?
          @enabled
        end

        # Trace a MongoDB operation.
        #
        # @param operation [Mongo::Operation] The MongoDB operation to trace.
        # @param operation_context [Mongo::Operation::Context] The context of the operation.
        # @param op_name [String, nil] An optional name for the operation.
        # @yield The block representing the operation to be traced.
        # @return [Object] The result of the operation.
        def trace_operation(operation, operation_context, op_name: nil, &block)
          return yield unless enabled?

          @operation_tracer.trace_operation(operation, operation_context, op_name: op_name, &block)
        end

        # Trace a MongoDB command.
        #
        # @param message [Mongo::Protocol::Message] The MongoDB command message to trace.
        # @param operation_context [Mongo::Operation::Context] The context of the operation.
        # @param connection [Mongo::Server::Connection] The connection used to send the command
        # @yield The block representing the command to be traced.
        # @return [Object] The result of the command.
        def trace_command(message, operation_context, connection, &block)
          return yield unless enabled?

          @command_tracer.trace_command(message, operation_context, connection, &block)
        end

        # Start a transaction span and activate its context.
        #
        # @param session [Mongo::Session] The session starting the transaction.
        def start_transaction_span(session)
          return unless enabled?

          key = transaction_map_key(session)
          return unless key

          # Create the transaction span with minimal attributes
          span = @otel_tracer.start_span(
            'transaction',
            attributes: { 'db.system' => 'mongodb' },
            kind: :client
          )

          # Create a context containing this span
          context = ::OpenTelemetry::Trace.context_with_span(span)

          # Activate the context and store the token for later detachment
          token = ::OpenTelemetry::Context.attach(context)

          # Store span, token, and context for later retrieval
          transaction_span_map[key] = span
          transaction_token_map[key] = token
          transaction_context_map[key] = context
        end

        # Finish a transaction span and deactivate its context.
        #
        # @param session [Mongo::Session] The session finishing the transaction.
        def finish_transaction_span(session)
          return unless enabled?

          key = transaction_map_key(session)
          return unless key

          span = transaction_span_map.delete(key)
          token = transaction_token_map.delete(key)
          transaction_context_map.delete(key)

          return unless span && token

          begin
            span.finish
          ensure
            ::OpenTelemetry::Context.detach(token)
          end
        end

        # Returns the cursor context map for tracking cursor-related OpenTelemetry contexts.
        #
        # @return [ Hash ] map of cursor IDs to OpenTelemetry contexts.
        def cursor_context_map
          @cursor_context_map ||= {}
        end

        # Generates a unique key for cursor tracking in the context map.
        #
        # @param session [ Mongo::Session ] the session associated with the cursor.
        # @param cursor_id [ Integer ] the cursor ID.
        #
        # @return [ String | nil ] unique key combining session ID and cursor ID, or nil if either is nil.
        def cursor_map_key(session, cursor_id)
          return if cursor_id.nil? || session.nil?

          "#{session.session_id['id'].to_uuid}-#{cursor_id}"
        end

        # Determines the parent OpenTelemetry context for an operation.
        #
        # Returns the transaction context if the operation is part of a transaction,
        # otherwise returns nil. Cursor-based context nesting is not currently implemented.
        #
        # @param operation_context [ Mongo::Operation::Context ] the operation context.
        # @param cursor_id [ Integer ] the cursor ID, if applicable.
        #
        # @return [ OpenTelemetry::Context | nil ] parent context or nil.
        def parent_context_for(operation_context, cursor_id)
          if (key = transaction_map_key(operation_context.session))
            transaction_context_map[key]
          elsif (_key = cursor_map_key(operation_context.session, cursor_id))
            # We return nil here unless we decide how to nest cursor operations.
            nil
          end
        end

        # Returns the transaction context map for tracking active transaction contexts.
        #
        # @return [ Hash ] map of transaction keys to OpenTelemetry contexts.
        def transaction_context_map
          @transaction_context_map ||= {}
        end

        # Returns the transaction span map for tracking active transaction spans.
        #
        # @return [ Hash ] map of transaction keys to OpenTelemetry spans.
        def transaction_span_map
          @transaction_span_map ||= {}
        end

        # Returns the transaction token map for tracking context attachment tokens.
        #
        # @return [ Hash ] map of transaction keys to OpenTelemetry context tokens.
        def transaction_token_map
          @transaction_token_map ||= {}
        end

        # Generates a unique key for transaction tracking.
        #
        # Returns nil for implicit sessions or sessions not in a transaction.
        #
        # @param session [ Mongo::Session ] the session.
        #
        # @return [ String | nil ] unique key combining session ID and transaction number, or nil.
        def transaction_map_key(session)
          return if session.nil? || session.implicit? || !session.in_transaction?

          "#{session.session_id['id'].to_uuid}-#{session.txn_num}"
        end

        private

        def check_opentelemetry_loaded
          return unless @enabled
          return if defined?(::OpenTelemetry)

          Logger.logger.warn('OpenTelemetry tracing for MongoDB is enabled, ' \
                             'but the OpenTelemetry library is not loaded. ' \
                             'Disabling tracing.')
          @enabled = false
        end

        def initialize_tracer
          return unless enabled?

          ::OpenTelemetry.tracer_provider.tracer(
            'mongo-ruby-driver',
            Mongo::VERSION
          )
        end
      end
    end
  end
end