Skip to content

Commit b9b9218

Browse files
committed
feat: Mongo semantic stability
1 parent 193d0af commit b9b9218

12 files changed

Lines changed: 1434 additions & 136 deletions

File tree

instrumentation/mongo/Appraisals

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,18 @@
44
#
55
# SPDX-License-Identifier: Apache-2.0
66

7-
appraise 'mongo-2.22' do
8-
gem 'mongo', '~> 2.22.0'
7+
# To facilitate database semantic convention stability migration, we are using
8+
# appraisal to test the different semantic convention modes along with different
9+
# gem versions. For more information on the semantic convention modes, see:
10+
# https://opentelemetry.io/docs/specs/semconv/non-normative/db-migration/
911

10-
# TODO: bson 5.1.0 isn't compatible with JRuby as of 2025/06/17
11-
gem 'bson', '< 5.1.0' if defined?(JRUBY_VERSION)
12+
semconv_stability = %w[old stable dup]
13+
14+
semconv_stability.each do |mode|
15+
appraise "mongo-latest-#{mode}" do
16+
gem 'mongo'
17+
18+
# TODO: bson 5.1.0 isn't compatible with JRuby as of 2025/06/17
19+
gem 'bson', '< 5.1.0' if defined?(JRUBY_VERSION)
20+
end
1221
end

instrumentation/mongo/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,22 @@ The `opentelemetry-instrumentation-mongo` gem source is [on github][repo-github]
7575

7676
The OpenTelemetry Ruby gems are maintained by the OpenTelemetry Ruby special interest group (SIG). You can get involved by joining us on our [GitHub Discussions][discussions-url], [Slack Channel][slack-channel] or attending our weekly meeting. See the [meeting calendar][community-meetings] for dates and times. For more information on this and other language SIGs, see the OpenTelemetry [community page][ruby-sig].
7777

