From a61e6eca15e165458f974dcbafb66547b95d2e18 Mon Sep 17 00:00:00 2001 From: xuan-cao-swi Date: Tue, 30 Jun 2026 13:30:19 -0400 Subject: [PATCH 1/5] mongo patch --- .../patch/tag_sql/sw_dbo_utils.rb | 27 +++++++++----- .../patch/tag_sql/sw_mongo_patch.rb | 37 +++++++++++++++++++ lib/solarwinds_apm/patch/tag_sql_patch.rb | 1 + 3 files changed, 56 insertions(+), 9 deletions(-) create mode 100644 lib/solarwinds_apm/patch/tag_sql/sw_mongo_patch.rb diff --git a/lib/solarwinds_apm/patch/tag_sql/sw_dbo_utils.rb b/lib/solarwinds_apm/patch/tag_sql/sw_dbo_utils.rb index 7ac8332d..728c50ce 100644 --- a/lib/solarwinds_apm/patch/tag_sql/sw_dbo_utils.rb +++ b/lib/solarwinds_apm/patch/tag_sql/sw_dbo_utils.rb @@ -11,17 +11,15 @@ module SWODboUtils def self.annotate_span_and_sql(sql) return sql if sql.to_s.empty? - current_span = ::OpenTelemetry::Trace.current_span + ::OpenTelemetry::Trace.current_span + transparent = annotated_traceparent annotated_sql = '' - if current_span.context.trace_flags.sampled? - traceparent = SolarWindsAPM::Utils.traceparent_from_context(current_span.context) - annotated_traceparent = "/*traceparent='#{traceparent}'*/" - current_span.add_attributes({ 'sw.query_tag' => annotated_traceparent }) - annotated_sql = "#{sql} #{annotated_traceparent}" - else - annotated_sql = sql - end + annotated_sql = if transparent.empty? + sql + else + "#{sql} #{transparent}" + end SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] annotated_sql: #{annotated_sql}" } annotated_sql @@ -29,6 +27,17 @@ def self.annotate_span_and_sql(sql) SolarWindsAPM.logger.error { "[#{self.class}/#{__method__}] Failed to annotated sql. Error: #{e.message}" } sql end + + def self.annotated_traceparent + current_span = ::OpenTelemetry::Trace.current_span + if current_span.context.trace_flags.sampled? + traceparent = SolarWindsAPM::Utils.traceparent_from_context(current_span.context) + current_span.add_attributes({ 'sw.query_tag' => "/*traceparent='#{traceparent}'*/" }) + "/*traceparent='#{traceparent}'*/" + else + '' + end + end end end end diff --git a/lib/solarwinds_apm/patch/tag_sql/sw_mongo_patch.rb b/lib/solarwinds_apm/patch/tag_sql/sw_mongo_patch.rb new file mode 100644 index 00000000..05480d93 --- /dev/null +++ b/lib/solarwinds_apm/patch/tag_sql/sw_mongo_patch.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +# © 2023 SolarWinds Worldwide, LLC. All rights reserved. +# +# 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 SolarWindsAPM + module Patch + module TagSql + module SWOMongoPatch + def execute_with_span(span, operation) + traceparent = ::SolarWindsAPM::Patch::TagSql::SWODboUtils.annotated_traceparent + original = operation.spec[:comment].to_s + operation.spec[:comment] = original.empty? ? traceparent : "#{original}; #{traceparent}" + super + end + end + + module SWOMongoInstrumentationPatch + def build_command + traceparent = ::SolarWindsAPM::Patch::TagSql::SWODboUtils.annotated_traceparent + original = command['comment'].to_s + command['comment'] = original.empty? ? traceparent : "#{original}; #{traceparent}" + super + end + end + end + end +end + +if defined?(Mongo::Tracing::OpenTelemetry::OperationTracer) && Gem::Version.new(Mongo::VERSION) >= Gem::Version.new('2.23.0') + Mongo::Tracing::OpenTelemetry::OperationTracer.prepend(SolarWindsAPM::Patch::TagSql::SWOMongoPatch) +elsif defined?(OpenTelemetry::Instrumentation::Mongo::CommandSerializer) && Gem::Version.new(Mongo::VERSION) < Gem::Version.new('2.23.0') + OpenTelemetry::Instrumentation::Mongo::CommandSerializer.prepend(SolarWindsAPM::Patch::TagSql::SWOMongoInstrumentationPatch) +end diff --git a/lib/solarwinds_apm/patch/tag_sql_patch.rb b/lib/solarwinds_apm/patch/tag_sql_patch.rb index c19b4ded..4c2588e7 100644 --- a/lib/solarwinds_apm/patch/tag_sql_patch.rb +++ b/lib/solarwinds_apm/patch/tag_sql_patch.rb @@ -9,3 +9,4 @@ require_relative 'tag_sql/sw_dbo_utils' require_relative 'tag_sql/sw_mysql2_patch' require_relative 'tag_sql/sw_pg_patch' +require_relative 'tag_sql/sw_mongo_patch' From 7a605cdb6fcfbfb1a0afe07c9e238cbf2841d544 Mon Sep 17 00:00:00 2001 From: xuan-cao-swi Date: Wed, 8 Jul 2026 12:15:26 -0400 Subject: [PATCH 2/5] mongo patch for injecting trace context --- .../patch/tag_sql/sw_mongo_patch.rb | 18 ++- test/patch/sw_mongo_patch_integrate_test.rb | 116 ++++++++++++++++++ test/patch/sw_mongo_patch_test.rb | 29 +++++ 3 files changed, 157 insertions(+), 6 deletions(-) create mode 100644 test/patch/sw_mongo_patch_integrate_test.rb create mode 100644 test/patch/sw_mongo_patch_test.rb diff --git a/lib/solarwinds_apm/patch/tag_sql/sw_mongo_patch.rb b/lib/solarwinds_apm/patch/tag_sql/sw_mongo_patch.rb index 05480d93..09486435 100644 --- a/lib/solarwinds_apm/patch/tag_sql/sw_mongo_patch.rb +++ b/lib/solarwinds_apm/patch/tag_sql/sw_mongo_patch.rb @@ -18,11 +18,17 @@ def execute_with_span(span, operation) end end - module SWOMongoInstrumentationPatch - def build_command - traceparent = ::SolarWindsAPM::Patch::TagSql::SWODboUtils.annotated_traceparent - original = command['comment'].to_s - command['comment'] = original.empty? ? traceparent : "#{original}; #{traceparent}" + module SWOMongoPatchV2220 + # from server/connection_base.rb in mongo 2.22.0 + def deliver(message, context, options = {}) + if message.is_a?(Mongo::Protocol::Msg) + main_doc = message.instance_variable_get(:@main_document) + if main_doc + traceparent = ::SolarWindsAPM::Patch::TagSql::SWODboUtils.annotated_traceparent + original = main_doc['comment'].to_s + main_doc['comment'] = original.empty? ? traceparent : "#{original}; #{traceparent}" + end + end super end end @@ -33,5 +39,5 @@ def build_command if defined?(Mongo::Tracing::OpenTelemetry::OperationTracer) && Gem::Version.new(Mongo::VERSION) >= Gem::Version.new('2.23.0') Mongo::Tracing::OpenTelemetry::OperationTracer.prepend(SolarWindsAPM::Patch::TagSql::SWOMongoPatch) elsif defined?(OpenTelemetry::Instrumentation::Mongo::CommandSerializer) && Gem::Version.new(Mongo::VERSION) < Gem::Version.new('2.23.0') - OpenTelemetry::Instrumentation::Mongo::CommandSerializer.prepend(SolarWindsAPM::Patch::TagSql::SWOMongoInstrumentationPatch) + Mongo::Server::ConnectionBase.prepend(SolarWindsAPM::Patch::TagSql::SWOMongoPatchV2220) end diff --git a/test/patch/sw_mongo_patch_integrate_test.rb b/test/patch/sw_mongo_patch_integrate_test.rb new file mode 100644 index 00000000..323bae64 --- /dev/null +++ b/test/patch/sw_mongo_patch_integrate_test.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +# Copyright (c) 2023 SolarWinds, LLC. +# All rights reserved. + +require 'minitest_helper' +require './lib/solarwinds_apm/config' +require './lib/solarwinds_apm/opentelemetry' +require './lib/solarwinds_apm/support/txn_name_manager' +require './lib/solarwinds_apm/otel_config' +require './lib/solarwinds_apm/api' +require './lib/solarwinds_apm/support' +require './lib/solarwinds_apm/patch/tag_sql/sw_dbo_utils' +require './lib/solarwinds_apm/patch/tag_sql/sw_mongo_patch' + +# rubocop:disable Naming/MethodName +module SolarWindsAPM + module Span + def self.createSpan(trans_name, domain, span_time, has_error); end + end +end +# rubocop:enable Naming/MethodName + +# MongoDB and its OpenTelemetry instrumentation are not available in the test +# environment, so stub the two patch targets to exercise the real patch modules +# without a live MongoDB connection. +class FakeMongoOperationTracer + def execute_with_span(_span, operation) + operation + end +end +FakeMongoOperationTracer.prepend(SolarWindsAPM::Patch::TagSql::SWOMongoPatch) + +# Stub the Mongo::Protocol::Msg class so the patch's type check passes. +module Mongo + module Protocol + class Msg + def initialize(main_document = {}) + @main_document = main_document + end + end + end +end + +class FakeMongoConnectionBase + def deliver(message, _context, _options = {}) + message + end +end +FakeMongoConnectionBase.prepend(SolarWindsAPM::Patch::TagSql::SWOMongoPatchV2220) + +describe 'mongo patch integrate test' do + puts "\n\033[1m=== TEST RUN MONGO PATCH TEST: #{RUBY_VERSION} #{File.basename(__FILE__)} #{Time.now.strftime('%Y-%m-%d %H:%M')} ===\033[0m\n" + + let(:sdk) { OpenTelemetry::SDK } + let(:exporter) { sdk::Trace::Export::InMemorySpanExporter.new } + let(:span_processor) { sdk::Trace::Export::SimpleSpanProcessor.new(exporter) } + + before do + OpenTelemetry::SDK.configure + OpenTelemetry.tracer_provider.add_span_processor(span_processor) + @tracer = OpenTelemetry.tracer_provider.tracer('mongo-patch-test') + end + + it 'injects traceparent comment into the operation spec and sets sw.query_tag attribute' do + operation = Struct.new(:spec).new({}) + + @tracer.in_span('mongo-op') do |_span| + FakeMongoOperationTracer.new.execute_with_span(nil, operation) + end + + finished_spans = exporter.finished_spans + + pattern = %r{/\*traceparent='[\da-f-]+'*\*/$} + assert_match pattern, finished_spans[0].attributes['sw.query_tag'], "Doesn't match sw.query_tag" + assert_match pattern, operation.spec[:comment], "operation comment doesn't contain traceparent" + end + + it 'appends traceparent comment when the operation already has a comment' do + operation = Struct.new(:spec).new({ comment: 'existing' }) + + @tracer.in_span('mongo-op') do |_span| + FakeMongoOperationTracer.new.execute_with_span(nil, operation) + end + + pattern = %r{^existing; /\*traceparent='[\da-f-]+'*\*/$} + assert_match pattern, operation.spec[:comment], "operation comment doesn't append traceparent" + end + + it 'injects traceparent comment into the message main document and sets sw.query_tag attribute' do + message = Mongo::Protocol::Msg.new({}) + + @tracer.in_span('mongo-op') do |_span| + FakeMongoConnectionBase.new.deliver(message, nil) + end + + finished_spans = exporter.finished_spans + + main_doc = message.instance_variable_get(:@main_document) + pattern = %r{/\*traceparent='[\da-f-]+'*\*/$} + assert_match pattern, finished_spans[0].attributes['sw.query_tag'], "Doesn't match sw.query_tag" + assert_match pattern, main_doc['comment'], "message comment doesn't contain traceparent" + end + + it 'appends traceparent comment when the message main document already has a comment' do + message = Mongo::Protocol::Msg.new({ 'comment' => 'existing' }) + + @tracer.in_span('mongo-op') do |_span| + FakeMongoConnectionBase.new.deliver(message, nil) + end + + main_doc = message.instance_variable_get(:@main_document) + pattern = %r{^existing; /\*traceparent='[\da-f-]+'*\*/$} + assert_match pattern, main_doc['comment'], "message comment doesn't append traceparent" + end +end diff --git a/test/patch/sw_mongo_patch_test.rb b/test/patch/sw_mongo_patch_test.rb new file mode 100644 index 00000000..ac4de54a --- /dev/null +++ b/test/patch/sw_mongo_patch_test.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +# Copyright (c) 2023 SolarWinds, LLC. +# All rights reserved. + +require 'minitest_helper' +require './lib/solarwinds_apm/config' +require './lib/solarwinds_apm/opentelemetry' +require './lib/solarwinds_apm/support/txn_name_manager' +require './lib/solarwinds_apm/otel_config' + +describe 'mongo patch test' do + puts "\n\033[1m=== TEST RUN MONGO PATCH TEST: #{RUBY_VERSION} #{File.basename(__FILE__)} #{Time.now.strftime('%Y-%m-%d %H:%M')} ===\033[0m\n" + + it 'does not prepend the SWO mongo patch when tag_sql is false' do + SolarWindsAPM::Config[:tag_sql] = false + SolarWindsAPM::OTelConfig.initialize + + if defined?(Mongo::Tracing::OpenTelemetry::OperationTracer) && Gem::Version.new(Mongo::VERSION) >= Gem::Version.new('2.23.0') + tracer_ancestors = Mongo::Tracing::OpenTelemetry::OperationTracer.ancestors + refute_includes tracer_ancestors, SolarWindsAPM::Patch::TagSql::SWOMongoPatch + elsif defined?(OpenTelemetry::Instrumentation::Mongo::CommandSerializer) && Gem::Version.new(Mongo::VERSION) < Gem::Version.new('2.23.0') + connection_ancestors = Mongo::Server::ConnectionBase.ancestors + refute_includes connection_ancestors, SolarWindsAPM::Patch::TagSql::SWOMongoPatchV2220 + else + skip 'MongoDB instrumentation is not available in this environment' + end + end +end From 22195630898fa4519b84879441edd11b03f7d38d Mon Sep 17 00:00:00 2001 From: xuan-cao-swi Date: Wed, 15 Jul 2026 13:08:50 -0400 Subject: [PATCH 3/5] rely on deliver function only --- .../patch/tag_sql/sw_mongo_patch.rb | 80 ++++++++++++++----- 1 file changed, 60 insertions(+), 20 deletions(-) diff --git a/lib/solarwinds_apm/patch/tag_sql/sw_mongo_patch.rb b/lib/solarwinds_apm/patch/tag_sql/sw_mongo_patch.rb index 09486435..8a047079 100644 --- a/lib/solarwinds_apm/patch/tag_sql/sw_mongo_patch.rb +++ b/lib/solarwinds_apm/patch/tag_sql/sw_mongo_patch.rb @@ -10,34 +10,74 @@ module SolarWindsAPM module Patch module TagSql module SWOMongoPatch - def execute_with_span(span, operation) - traceparent = ::SolarWindsAPM::Patch::TagSql::SWODboUtils.annotated_traceparent - original = operation.spec[:comment].to_s - operation.spec[:comment] = original.empty? ? traceparent : "#{original}; #{traceparent}" + # Annotate the final OP_MSG document rather than the operation spec. + # + # ConnectionBase#deliver is the shared boundary immediately before + # serialization, so this works independently of the driver's + # OpenTelemetry implementation and its internal tracer classes. + def deliver(message, context, options = {}) + annotate_message_comment(message) super end - end + private :deliver - module SWOMongoPatchV2220 - # from server/connection_base.rb in mongo 2.22.0 - def deliver(message, context, options = {}) - if message.is_a?(Mongo::Protocol::Msg) - main_doc = message.instance_variable_get(:@main_document) - if main_doc - traceparent = ::SolarWindsAPM::Patch::TagSql::SWODboUtils.annotated_traceparent - original = main_doc['comment'].to_s - main_doc['comment'] = original.empty? ? traceparent : "#{original}; #{traceparent}" - end + private + + def annotate_message_comment(message) + return unless defined?(::Mongo::Protocol::Msg) && + message.is_a?(::Mongo::Protocol::Msg) + + main_document = message.instance_variable_get(:@main_document) + return unless main_document + + traceparent = ::SolarWindsAPM::Patch::TagSql::SWODboUtils.annotated_traceparent + return if traceparent.nil? || traceparent.empty? + + key = comment_key(main_document) + main_document[key] = annotated_comment(main_document[key], traceparent) + end + + def comment_key(main_document) + return 'comment' if main_document.key?('comment') + return :comment if main_document.key?(:comment) + + 'comment' + end + + def annotated_comment(original, traceparent) + case original + when nil + traceparent + when String + original.empty? ? traceparent : "#{original}; #{traceparent}" + else + # MongoDB permits BSON values other than strings for comment. + # Wrap the value so instrumentation does not coerce or discard + # the user's original comment type and content. + BSON::Document.new( + 'swo_original_comment' => original, + 'traceparent' => traceparent + ) end - super end end end end end -if defined?(Mongo::Tracing::OpenTelemetry::OperationTracer) && Gem::Version.new(Mongo::VERSION) >= Gem::Version.new('2.23.0') - Mongo::Tracing::OpenTelemetry::OperationTracer.prepend(SolarWindsAPM::Patch::TagSql::SWOMongoPatch) -elsif defined?(OpenTelemetry::Instrumentation::Mongo::CommandSerializer) && Gem::Version.new(Mongo::VERSION) < Gem::Version.new('2.23.0') - Mongo::Server::ConnectionBase.prepend(SolarWindsAPM::Patch::TagSql::SWOMongoPatchV2220) +# ConnectionBase#deliver exists before 2.22.0 and remains the common +# serialization path in 2.23.0 and newer. Do not couple this patch to +# either of the driver's OpenTelemetry integration implementations. +if defined?(::Mongo::Server::ConnectionBase) + ::Mongo::Server::ConnectionBase.prepend( + ::SolarWindsAPM::Patch::TagSql::SWOMongoPatch + ) end + +# Why use execute_with_span or not execute_with_span? +# +# execute_with_span only runs when the driver's OpenTelemetry tracing is enabled. +# Look at the real entry point, Tracer#trace_operation in tracer.rb:72-76: +# And enabled? is driven by OTEL_RUBY_INSTRUMENTATION_MONGODB_ENABLED (tracer.rb:42-46). +# So if that env var isn't set to true/1/yes, OperationTracer#trace_operation is never called, +# and therefore your prepended execute_with_span never fires — your traceparent silently disappears. From 6674137de0cbef8c8ee2f19d85a89040e364e4a1 Mon Sep 17 00:00:00 2001 From: xuan-cao-swi Date: Wed, 15 Jul 2026 13:09:32 -0400 Subject: [PATCH 4/5] use updated annotated_comment for json and bson object (flatten) --- .../patch/tag_sql/sw_mongo_patch.rb | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/solarwinds_apm/patch/tag_sql/sw_mongo_patch.rb b/lib/solarwinds_apm/patch/tag_sql/sw_mongo_patch.rb index 8a047079..408da9f1 100644 --- a/lib/solarwinds_apm/patch/tag_sql/sw_mongo_patch.rb +++ b/lib/solarwinds_apm/patch/tag_sql/sw_mongo_patch.rb @@ -50,10 +50,19 @@ def annotated_comment(original, traceparent) traceparent when String original.empty? ? traceparent : "#{original}; #{traceparent}" + when Hash, BSON::Document + # Real document: add traceparent as a sibling key. + # Guard against clobbering a user key of the same name. + doc = BSON::Document.new(original) + if doc.key?('traceparent') || doc.key?(:traceparent) + doc['swo_traceparent'] = traceparent + else + doc['traceparent'] = traceparent + end + doc else - # MongoDB permits BSON values other than strings for comment. - # Wrap the value so instrumentation does not coerce or discard - # the user's original comment type and content. + # Scalar BSON value (ObjectId, Time, Integer, Float, true/false, ...). + # These have no fields to extend, so wrap them in a new document. BSON::Document.new( 'swo_original_comment' => original, 'traceparent' => traceparent From 6d9763e3140e9863579fa2538485ddeaae805e5f Mon Sep 17 00:00:00 2001 From: xuan-cao-swi Date: Thu, 16 Jul 2026 14:43:37 -0400 Subject: [PATCH 5/5] update test case --- Gemfile | 1 + test/patch/sw_mongo_patch_integrate_test.rb | 107 ++++++++++---------- test/patch/sw_mongo_patch_test.rb | 14 +-- 3 files changed, 58 insertions(+), 64 deletions(-) diff --git a/Gemfile b/Gemfile index 419321ac..8a5cc8e4 100644 --- a/Gemfile +++ b/Gemfile @@ -36,6 +36,7 @@ group :development, :test do gem 'simplecov-console', require: false, group: :test gem 'webmock' if RUBY_VERSION >= '2.0.0' gem 'base64' if RUBY_VERSION >= '3.4.0' + gem 'mongo', '>= 2.24.1' gem 'opentelemetry-propagator-b3' gem 'opentelemetry-test-helpers' diff --git a/test/patch/sw_mongo_patch_integrate_test.rb b/test/patch/sw_mongo_patch_integrate_test.rb index 323bae64..72fd9801 100644 --- a/test/patch/sw_mongo_patch_integrate_test.rb +++ b/test/patch/sw_mongo_patch_integrate_test.rb @@ -4,6 +4,7 @@ # All rights reserved. require 'minitest_helper' +require 'mongo' require './lib/solarwinds_apm/config' require './lib/solarwinds_apm/opentelemetry' require './lib/solarwinds_apm/support/txn_name_manager' @@ -21,33 +22,16 @@ def self.createSpan(trans_name, domain, span_time, has_error); end end # rubocop:enable Naming/MethodName -# MongoDB and its OpenTelemetry instrumentation are not available in the test -# environment, so stub the two patch targets to exercise the real patch modules -# without a live MongoDB connection. -class FakeMongoOperationTracer - def execute_with_span(_span, operation) - operation - end -end -FakeMongoOperationTracer.prepend(SolarWindsAPM::Patch::TagSql::SWOMongoPatch) - -# Stub the Mongo::Protocol::Msg class so the patch's type check passes. -module Mongo - module Protocol - class Msg - def initialize(main_document = {}) - @main_document = main_document - end - end - end -end - +# ConnectionBase#deliver performs a live socket write, so exercise the real +# patch module through a test double that returns the delivered message. The +# mongo gem is installed for tests, so the real Mongo::Protocol::Msg is used. class FakeMongoConnectionBase def deliver(message, _context, _options = {}) message end + private :deliver end -FakeMongoConnectionBase.prepend(SolarWindsAPM::Patch::TagSql::SWOMongoPatchV2220) +FakeMongoConnectionBase.prepend(SolarWindsAPM::Patch::TagSql::SWOMongoPatch) describe 'mongo patch integrate test' do puts "\n\033[1m=== TEST RUN MONGO PATCH TEST: #{RUBY_VERSION} #{File.basename(__FILE__)} #{Time.now.strftime('%Y-%m-%d %H:%M')} ===\033[0m\n" @@ -55,6 +39,7 @@ def deliver(message, _context, _options = {}) let(:sdk) { OpenTelemetry::SDK } let(:exporter) { sdk::Trace::Export::InMemorySpanExporter.new } let(:span_processor) { sdk::Trace::Export::SimpleSpanProcessor.new(exporter) } + let(:traceparent_pattern) { %r{/\*traceparent='[\da-f-]+'*\*/$} } before do OpenTelemetry::SDK.configure @@ -62,55 +47,69 @@ def deliver(message, _context, _options = {}) @tracer = OpenTelemetry.tracer_provider.tracer('mongo-patch-test') end - it 'injects traceparent comment into the operation spec and sets sw.query_tag attribute' do - operation = Struct.new(:spec).new({}) - + def deliver_in_span(message) @tracer.in_span('mongo-op') do |_span| - FakeMongoOperationTracer.new.execute_with_span(nil, operation) + FakeMongoConnectionBase.new.send(:deliver, message, nil) end + message.instance_variable_get(:@main_document) + end + + it 'injects traceparent comment into the message main document and sets sw.query_tag attribute' do + message = Mongo::Protocol::Msg.new([], {}, {}) + + main_doc = deliver_in_span(message) finished_spans = exporter.finished_spans + assert_match traceparent_pattern, finished_spans[0].attributes['sw.query_tag'], "Doesn't match sw.query_tag" + assert_match traceparent_pattern, main_doc['comment'], "message comment doesn't contain traceparent" + end + + it 'appends traceparent comment when the message main document already has a string comment' do + message = Mongo::Protocol::Msg.new([], {}, { 'comment' => 'existing' }) - pattern = %r{/\*traceparent='[\da-f-]+'*\*/$} - assert_match pattern, finished_spans[0].attributes['sw.query_tag'], "Doesn't match sw.query_tag" - assert_match pattern, operation.spec[:comment], "operation comment doesn't contain traceparent" + main_doc = deliver_in_span(message) + + assert_match %r{^existing; /\*traceparent='[\da-f-]+'*\*/$}, main_doc['comment'], "message comment doesn't append traceparent" end - it 'appends traceparent comment when the operation already has a comment' do - operation = Struct.new(:spec).new({ comment: 'existing' }) + it 'appends traceparent comment when the main document uses a symbol comment key' do + message = Mongo::Protocol::Msg.new([], {}, { comment: 'existing' }) - @tracer.in_span('mongo-op') do |_span| - FakeMongoOperationTracer.new.execute_with_span(nil, operation) - end + main_doc = deliver_in_span(message) - pattern = %r{^existing; /\*traceparent='[\da-f-]+'*\*/$} - assert_match pattern, operation.spec[:comment], "operation comment doesn't append traceparent" + assert_match %r{^existing; /\*traceparent='[\da-f-]+'*\*/$}, main_doc[:comment], "message comment doesn't append traceparent" end - it 'injects traceparent comment into the message main document and sets sw.query_tag attribute' do - message = Mongo::Protocol::Msg.new({}) + it 'adds traceparent as a sibling key when the comment is a document' do + message = Mongo::Protocol::Msg.new([], {}, { 'comment' => { 'foo' => 'bar' } }) - @tracer.in_span('mongo-op') do |_span| - FakeMongoConnectionBase.new.deliver(message, nil) - end + main_doc = deliver_in_span(message) - finished_spans = exporter.finished_spans + comment = main_doc['comment'] + assert_instance_of BSON::Document, comment + assert_equal 'bar', comment['foo'] + assert_match traceparent_pattern, comment['traceparent'], "document comment doesn't contain traceparent" + end + + it 'preserves a user traceparent key by storing the trace under swo_traceparent' do + message = Mongo::Protocol::Msg.new([], {}, { 'comment' => { 'traceparent' => 'user-value' } }) - main_doc = message.instance_variable_get(:@main_document) - pattern = %r{/\*traceparent='[\da-f-]+'*\*/$} - assert_match pattern, finished_spans[0].attributes['sw.query_tag'], "Doesn't match sw.query_tag" - assert_match pattern, main_doc['comment'], "message comment doesn't contain traceparent" + main_doc = deliver_in_span(message) + + comment = main_doc['comment'] + assert_instance_of BSON::Document, comment + assert_equal 'user-value', comment['traceparent'] + assert_match traceparent_pattern, comment['swo_traceparent'], "document comment doesn't preserve user traceparent" end - it 'appends traceparent comment when the message main document already has a comment' do - message = Mongo::Protocol::Msg.new({ 'comment' => 'existing' }) + it 'wraps a scalar comment in a document with the original value' do + message = Mongo::Protocol::Msg.new([], {}, { 'comment' => 42 }) - @tracer.in_span('mongo-op') do |_span| - FakeMongoConnectionBase.new.deliver(message, nil) - end + main_doc = deliver_in_span(message) - main_doc = message.instance_variable_get(:@main_document) - pattern = %r{^existing; /\*traceparent='[\da-f-]+'*\*/$} - assert_match pattern, main_doc['comment'], "message comment doesn't append traceparent" + comment = main_doc['comment'] + assert_instance_of BSON::Document, comment + assert_equal 42, comment['swo_original_comment'] + assert_match traceparent_pattern, comment['traceparent'], "scalar comment doesn't contain traceparent" end end diff --git a/test/patch/sw_mongo_patch_test.rb b/test/patch/sw_mongo_patch_test.rb index ac4de54a..c2ef3057 100644 --- a/test/patch/sw_mongo_patch_test.rb +++ b/test/patch/sw_mongo_patch_test.rb @@ -4,6 +4,7 @@ # All rights reserved. require 'minitest_helper' +require 'mongo' require './lib/solarwinds_apm/config' require './lib/solarwinds_apm/opentelemetry' require './lib/solarwinds_apm/support/txn_name_manager' @@ -12,18 +13,11 @@ describe 'mongo patch test' do puts "\n\033[1m=== TEST RUN MONGO PATCH TEST: #{RUBY_VERSION} #{File.basename(__FILE__)} #{Time.now.strftime('%Y-%m-%d %H:%M')} ===\033[0m\n" - it 'does not prepend the SWO mongo patch when tag_sql is false' do + it 'does not prepend the SWO mongo patch to Mongo::Server::ConnectionBase when tag_sql is false' do SolarWindsAPM::Config[:tag_sql] = false SolarWindsAPM::OTelConfig.initialize - if defined?(Mongo::Tracing::OpenTelemetry::OperationTracer) && Gem::Version.new(Mongo::VERSION) >= Gem::Version.new('2.23.0') - tracer_ancestors = Mongo::Tracing::OpenTelemetry::OperationTracer.ancestors - refute_includes tracer_ancestors, SolarWindsAPM::Patch::TagSql::SWOMongoPatch - elsif defined?(OpenTelemetry::Instrumentation::Mongo::CommandSerializer) && Gem::Version.new(Mongo::VERSION) < Gem::Version.new('2.23.0') - connection_ancestors = Mongo::Server::ConnectionBase.ancestors - refute_includes connection_ancestors, SolarWindsAPM::Patch::TagSql::SWOMongoPatchV2220 - else - skip 'MongoDB instrumentation is not available in this environment' - end + connection_ancestors = Mongo::Server::ConnectionBase.ancestors.map(&:to_s) + refute_includes connection_ancestors, 'SolarWindsAPM::Patch::TagSql::SWOMongoPatch' end end