Skip to content

Commit c674b40

Browse files
committed
feat: Add SQL Comment Propagator
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
1 parent bc5057d commit c674b40

File tree

7 files changed

+226
-2
lines changed

7 files changed

+226
-2
lines changed

.toys/.data/releases.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,4 +300,4 @@ gems:
300300

301301
- name: opentelemetry-sampler-xray
302302
directory: sampler/xray
303-
version_constant: [OpenTelemetry, Sampler, XRay, VERSION]
303+
version_constant: [OpenTelemetry, Sampler, XRay, VERSION]

helpers/sql-processor/Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ gemspec
1111
group :test do
1212
gem 'bundler', '~> 2.4'
1313
gem 'minitest', '~> 5.0'
14+
gem 'opentelemetry-sdk', '~> 1.5'
1415
gem 'opentelemetry-test-helpers', '~> 0.3'
1516
gem 'rake', '~> 13.0'
1617
gem 'rubocop', '~> 1.79.1'

helpers/sql-processor/lib/opentelemetry/helpers/sql_processor.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
66

77
require 'opentelemetry-common'
88
require_relative 'sql_processor/obfuscator'
9+
require_relative 'sql_processor/commenter'
910

1011
module OpenTelemetry
1112
module Helpers
1213
# SQL processing utilities for OpenTelemetry instrumentation.
1314
#
1415
# This module provides a unified interface for SQL processing operations
15-
# commonly needed in database adapter instrumentation, including SQL obfuscation.
16+
# commonly needed in database adapter instrumentation, including SQL obfuscation
17+
# and SQL comment-based trace context propagation.
1618
#
1719
# @api public
1820
module SqlProcessor
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# frozen_string_literal: true
2+
3+
# Copyright The OpenTelemetry Authors
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
require 'cgi'
8+
9+
module OpenTelemetry
10+
module Helpers
11+
module SqlProcessor
12+
# SqlCommenter provides SQL comment-based trace context propagation
13+
# according to the SQL Commenter specification.
14+
#
15+
# This module implements a propagator interface compatible with Vitess,
16+
# allowing it to be used as a drop-in replacement.
17+
#
18+
# @api public
19+
module SqlCommenter
20+
extend self
21+
22+
# SqlQuerySetter is responsible for formatting trace context as SQL comments
23+
# and appending them to SQL queries according to the SQL Commenter specification.
24+
#
25+
# Format: /*key='value',key2='value2'*/
26+
# Values are URL-encoded per the SQL Commenter spec
27+
module SqlQuerySetter
28+
extend self
29+
30+
# Appends trace context as a SQL comment to the carrier (SQL query string)
31+
#
32+
# @param carrier [String] The SQL query string to modify
33+
# @param headers [Hash] Hash of trace context headers (e.g., {'traceparent' => '00-...'})
34+
def set(carrier, headers)
35+
return if headers.empty?
36+
return if carrier.frozen?
37+
38+
# Convert headers hash to SQL commenter format
39+
# Format: /*key1='value1',key2='value2'*/
40+
comment_parts = headers.map do |key, value|
41+
# URL encode values as per SQL Commenter spec (using URI component encoding)
42+
encoded_value = CGI.escapeURIComponent(value.to_s)
43+
"#{key}='#{encoded_value}'"
44+
end
45+
46+
comment = "/*#{comment_parts.join(',')}*/"
47+
48+
# Append to end of query (spec recommendation)
49+
carrier.concat(" #{comment}")
50+
end
51+
end
52+
53+
# SqlQueryPropagator propagates trace context using SQL comments
54+
# according to the SQL Commenter specification.
55+
#
56+
# This propagator implements the same interface as the Vitess propagator
57+
# and can be used as a drop-in replacement.
58+
#
59+
# @example
60+
# propagator = OpenTelemetry::Helpers::SqlProcessor::SqlCommenter.sql_query_propagator
61+
# sql = "SELECT * FROM users"
62+
# propagator.inject(sql, context: current_context)
63+
# # => "SELECT * FROM users /*traceparent='00-...',tracestate='...'*/"
64+
module SqlQueryPropagator
65+
extend self
66+
67+
# Injects trace context into a SQL query as a comment
68+
#
69+
# @param carrier [String] The SQL query string to inject context into
70+
# @param context [optional, Context] The context to inject. Defaults to current context.
71+
# @param setter [optional, #set] The setter to use for appending the comment.
72+
# Defaults to SqlQuerySetter.
73+
# @return [nil]
74+
def inject(carrier, context: OpenTelemetry::Context.current, setter: SqlQuerySetter)
75+
# Use the global propagator to extract headers into a hash
76+
headers = {}
77+
OpenTelemetry.propagation.inject(headers, context: context)
78+
79+
# Pass the headers to our SQL comment setter
80+
setter.set(carrier, headers)
81+
nil
82+
end
83+
end
84+
85+
# Returns the SqlQueryPropagator module for stateless propagation
86+
#
87+
# @return [Module] The SqlQueryPropagator module
88+
def sql_query_propagator
89+
SqlQueryPropagator
90+
end
91+
end
92+
end
93+
end
94+
end

helpers/sql-processor/opentelemetry-helpers-sql-processor.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Gem::Specification.new do |spec|
2525
spec.require_paths = ['lib']
2626
spec.required_ruby_version = '>= 3.2'
2727