78+
## Database semantic convention stability
79+
80+
In the OpenTelemetry ecosystem, database semantic conventions have now reached a stable state. However, the initial Mongo instrumentation was introduced before this stability was achieved, which resulted in database attributes being based on an older version of the semantic conventions.
81+
82+
To facilitate the migration to stable semantic conventions, you can use the `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable. This variable allows you to opt-in to the new stable conventions, ensuring compatibility and future-proofing your instrumentation.
83+
84+
When setting the value for `OTEL_SEMCONV_STABILITY_OPT_IN`, you can specify which conventions you wish to adopt:
85+
86+
- `database` - Emits the stable database and networking conventions and ceases emitting the old conventions previously emitted by the instrumentation.
87+
- `database/dup` - Emits both the old and stable database and networking conventions, enabling a phased rollout of the stable semantic conventions.
88+
- Default behavior (in the absence of either value) is to continue emitting the old database and networking conventions the instrumentation previously emitted.
89+
90+
During the transition from old to stable conventions, Mongo instrumentation code comes in three patch versions: `dup`, `old`, and `stable`. These versions are identical except for the attributes they send. Any changes to Mongo instrumentation should consider all three patches.
91+
92+
For additional information on migration, please refer to our [documentation](https://opentelemetry.io/docs/specs/semconv/non-normative/db-migration/).
93+
7894
## License
7995

8096
Apache 2.0 license. See [LICENSE][license-github] for more information.

instrumentation/mongo/lib/opentelemetry/instrumentation/mongo/instrumentation.rb

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,53 @@ class Instrumentation < OpenTelemetry::Instrumentation::Base
3535
option :peer_service, default: nil, validate: :string
3636
option :db_statement, default: :obfuscate, validate: %I[omit obfuscate include]
3737

38+
attr_reader :semconv
39+
3840
private
3941

4042
def gem_version
4143
Gem::Version.new(::Mongo::VERSION)
4244
end
4345

46+
def determine_semconv
47+
opt_in = ENV.fetch('OTEL_SEMCONV_STABILITY_OPT_IN', nil)
48+
return :old if opt_in.nil?
49+
50+
opt_in_values = opt_in.split(',').map(&:strip)
51+
52+
if opt_in_values.include?('database/dup')
53+
:dup
54+
elsif opt_in_values.include?('database')
55+
:stable
56+
else
57+
:old
58+
end
59+
end
60+
4461
def require_dependencies
45-
require_relative 'subscriber'
62+
@semconv = determine_semconv
63+
64+
case @semconv
65+
when :old
66+
require_relative 'subscribers/old/subscriber'
67+
when :stable
68+
require_relative 'subscribers/stable/subscriber'
69+
when :dup
70+
require_relative 'subscribers/dup/subscriber'
71+
end
4672
end
4773

4874
def register_subscriber
75+
subscriber_class = case @semconv
76+
when :stable
77+
Subscribers::Stable::Subscriber
78+
when :dup
79+
Subscribers::Dup::Subscriber
80+
else
81+
Subscribers::Old::Subscriber
82+
end
4983
# Subscribe to all COMMAND queries with our subscriber class
50-
::Mongo::Monitoring::Global.subscribe(::Mongo::Monitoring::COMMAND, Subscriber.new)
84+
::Mongo::Monitoring::Global.subscribe(::Mongo::Monitoring::COMMAND, subscriber_class.new)
5185
end
5286
end
5387
end

instrumentation/mongo/lib/opentelemetry/instrumentation/mongo/subscriber.rb

Lines changed: 0 additions & 107 deletions
This file was deleted.
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# frozen_string_literal: true
2+
3+
# Copyright The OpenTelemetry Authors
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
require_relative '../../command_serializer'
8+
9+
module OpenTelemetry
10+
module Instrumentation
11+
module Mongo
12+
module Subscribers
13+
module Dup
14+
# Event handler class for Mongo Ruby driver (dup mode - emits both old and new semantic conventions)
15+
class Subscriber
16+
THREAD_KEY = :__opentelemetry_mongo_spans__
17+
18+
def started(event)
19+
# start a trace and store it in the current thread; using the `operation_id`
20+
# is safe since it's a unique id used to link events together. Also only one
21+
# thread is involved in this execution so thread-local storage should be safe. Reference:
22+
# https://github.com/mongodb/mongo-ruby-driver/blob/master/lib/mongo/monitoring.rb#L70
23+
# https://github.com/mongodb/mongo-ruby-driver/blob/master/lib/mongo/monitoring/publishable.rb#L38-L56
24+
25+
collection = get_collection(event.command)
26+
27+
# Old conventions
28+
attributes = {
29+
'db.system' => 'mongodb',
30+
'db.name' => event.database_name,
31+
'db.operation' => event.command_name,
32+
'net.peer.name' => event.address.host,
33+
'net.peer.port' => event.address.port
34+
}
35+
36+
# New stable conventions
37+
attributes['db.system.name'] = 'mongodb'
38+
attributes['db.namespace'] = event.database_name
39+
attributes['db.operation.name'] = event.command_name
40+
attributes['server.address'] = event.address.host
41+
attributes['server.port'] = event.address.port
42+
43+
config = Mongo::Instrumentation.instance.config
44+
attributes['peer.service'] = config[:peer_service] if config[:peer_service]
45+
omit = config[:db_statement] == :omit
46+
obfuscate = config[:db_statement] == :obfuscate
47+
unless omit
48+
serialized = CommandSerializer.new(event.command, obfuscate).serialize
49+
# Both old and new attributes
50+
attributes['db.statement'] = serialized
51+
attributes['db.query.text'] = serialized
52+
end
53+
if collection
54+
# Both old and new attributes
55+
attributes['db.mongodb.collection'] = collection
56+
attributes['db.collection.name'] = collection
57+
end
58+
attributes.compact!
59+
60+
span = tracer.start_span(span_name(collection, event.command_name), attributes: attributes, kind: :client)
61+
set_span(event, span)
62+
end
63+
64+
def failed(event)
65+
finish_event('failed', event) do |span|
66+
if event.is_a?(::Mongo::Monitoring::Event::CommandFailed)
67+
error_type = extract_error_type(event)
68+
# New stable convention attributes
69+
span.set_attribute('error.type', error_type)
70+
status_code = event.failure['code']
71+
span.set_attribute('db.response.status_code', status_code.to_s) if status_code
72+
span.add_event('exception',
73+
attributes: {
74+
'exception.type' => 'CommandFailed',
75+
'exception.message' => event.message
76+
})
77+
span.status = OpenTelemetry::Trace::Status.error(event.message)
78+
end
79+
end
80+
end
81+
82+
def succeeded(event)
83+
finish_event('succeeded', event)
84+
end
85+
86+
private
87+
88+
def finish_event(name, event)
89+
span = get_span(event)
90+
return unless span
91+
92+
yield span if block_given?
93+
rescue StandardError => e
94+
OpenTelemetry.logger.debug("error when handling MongoDB '#{name}' event: #{e}")
95+
ensure
96+
# finish span to prevent leak and remove it from thread storage
97+
span&.finish
98+
clear_span(event)
99+
end
100+
101+
def span_name(collection, command_name)
102+
return command_name unless collection
103+
104+
"#{command_name} #{collection}"
105+
end
106+
107+
def get_collection(command)
108+
collection = command.values.first
109+
collection if collection.is_a?(String)
110+
end
111+
112+
def get_span(event)
113+
Thread.current[THREAD_KEY]&.[](event.request_id)
114+
end
115+
116+
def set_span(event, span)
117+
Thread.current[THREAD_KEY] ||= {}
118+
Thread.current[THREAD_KEY][event.request_id] = span
119+
end
120+
121+
def clear_span(event)
122+
Thread.current[THREAD_KEY]&.delete(event.request_id)
123+
end
124+
125+
def extract_error_type(event)
126+
event.failure['codeName'] || 'CommandFailed'
127+
end
128+
129+
def tracer
130+
Mongo::Instrumentation.instance.tracer
131+
end
132+
end
133+
end
134+
end
135+
end
136+
end
137+
end

0 commit comments

Comments
 (0)