From c674b40a8856a0ae236ce043dac0a4714790c2f2 Mon Sep 17 00:00:00 2001 From: Ariel Valentin Date: Wed, 26 Nov 2025 22:57:23 -0600 Subject: [PATCH 1/5] feat: Add SQL Comment Propagator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements a helper to propagate context via SQL comments as specified here: > Instrumentations MAY propagate context using SQL commenter by injecting comments into SQL queries before execution. SQL commenter-based context propagation SHOULD NOT be enabled by default, but instrumentation MAY allow users to opt into it. > > The instrumentation implementation SHOULD append the comment to the end of the query. Semantic conventions for individual database systems MAY specify different format, which may include different position, encoding, or schema, depending on the specific database system’s requirements or preferences. > > The instrumentation SHOULD allow users to pass a propagator to overwrite the global propagator. If no propagator is provided by the user, instrumentation SHOULD use the global propagator. https://opentelemetry.io/docs/specs/semconv/database/database-spans/#context-propagation This helper conforms to the propagator inject interface, since it does not ever extract values we are skipping that implementation. Once the processors replaces the obfuscator then we can include the propagators in the SQL gems for trilogy, mysql2, and pg --- .toys/.data/releases.yml | 2 +- helpers/sql-processor/Gemfile | 1 + .../opentelemetry/helpers/sql_processor.rb | 4 +- .../helpers/sql_processor/commenter.rb | 94 ++++++++++++++ ...pentelemetry-helpers-sql-processor.gemspec | 1 + .../helpers/sql_processor/commenter_test.rb | 120 ++++++++++++++++++ helpers/sql-processor/test/test_helper.rb | 6 + 7 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 helpers/sql-processor/lib/opentelemetry/helpers/sql_processor/commenter.rb create mode 100644 helpers/sql-processor/test/opentelemetry/helpers/sql_processor/commenter_test.rb diff --git a/.toys/.data/releases.yml b/.toys/.data/releases.yml index 2a259590f3..720e3d1b87 100644 --- a/.toys/.data/releases.yml +++ b/.toys/.data/releases.yml @@ -300,4 +300,4 @@ gems: - name: opentelemetry-sampler-xray directory: sampler/xray - version_constant: [OpenTelemetry, Sampler, XRay, VERSION] \ No newline at end of file + version_constant: [OpenTelemetry, Sampler, XRay, VERSION] diff --git a/helpers/sql-processor/Gemfile b/helpers/sql-processor/Gemfile index f77eaf288e..d590696acf 100644 --- a/helpers/sql-processor/Gemfile +++ b/helpers/sql-processor/Gemfile @@ -11,6 +11,7 @@ gemspec group :test do gem 'bundler', '~> 2.4' gem 'minitest', '~> 5.0' + gem 'opentelemetry-sdk', '~> 1.5' gem 'opentelemetry-test-helpers', '~> 0.3' gem 'rake', '~> 13.0' gem 'rubocop', '~> 1.79.1' diff --git a/helpers/sql-processor/lib/opentelemetry/helpers/sql_processor.rb b/helpers/sql-processor/lib/opentelemetry/helpers/sql_processor.rb index b096a25a5b..dd660c8553 100644 --- a/helpers/sql-processor/lib/opentelemetry/helpers/sql_processor.rb +++ b/helpers/sql-processor/lib/opentelemetry/helpers/sql_processor.rb @@ -6,13 +6,15 @@ require 'opentelemetry-common' require_relative 'sql_processor/obfuscator' +require_relative 'sql_processor/commenter' module OpenTelemetry module Helpers # SQL processing utilities for OpenTelemetry instrumentation. # # This module provides a unified interface for SQL processing operations - # commonly needed in database adapter instrumentation, including SQL obfuscation. + # commonly needed in database adapter instrumentation, including SQL obfuscation + # and SQL comment-based trace context propagation. # # @api public module SqlProcessor diff --git a/helpers/sql-processor/lib/opentelemetry/helpers/sql_processor/commenter.rb b/helpers/sql-processor/lib/opentelemetry/helpers/sql_processor/commenter.rb new file mode 100644 index 0000000000..383026c721 --- /dev/null +++ b/helpers/sql-processor/lib/opentelemetry/helpers/sql_processor/commenter.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'cgi' + +module OpenTelemetry + module Helpers + module SqlProcessor + # SqlCommenter provides SQL comment-based trace context propagation + # according to the SQL Commenter specification. + # + # This module implements a propagator interface compatible with Vitess, + # allowing it to be used as a drop-in replacement. + # + # @api public + module SqlCommenter + extend self + + # SqlQuerySetter is responsible for formatting trace context as SQL comments + # and appending them to SQL queries according to the SQL Commenter specification. + # + # Format: /*key='value',key2='value2'*/ + # Values are URL-encoded per the SQL Commenter spec + module SqlQuerySetter + extend self + + # Appends trace context as a SQL comment to the carrier (SQL query string) + # + # @param carrier [String] The SQL query string to modify + # @param headers [Hash] Hash of trace context headers (e.g., {'traceparent' => '00-...'}) + def set(carrier, headers) + return if headers.empty? + return if carrier.frozen? + + # Convert headers hash to SQL commenter format + # Format: /*key1='value1',key2='value2'*/ + comment_parts = headers.map do |key, value| + # URL encode values as per SQL Commenter spec (using URI component encoding) + encoded_value = CGI.escapeURIComponent(value.to_s) + "#{key}='#{encoded_value}'" + end + + comment = "/*#{comment_parts.join(',')}*/" + + # Append to end of query (spec recommendation) + carrier.concat(" #{comment}") + end + end + + # SqlQueryPropagator propagates trace context using SQL comments + # according to the SQL Commenter specification. + # + # This propagator implements the same interface as the Vitess propagator + # and can be used as a drop-in replacement. + # + # @example + # propagator = OpenTelemetry::Helpers::SqlProcessor::SqlCommenter.sql_query_propagator + # sql = "SELECT * FROM users" + # propagator.inject(sql, context: current_context) + # # => "SELECT * FROM users /*traceparent='00-...',tracestate='...'*/" + module SqlQueryPropagator + extend self + + # Injects trace context into a SQL query as a comment + # + # @param carrier [String] The SQL query string to inject context into + # @param context [optional, Context] The context to inject. Defaults to current context. + # @param setter [optional, #set] The setter to use for appending the comment. + # Defaults to SqlQuerySetter. + # @return [nil] + def inject(carrier, context: OpenTelemetry::Context.current, setter: SqlQuerySetter) + # Use the global propagator to extract headers into a hash + headers = {} + OpenTelemetry.propagation.inject(headers, context: context) + + # Pass the headers to our SQL comment setter + setter.set(carrier, headers) + nil + end + end + + # Returns the SqlQueryPropagator module for stateless propagation + # + # @return [Module] The SqlQueryPropagator module + def sql_query_propagator + SqlQueryPropagator + end + end + end + end +end diff --git a/helpers/sql-processor/opentelemetry-helpers-sql-processor.gemspec b/helpers/sql-processor/opentelemetry-helpers-sql-processor.gemspec index 07db51e0be..972b29d574 100644 --- a/helpers/sql-processor/opentelemetry-helpers-sql-processor.gemspec +++ b/helpers/sql-processor/opentelemetry-helpers-sql-processor.gemspec @@ -25,6 +25,7 @@ Gem::Specification.new do |spec| spec.require_paths = ['lib'] spec.required_ruby_version = '>= 3.2' + spec.add_dependency 'opentelemetry-api', '~> 1.0' spec.add_dependency 'opentelemetry-common', '~> 0.21' if spec.respond_to?(:metadata) diff --git a/helpers/sql-processor/test/opentelemetry/helpers/sql_processor/commenter_test.rb b/helpers/sql-processor/test/opentelemetry/helpers/sql_processor/commenter_test.rb new file mode 100644 index 0000000000..a504cfa60c --- /dev/null +++ b/helpers/sql-processor/test/opentelemetry/helpers/sql_processor/commenter_test.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::Helpers::SqlProcessor::SqlCommenter do + let(:span_id) { 'e457b5a2e4d86bd1' } + let(:trace_id) { '80f198ee56343ba864fe8b2a57d3eff7' } + let(:trace_flags) { OpenTelemetry::Trace::TraceFlags::SAMPLED } + + let(:context) do + OpenTelemetry::Trace.context_with_span( + OpenTelemetry::Trace.non_recording_span( + OpenTelemetry::Trace::SpanContext.new( + trace_id: Array(trace_id).pack('H*'), + span_id: Array(span_id).pack('H*'), + trace_flags: trace_flags + ) + ) + ) + end + + describe 'SqlQueryPropagator.inject' do + let(:propagator) { OpenTelemetry::Helpers::SqlProcessor::SqlCommenter.sql_query_propagator } + + it 'injects trace context into SQL' do + sql = +'SELECT * FROM users' + propagator.inject(sql, context: context) + + expected = "SELECT * FROM users /*traceparent='00-#{trace_id}-#{span_id}-01'*/" + _(sql).must_equal(expected) + end + + it 'handles frozen strings by not modifying them' do + sql = -'SELECT * FROM users' + propagator.inject(sql, context: context) + + # Frozen string should remain unchanged (setter will return early) + _(sql).must_equal('SELECT * FROM users') + end + + it 'handles empty context' do + sql = +'SELECT * FROM users' + propagator.inject(sql, context: OpenTelemetry::Context.empty) + + # Should not modify SQL when context produces no headers + _(sql).must_equal('SELECT * FROM users') + end + + it 'includes tracestate when present' do + span_context = OpenTelemetry::Trace::SpanContext.new( + trace_id: Array(trace_id).pack('H*'), + span_id: Array(span_id).pack('H*'), + trace_flags: trace_flags, + tracestate: OpenTelemetry::Trace::Tracestate.from_string('congo=t61rcWkgMzE,rojo=00f067aa0ba902b7') + ) + + ctx = OpenTelemetry::Trace.context_with_span( + OpenTelemetry::Trace.non_recording_span(span_context) + ) + + sql = +'SELECT * FROM users' + propagator.inject(sql, context: ctx) + + expected = "SELECT * FROM users /*traceparent='00-#{trace_id}-#{span_id}-01',tracestate='congo%3Dt61rcWkgMzE%2Crojo%3D00f067aa0ba902b7'*/" + _(sql).must_equal(expected) + end + + it 'returns nil' do + sql = +'SELECT * FROM users' + result = propagator.inject(sql, context: context) + + _(result).must_be_nil + end + end + + describe 'SqlQuerySetter.set' do + let(:setter) { OpenTelemetry::Helpers::SqlProcessor::SqlCommenter::SqlQuerySetter } + + it 'formats headers as SQL comments' do + sql = +'SELECT * FROM users' + headers = { 'traceparent' => '00-80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1-01' } + + setter.set(sql, headers) + + expected = "SELECT * FROM users /*traceparent='00-80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1-01'*/" + _(sql).must_equal(expected) + end + + it 'URL encodes values' do + sql = +'SELECT * FROM users' + headers = { 'key' => 'value with spaces' } + + setter.set(sql, headers) + + expected = "SELECT * FROM users /*key='value%20with%20spaces'*/" + _(sql).must_equal(expected) + end + + it 'handles empty headers' do + sql = +'SELECT * FROM users' + setter.set(sql, {}) + + _(sql).must_equal('SELECT * FROM users') + end + + it 'handles frozen strings by not modifying them' do + sql = -'SELECT * FROM users' + headers = { 'traceparent' => '00-80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1-01' } + + setter.set(sql, headers) + + # Frozen string should remain unchanged + _(sql).must_equal('SELECT * FROM users') + end + end +end diff --git a/helpers/sql-processor/test/test_helper.rb b/helpers/sql-processor/test/test_helper.rb index 419b09eb04..46734af04c 100644 --- a/helpers/sql-processor/test/test_helper.rb +++ b/helpers/sql-processor/test/test_helper.rb @@ -10,3 +10,9 @@ require 'minitest/autorun' require 'opentelemetry-helpers-sql-processor' +require 'opentelemetry/sdk' + +OpenTelemetry.logger = Logger.new($stderr, level: ENV.fetch('OTEL_LOG_LEVEL', 'fatal').to_sym) + +# Configure the SDK to set up the default propagators +OpenTelemetry::SDK.configure From 762fca4186e71072c24c6a391d6d1ea655f8e4fe Mon Sep 17 00:00:00 2001 From: Ariel Valentin Date: Wed, 26 Nov 2025 23:17:19 -0600 Subject: [PATCH 2/5] squash: use shift instead of concat for faster and more idiomatic inserts --- .../lib/opentelemetry/helpers/sql_processor/commenter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/sql-processor/lib/opentelemetry/helpers/sql_processor/commenter.rb b/helpers/sql-processor/lib/opentelemetry/helpers/sql_processor/commenter.rb index 383026c721..446d63f894 100644 --- a/helpers/sql-processor/lib/opentelemetry/helpers/sql_processor/commenter.rb +++ b/helpers/sql-processor/lib/opentelemetry/helpers/sql_processor/commenter.rb @@ -46,7 +46,7 @@ def set(carrier, headers) comment = "/*#{comment_parts.join(',')}*/" # Append to end of query (spec recommendation) - carrier.concat(" #{comment}") + carrier << " #{comment}" end end From c8b5ed56e6e5ab7cf36ac0696f28bce8ee8052b7 Mon Sep 17 00:00:00 2001 From: Ariel Valentin Date: Wed, 26 Nov 2025 23:18:35 -0600 Subject: [PATCH 3/5] squash: inline string generation --- .../lib/opentelemetry/helpers/sql_processor/commenter.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/helpers/sql-processor/lib/opentelemetry/helpers/sql_processor/commenter.rb b/helpers/sql-processor/lib/opentelemetry/helpers/sql_processor/commenter.rb index 446d63f894..245c4dbc7b 100644 --- a/helpers/sql-processor/lib/opentelemetry/helpers/sql_processor/commenter.rb +++ b/helpers/sql-processor/lib/opentelemetry/helpers/sql_processor/commenter.rb @@ -43,10 +43,8 @@ def set(carrier, headers) "#{key}='#{encoded_value}'" end - comment = "/*#{comment_parts.join(',')}*/" - # Append to end of query (spec recommendation) - carrier << " #{comment}" + carrier << " /*#{comment_parts.join(',')}*/" end end From 703fa306c458a9286286d44306d780e58b21611f Mon Sep 17 00:00:00 2001 From: Ariel Valentin Date: Tue, 2 Dec 2025 23:43:53 -0600 Subject: [PATCH 4/5] feat: Add SQL Comment Propagator --- .../instrumentation/mysql2/instrumentation.rb | 24 ++++++++- .../mysql2/instrumentation_test.rb | 42 ++++++++++++++++ .../instrumentation/pg/instrumentation.rb | 16 +++++- .../pg/instrumentation_test.rb | 42 ++++++++++++++++ .../trilogy/instrumentation.rb | 3 +- .../trilogy/instrumentation_test.rb | 49 +++++++++++++++++++ 6 files changed, 173 insertions(+), 3 deletions(-) diff --git a/instrumentation/mysql2/lib/opentelemetry/instrumentation/mysql2/instrumentation.rb b/instrumentation/mysql2/lib/opentelemetry/instrumentation/mysql2/instrumentation.rb index 9a7b78ccbc..89eab66fc6 100644 --- a/instrumentation/mysql2/lib/opentelemetry/instrumentation/mysql2/instrumentation.rb +++ b/instrumentation/mysql2/lib/opentelemetry/instrumentation/mysql2/instrumentation.rb @@ -10,9 +10,10 @@ module Mysql2 # The Instrumentation class contains logic to detect and install the Mysql2 # instrumentation class Instrumentation < OpenTelemetry::Instrumentation::Base - install do |_config| + install do |config| require_dependencies patch_client + configure_propagator(config) end present do @@ -23,6 +24,9 @@ class Instrumentation < OpenTelemetry::Instrumentation::Base option :db_statement, default: :obfuscate, validate: %I[omit include obfuscate] option :span_name, default: :statement_type, validate: %I[statement_type db_name db_operation_and_name] option :obfuscation_limit, default: 2000, validate: :integer + option :propagator, default: 'none', validate: %w[none tracecontext vitess] + + attr_reader :propagator private @@ -33,6 +37,24 @@ def require_dependencies def patch_client ::Mysql2::Client.prepend(Patches::Client) end + + def configure_propagator(config) + propagator = config[:propagator] + @propagator = case propagator + when 'tracecontext' then OpenTelemetry::Helpers::SqlProcessor::SqlCommenter.sql_query_propagator + when 'vitess' then fetch_propagator(propagator, 'OpenTelemetry::Propagator::Vitess') + when 'none', nil then nil + else + OpenTelemetry.logger.warn "The #{propagator} propagator is unknown and cannot be configured" + end + end + + def fetch_propagator(name, class_name, gem_suffix = name) + Kernel.const_get(class_name).sql_query_propagator + rescue NameError + OpenTelemetry.logger.warn "The #{name} propagator cannot be configured - please add opentelemetry-helpers-#{gem_suffix} to your Gemfile" + nil + end end end end diff --git a/instrumentation/mysql2/test/opentelemetry/instrumentation/mysql2/instrumentation_test.rb b/instrumentation/mysql2/test/opentelemetry/instrumentation/mysql2/instrumentation_test.rb index 405b714ee3..146fc05413 100644 --- a/instrumentation/mysql2/test/opentelemetry/instrumentation/mysql2/instrumentation_test.rb +++ b/instrumentation/mysql2/test/opentelemetry/instrumentation/mysql2/instrumentation_test.rb @@ -266,6 +266,48 @@ end end + describe 'when propagator is set to tracecontext' do + let(:config) { { propagator: 'tracecontext' } } + + it 'injects context into SQL query' do + sql = +'SELECT * from users where users.id = 1' + + expect do + client.query(sql) + end.must_raise Mysql2::Error + + # Verify the SQL was modified with trace context + _(sql).must_match(%r{/\*traceparent='00-#{span.hex_trace_id}-#{span.hex_span_id}-01'\*/}) + end + + it 'does not modify frozen strings' do + sql = 'SELECT * from users where users.id = 1' + _(sql).must_be :frozen? + + expect do + client.query(sql) + end.must_raise Mysql2::Error + + # Frozen strings should not be modified + _(sql).wont_match(%r{/\*traceparent=}) + end + end + + describe 'when propagator is set to none' do + let(:config) { { propagator: 'none' } } + + it 'does not inject context' do + sql = +'SELECT * from users where users.id = 1' + original_sql = sql.dup + + expect do + client.query(sql) + end.must_raise Mysql2::Error + + _(sql).must_equal original_sql + end + end + describe 'when db_statement is configured via environment variable' do describe 'when db_statement set as omit' do it 'omits db.statement attribute' do diff --git a/instrumentation/pg/lib/opentelemetry/instrumentation/pg/instrumentation.rb b/instrumentation/pg/lib/opentelemetry/instrumentation/pg/instrumentation.rb index 840ddb6fbb..5cdd561f46 100644 --- a/instrumentation/pg/lib/opentelemetry/instrumentation/pg/instrumentation.rb +++ b/instrumentation/pg/lib/opentelemetry/instrumentation/pg/instrumentation.rb @@ -11,9 +11,10 @@ module PG class Instrumentation < OpenTelemetry::Instrumentation::Base MINIMUM_VERSION = Gem::Version.new('1.1.0') - install do |_config| + install do |config| require_dependencies patch_client + configure_propagator(config) end present do @@ -27,6 +28,9 @@ class Instrumentation < OpenTelemetry::Instrumentation::Base option :peer_service, default: nil, validate: :string option :db_statement, default: :obfuscate, validate: %I[omit include obfuscate] option :obfuscation_limit, default: 2000, validate: :integer + option :propagator, default: 'none', validate: %w[none tracecontext] + + attr_reader :propagator private @@ -42,6 +46,16 @@ def patch_client ::PG::Connection.prepend(Patches::Connection) ::PG::Connection.singleton_class.prepend(Patches::Connect) end + + def configure_propagator(config) + propagator = config[:propagator] + @propagator = case propagator + when 'tracecontext' then OpenTelemetry::Helpers::SqlProcessor::SqlCommenter.sql_query_propagator + when 'none', nil then nil + else + OpenTelemetry.logger.warn "The #{propagator} propagator is unknown and cannot be configured" + end + end end end end diff --git a/instrumentation/pg/test/opentelemetry/instrumentation/pg/instrumentation_test.rb b/instrumentation/pg/test/opentelemetry/instrumentation/pg/instrumentation_test.rb index f70758d042..c4b3b55df7 100644 --- a/instrumentation/pg/test/opentelemetry/instrumentation/pg/instrumentation_test.rb +++ b/instrumentation/pg/test/opentelemetry/instrumentation/pg/instrumentation_test.rb @@ -366,6 +366,48 @@ client.query('DROP TABLE test_table') # Drop table to avoid conflicts end + describe 'when propagator is set to tracecontext' do + let(:config) { { propagator: 'tracecontext' } } + + it 'injects context into SQL query' do + sql = +'SELECT * from users where users.id = 1' + + expect do + client.exec(sql) + end.must_raise PG::UndefinedTable + + # Verify the SQL was modified with trace context + _(sql).must_match(%r{/\*traceparent='00-#{last_span.hex_trace_id}-#{last_span.hex_span_id}-01'\*/}) + end + + it 'does not modify frozen strings' do + sql = 'SELECT * from users where users.id = 1' + _(sql).must_be :frozen? + + expect do + client.exec(sql) + end.must_raise PG::UndefinedTable + + # Frozen strings should not be modified + _(sql).wont_match(%r{/\*traceparent=}) + end + end + + describe 'when propagator is set to none' do + let(:config) { { propagator: 'none' } } + + it 'does not inject context' do + sql = +'SELECT * from users where users.id = 1' + original_sql = sql.dup + + expect do + client.exec(sql) + end.must_raise PG::UndefinedTable + + _(sql).must_equal original_sql + end + end + describe 'when db_statement is obfuscate' do let(:config) { { db_statement: :obfuscate } } diff --git a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/instrumentation.rb b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/instrumentation.rb index 031c15d1ec..b4d7b20693 100644 --- a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/instrumentation.rb +++ b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/instrumentation.rb @@ -27,7 +27,7 @@ class Instrumentation < OpenTelemetry::Instrumentation::Base option :db_statement, default: :obfuscate, validate: %I[omit include obfuscate] option :span_name, default: :statement_type, validate: %I[statement_type db_name db_operation_and_name] option :obfuscation_limit, default: 2000, validate: :integer - option :propagator, default: nil, validate: :string + option :propagator, default: 'none', validate: %w[none tracecontext vitess] option :record_exception, default: true, validate: :boolean attr_reader :propagator @@ -45,6 +45,7 @@ def patch_client def configure_propagator(config) propagator = config[:propagator] @propagator = case propagator + when 'tracecontext' then OpenTelemetry::Helpers::SqlProcessor::SqlCommenter.sql_query_propagator when 'vitess' then fetch_propagator(propagator, 'OpenTelemetry::Propagator::Vitess') when 'none', nil then nil else diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/instrumentation_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/instrumentation_test.rb index c0ede54bae..8290018e0e 100644 --- a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/instrumentation_test.rb +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/instrumentation_test.rb @@ -425,6 +425,55 @@ end end + describe 'when propagator is set to tracecontext' do + let(:config) { { propagator: 'tracecontext' } } + + it 'injects context on frozen strings' do + sql = 'SELECT * from users where users.id = 1 and users.email = "test@test.com"' + _(sql).must_be :frozen? + propagator = OpenTelemetry::Instrumentation::Trilogy::Instrumentation.instance.propagator + + arg_cache = {} # maintain handles to args + allow(client).to receive(:query).and_wrap_original do |m, *args| + arg_cache[:query_input] = args[0] + _(args[0]).must_be :frozen? + m.call(args[0]) + end + + allow(propagator).to receive(:inject).and_wrap_original do |m, *args| + arg_cache[:inject_input] = args[0] + _(args[0]).wont_be :frozen? + _(args[0]).must_match(sql) + m.call(args[0], context: args[1][:context]) + end + + expect do + client.query(sql) + end.must_raise Trilogy::Error + + # arg_cache[:inject_input] _was_ a mutable string, so it has the context injected + # The tracecontext propagator injects traceparent and tracestate headers as SQL comments + _(arg_cache[:inject_input]).must_match(%r{/\*traceparent='00-#{span.hex_trace_id}-#{span.hex_span_id}-01'\*/}) + + # arg_cache[:inject_input] is now frozen + _(arg_cache[:inject_input]).must_be :frozen? + end + + it 'injects context on unfrozen strings' do + # inbound SQL is not frozen (string prefixed with +) + sql = +'SELECT * from users where users.id = 1 and users.email = "test@test.com"' + _(sql).wont_be :frozen? + + expect do + client.query(sql) + end.must_raise Trilogy::Error + + # The tracecontext propagator injects traceparent header as SQL comment + _(sql).must_match(%r{/\*traceparent='00-#{span.hex_trace_id}-#{span.hex_span_id}-01'\*/}) + _(sql).wont_be :frozen? + end + end + describe 'when db_statement is set to omit' do let(:config) { { db_statement: :omit } } From 1b50183956717f22decd890658e12d644a08193d Mon Sep 17 00:00:00 2001 From: Ariel Valentin Date: Wed, 3 Dec 2025 07:01:57 -0600 Subject: [PATCH 5/5] squash: actually implment comment injection --- .../instrumentation/mysql2/patches/client.rb | 28 ++++++++++++--- .../instrumentation/pg/patches/connection.rb | 35 +++++++++++++++++-- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/instrumentation/mysql2/lib/opentelemetry/instrumentation/mysql2/patches/client.rb b/instrumentation/mysql2/lib/opentelemetry/instrumentation/mysql2/patches/client.rb index dcbcd7c9a4..700c838acd 100644 --- a/instrumentation/mysql2/lib/opentelemetry/instrumentation/mysql2/patches/client.rb +++ b/instrumentation/mysql2/lib/opentelemetry/instrumentation/mysql2/patches/client.rb @@ -18,8 +18,16 @@ def query(sql, options = {}) _otel_span_name(sql), attributes: _otel_span_attributes(sql), kind: :client - ) do - super + ) do |_span, context| + if propagator && sql.frozen? + sql = +sql + propagator.inject(sql, context: context) + sql.freeze + elsif propagator + propagator.inject(sql, context: context) + end + + super(sql, options) end end @@ -28,8 +36,16 @@ def prepare(sql) _otel_span_name(sql), attributes: _otel_span_attributes(sql), kind: :client - ) do - super + ) do |_span, context| + if propagator && sql.frozen? + sql = +sql + propagator.inject(sql, context: context) + sql.freeze + elsif propagator + propagator.inject(sql, context: context) + end + + super(sql) end end @@ -93,6 +109,10 @@ def tracer def config Mysql2::Instrumentation.instance.config end + + def propagator + Mysql2::Instrumentation.instance.propagator + end end end end diff --git a/instrumentation/pg/lib/opentelemetry/instrumentation/pg/patches/connection.rb b/instrumentation/pg/lib/opentelemetry/instrumentation/pg/patches/connection.rb index 409889703b..a3d9a4b1ce 100644 --- a/instrumentation/pg/lib/opentelemetry/instrumentation/pg/patches/connection.rb +++ b/instrumentation/pg/lib/opentelemetry/instrumentation/pg/patches/connection.rb @@ -79,7 +79,20 @@ module Connection # rubocop:disable Metrics/ModuleLength PG::Constants::EXEC_ISH_METHODS.each do |method| define_method method do |*args, &block| span_name, attrs = span_attrs(:query, *args) - tracer.in_span(span_name, attributes: attrs, kind: :client) do + tracer.in_span(span_name, attributes: attrs, kind: :client) do |_span, context| + # Inject propagator context into SQL if propagator is configured + if propagator && args[0].is_a?(String) + sql = args[0] + if sql.frozen? + sql = +sql + propagator.inject(sql, context: context) + sql.freeze + args[0] = sql + else + propagator.inject(sql, context: context) + end + end + if block block.call(super(*args)) else @@ -92,7 +105,21 @@ module Connection # rubocop:disable Metrics/ModuleLength PG::Constants::PREPARE_ISH_METHODS.each do |method| define_method method do |*args| span_name, attrs = span_attrs(:prepare, *args) - tracer.in_span(span_name, attributes: attrs, kind: :client) do + tracer.in_span(span_name, attributes: attrs, kind: :client) do |_span, context| + # Inject propagator context into SQL if propagator is configured + # For prepare, the SQL is in args[1] + if propagator && args[1].is_a?(String) + sql = args[1] + if sql.frozen? + sql = +sql + propagator.inject(sql, context: context) + sql.freeze + args[1] = sql + else + propagator.inject(sql, context: context) + end + end + super(*args) end end @@ -248,6 +275,10 @@ def transport_port p = conninfo_hash[:port] p.to_i unless p.nil? || p.empty? || p.include?(',') end + + def propagator + OpenTelemetry::Instrumentation::PG::Instrumentation.instance.propagator + end end end end