28+
spec.add_dependency 'opentelemetry-api', '~> 1.0'
2829
spec.add_dependency 'opentelemetry-common', '~> 0.21'
2930

3031
if spec.respond_to?(:metadata)
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# frozen_string_literal: true
2+
3+
# Copyright The OpenTelemetry Authors
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
require 'test_helper'
8+
9+
describe OpenTelemetry::Helpers::SqlProcessor::SqlCommenter do
10+
let(:span_id) { 'e457b5a2e4d86bd1' }
11+
let(:trace_id) { '80f198ee56343ba864fe8b2a57d3eff7' }
12+
let(:trace_flags) { OpenTelemetry::Trace::TraceFlags::SAMPLED }
13+
14+
let(:context) do
15+
OpenTelemetry::Trace.context_with_span(
16+
OpenTelemetry::Trace.non_recording_span(
17+
OpenTelemetry::Trace::SpanContext.new(
18+
trace_id: Array(trace_id).pack('H*'),
19+
span_id: Array(span_id).pack('H*'),
20+
trace_flags: trace_flags
21+
)
22+
)
23+
)
24+
end
25+
26+
describe 'SqlQueryPropagator.inject' do
27+
let(:propagator) { OpenTelemetry::Helpers::SqlProcessor::SqlCommenter.sql_query_propagator }
28+
29+
it 'injects trace context into SQL' do
30+
sql = +'SELECT * FROM users'
31+
propagator.inject(sql, context: context)
32+
33+
expected = "SELECT * FROM users /*traceparent='00-#{trace_id}-#{span_id}-01'*/"
34+
_(sql).must_equal(expected)
35+
end
36+
37+
it 'handles frozen strings by not modifying them' do
38+
sql = -'SELECT * FROM users'
39+
propagator.inject(sql, context: context)
40+
41+
# Frozen string should remain unchanged (setter will return early)
42+
_(sql).must_equal('SELECT * FROM users')
43+
end
44+
45+
it 'handles empty context' do
46+
sql = +'SELECT * FROM users'
47+
propagator.inject(sql, context: OpenTelemetry::Context.empty)
48+
49+
# Should not modify SQL when context produces no headers
50+
_(sql).must_equal('SELECT * FROM users')
51+
end
52+
53+
it 'includes tracestate when present' do
54+
span_context = OpenTelemetry::Trace::SpanContext.new(
55+
trace_id: Array(trace_id).pack('H*'),
56+
span_id: Array(span_id).pack('H*'),
57+
trace_flags: trace_flags,
58+
tracestate: OpenTelemetry::Trace::Tracestate.from_string('congo=t61rcWkgMzE,rojo=00f067aa0ba902b7')
59+
)
60+
61+
ctx = OpenTelemetry::Trace.context_with_span(
62+
OpenTelemetry::Trace.non_recording_span(span_context)
63+
)
64+
65+
sql = +'SELECT * FROM users'
66+
propagator.inject(sql, context: ctx)
67+
68+
expected = "SELECT * FROM users /*traceparent='00-#{trace_id}-#{span_id}-01',tracestate='congo%3Dt61rcWkgMzE%2Crojo%3D00f067aa0ba902b7'*/"
69+
_(sql).must_equal(expected)
70+
end
71+
72+
it 'returns nil' do
73+
sql = +'SELECT * FROM users'
74+
result = propagator.inject(sql, context: context)
75+
76+
_(result).must_be_nil
77+
end
78+
end
79+
80+
describe 'SqlQuerySetter.set' do
81+
let(:setter) { OpenTelemetry::Helpers::SqlProcessor::SqlCommenter::SqlQuerySetter }
82+
83+
it 'formats headers as SQL comments' do
84+
sql = +'SELECT * FROM users'
85+
headers = { 'traceparent' => '00-80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1-01' }
86+
87+
setter.set(sql, headers)
88+
89+
expected = "SELECT * FROM users /*traceparent='00-80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1-01'*/"
90+
_(sql).must_equal(expected)
91+
end
92+
93+
it 'URL encodes values' do
94+
sql = +'SELECT * FROM users'
95+
headers = { 'key' => 'value with spaces' }
96+
97+
setter.set(sql, headers)
98+
99+
expected = "SELECT * FROM users /*key='value%20with%20spaces'*/"
100+
_(sql).must_equal(expected)
101+
end
102+
103+
it 'handles empty headers' do
104+
sql = +'SELECT * FROM users'
105+
setter.set(sql, {})
106+
107+
_(sql).must_equal('SELECT * FROM users')
108+
end
109+
110+
it 'handles frozen strings by not modifying them' do
111+
sql = -'SELECT * FROM users'
112+
headers = { 'traceparent' => '00-80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1-01' }
113+
114+
setter.set(sql, headers)
115+
116+
# Frozen string should remain unchanged
117+
_(sql).must_equal('SELECT * FROM users')
118+
end
119+
end
120+
end

helpers/sql-processor/test/test_helper.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,9 @@
1010

1111
require 'minitest/autorun'
1212
require 'opentelemetry-helpers-sql-processor'
13+
require 'opentelemetry/sdk'
14+
15+
OpenTelemetry.logger = Logger.new($stderr, level: ENV.fetch('OTEL_LOG_LEVEL', 'fatal').to_sym)
16+
17+
# Configure the SDK to set up the default propagators
18+
OpenTelemetry::SDK.configure

0 commit comments

Comments
 (0)