From 2088497a9cd97f2853f6a5551809705c00d9cc3b Mon Sep 17 00:00:00 2001 From: Hannah Ramadan Date: Wed, 18 Mar 2026 13:43:53 -0700 Subject: [PATCH 01/23] feat: Trilogy semantic convention stability --- instrumentation/trilogy/.rubocop.yml | 5 + instrumentation/trilogy/Appraisals | 19 +- instrumentation/trilogy/README.md | 16 + instrumentation/trilogy/Rakefile | 8 + .../trilogy/instrumentation.rb | 37 +- .../instrumentation/trilogy/patches/client.rb | 117 ------ .../trilogy/patches/dup/client.rb | 157 +++++++ .../trilogy/patches/old/client.rb | 119 ++++++ .../trilogy/patches/stable/client.rb | 134 ++++++ .../patches/dup/client_attributes_test.rb | 185 +++++++++ .../patches/dup/instrumentation_test.rb | 382 ++++++++++++++++++ .../{ => old}/client_attributes_test.rb | 10 +- .../{ => patches/old}/instrumentation_test.rb | 8 +- .../patches/stable/client_attributes_test.rb | 205 ++++++++++ .../patches/stable/instrumentation_test.rb | 343 ++++++++++++++++ 15 files changed, 1613 insertions(+), 132 deletions(-) delete mode 100644 instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/client.rb create mode 100644 instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/dup/client.rb create mode 100644 instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/old/client.rb create mode 100644 instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb create mode 100644 instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/client_attributes_test.rb create mode 100644 instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb rename instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/{ => old}/client_attributes_test.rb (95%) rename instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/{ => patches/old}/instrumentation_test.rb (98%) create mode 100644 instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/client_attributes_test.rb create mode 100644 instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb diff --git a/instrumentation/trilogy/.rubocop.yml b/instrumentation/trilogy/.rubocop.yml index 1248a2f825..47a500e984 100644 --- a/instrumentation/trilogy/.rubocop.yml +++ b/instrumentation/trilogy/.rubocop.yml @@ -1 +1,6 @@ inherit_from: ../../.rubocop.yml + +Metrics/ModuleLength: + Exclude: + - 'lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb' + - 'lib/opentelemetry/instrumentation/trilogy/patches/dup/client.rb' diff --git a/instrumentation/trilogy/Appraisals b/instrumentation/trilogy/Appraisals index 8e330defbe..039b77261f 100644 --- a/instrumentation/trilogy/Appraisals +++ b/instrumentation/trilogy/Appraisals @@ -4,10 +4,19 @@ # # SPDX-License-Identifier: Apache-2.0 -appraise 'trilogy-2.9' do - gem 'trilogy', '~> 2.9.0' -end +# To facilitate database semantic convention stability migration, we are using +# appraisal to test the different semantic convention modes along with different +# gem versions. For more information on the semantic convention modes, see: +# https://opentelemetry.io/docs/specs/semconv/non-normative/db-migration/ + +semconv_stability = %w[old stable dup] + +semconv_stability.each do |mode| + appraise "trilogy-2-#{mode}" do + gem 'trilogy', '~> 2.9' + end -appraise 'trilogy-latest' do - gem 'trilogy' + appraise "trilogy-latest-#{mode}" do + gem 'trilogy' + end end diff --git a/instrumentation/trilogy/README.md b/instrumentation/trilogy/README.md index b931e78b7b..6bbb562125 100644 --- a/instrumentation/trilogy/README.md +++ b/instrumentation/trilogy/README.md @@ -70,6 +70,22 @@ The `opentelemetry-instrumentation-trilogy` gem source is [on github][repo-githu 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]. +## Database semantic convention stability + +In the OpenTelemetry ecosystem, database semantic conventions have now reached a stable state. However, the initial Trilogy instrumentation was introduced before this stability was achieved, which resulted in database attributes being based on an older version of the semantic conventions. + +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. + +When setting the value for `OTEL_SEMCONV_STABILITY_OPT_IN`, you can specify which conventions you wish to adopt: + +- `database` - Emits the stable database and networking conventions and ceases emitting the old conventions previously emitted by the instrumentation. +- `database/dup` - Emits both the old and stable database and networking conventions, enabling a phased rollout of the stable semantic conventions. +- Default behavior (in the absence of either value) is to continue emitting the old database and networking conventions the instrumentation previously emitted. + +During the transition from old to stable conventions, Trilogy instrumentation code comes in three patch versions: `dup`, `old`, and `stable`. These versions are identical except for the attributes they send. Any changes to Trilogy instrumentation should consider all three patches. + +For additional information on migration, please refer to our [documentation](https://opentelemetry.io/docs/specs/semconv/non-normative/db-migration/). + ## License The `opentelemetry-instrumentation-trilogy` gem is distributed under the Apache 2.0 license. See [LICENSE][license-github] for more information. diff --git a/instrumentation/trilogy/Rakefile b/instrumentation/trilogy/Rakefile index 1a64ba842e..e9d17cb879 100644 --- a/instrumentation/trilogy/Rakefile +++ b/instrumentation/trilogy/Rakefile @@ -11,6 +11,14 @@ require 'rubocop/rake_task' RuboCop::RakeTask.new +# Set OTEL_SEMCONV_STABILITY_OPT_IN based on appraisal name +gemfile = ENV.fetch('BUNDLE_GEMFILE', '') +if gemfile.include?('stable') + ENV['OTEL_SEMCONV_STABILITY_OPT_IN'] = 'database' +elsif gemfile.include?('dup') + ENV['OTEL_SEMCONV_STABILITY_OPT_IN'] = 'database/dup' +end + Rake::TestTask.new :test do |t| t.libs << 'test' t.libs << 'lib' diff --git a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/instrumentation.rb b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/instrumentation.rb index b4d7b20693..c69546ae37 100644 --- a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/instrumentation.rb +++ b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/instrumentation.rb @@ -30,16 +30,47 @@ class Instrumentation < OpenTelemetry::Instrumentation::Base option :propagator, default: 'none', validate: %w[none tracecontext vitess] option :record_exception, default: true, validate: :boolean - attr_reader :propagator + attr_reader :propagator, :semconv private def require_dependencies - require_relative 'patches/client' + @semconv = determine_semconv + + case @semconv + when :old + require_relative 'patches/old/client' + when :stable + require_relative 'patches/stable/client' + when :dup + require_relative 'patches/dup/client' + end end def patch_client - ::Trilogy.prepend(Patches::Client) + case @semconv + when :old + ::Trilogy.prepend(Patches::Old::Client) + when :stable + ::Trilogy.prepend(Patches::Stable::Client) + when :dup + ::Trilogy.prepend(Patches::Dup::Client) + end + end + + def determine_semconv + opt_in = ENV.fetch('OTEL_SEMCONV_STABILITY_OPT_IN', nil) + return :old if opt_in.nil? + + opt_in_values = opt_in.split(',').map(&:strip) + + if opt_in_values.include?('database/dup') + :dup + elsif opt_in_values.include?('database') + :stable + else + :old + end end def configure_propagator(config) diff --git a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/client.rb b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/client.rb deleted file mode 100644 index b29dbb115c..0000000000 --- a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/client.rb +++ /dev/null @@ -1,117 +0,0 @@ -# frozen_string_literal: true - -# Copyright The OpenTelemetry Authors -# -# SPDX-License-Identifier: Apache-2.0 - -require 'opentelemetry-helpers-mysql' -require 'opentelemetry-helpers-sql-processor' - -module OpenTelemetry - module Instrumentation - module Trilogy - module Patches - # Module to prepend to Trilogy for instrumentation - module Client - def initialize(options = {}) - @connection_options = options # This is normally done by Trilogy#initialize - @_otel_database_name = connection_options&.dig(:database) - @_otel_base_attributes = _build_otel_base_attributes.freeze - - tracer.in_span( - 'connect', - attributes: client_attributes.merge!(OpenTelemetry::Instrumentation::Trilogy.attributes), - kind: :client, - record_exception: config[:record_exception] - ) do - super - end - end - - def ping(...) - tracer.in_span( - 'ping', - attributes: client_attributes.merge!(OpenTelemetry::Instrumentation::Trilogy.attributes), - kind: :client, - record_exception: config[:record_exception] - ) do - super - end - end - - def query(sql) - context_attributes = OpenTelemetry::Instrumentation::Trilogy.attributes - - tracer.in_span( - OpenTelemetry::Helpers::MySQL.database_span_name( - sql, - context_attributes[OpenTelemetry::SemanticConventions::Trace::DB_OPERATION], - @_otel_database_name, - config - ), - attributes: client_attributes(sql).merge!(context_attributes), - kind: :client, - record_exception: config[:record_exception] - ) 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 - end - end - - private - - def _build_otel_base_attributes - database_user = connection_options&.dig(:username) - - attributes = { - ::OpenTelemetry::SemanticConventions::Trace::DB_SYSTEM => 'mysql', - ::OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME => connection_options&.fetch(:host, 'unknown sock') || 'unknown sock' - } - - attributes[::OpenTelemetry::SemanticConventions::Trace::DB_NAME] = @_otel_database_name if @_otel_database_name - attributes[::OpenTelemetry::SemanticConventions::Trace::DB_USER] = database_user if database_user - attributes[::OpenTelemetry::SemanticConventions::Trace::PEER_SERVICE] = config[:peer_service] unless config[:peer_service].nil? - attributes - end - - def client_attributes(sql = nil) - attributes = @_otel_base_attributes.dup - - attributes['db.instance.id'] = @connected_host unless @connected_host.nil? - - if sql - case config[:db_statement] - when :obfuscate - attributes[::OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT] = - OpenTelemetry::Helpers::SqlProcessor.obfuscate_sql(sql, obfuscation_limit: config[:obfuscation_limit], adapter: :mysql) - when :include - attributes[::OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT] = sql - end - end - - attributes - end - - def tracer - Trilogy::Instrumentation.instance.tracer - end - - def config - Trilogy::Instrumentation.instance.config - end - - def propagator - Trilogy::Instrumentation.instance.propagator - end - end - end - end - end -end diff --git a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/dup/client.rb b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/dup/client.rb new file mode 100644 index 0000000000..b0bbef79f8 --- /dev/null +++ b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/dup/client.rb @@ -0,0 +1,157 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'opentelemetry-helpers-mysql' +require 'opentelemetry-helpers-sql-processor' + +module OpenTelemetry + module Instrumentation + module Trilogy + module Patches + module Dup + # Module to prepend to Trilogy for instrumentation (emits both old and stable semantic conventions) + module Client + + def initialize(options = {}) + @connection_options = options # This is normally done by Trilogy#initialize + @_otel_database_name = connection_options&.dig(:database) + @_otel_base_attributes = _build_otel_base_attributes.freeze + + tracer.in_span( + 'connect', + attributes: client_attributes.merge!(OpenTelemetry::Instrumentation::Trilogy.attributes), + kind: :client, + record_exception: config[:record_exception] + ) do |span| + super + rescue StandardError => e + set_error_attributes(span, e) + raise + end + end + + def ping(...) + tracer.in_span( + 'ping', + attributes: client_attributes.merge!(OpenTelemetry::Instrumentation::Trilogy.attributes), + kind: :client, + record_exception: config[:record_exception] + ) do |span| + super + rescue StandardError => e + set_error_attributes(span, e) + raise + end + end + + def query(sql) + context_attributes = OpenTelemetry::Instrumentation::Trilogy.attributes + + tracer.in_span( + OpenTelemetry::Helpers::MySQL.database_span_name( + sql, + context_attributes[OpenTelemetry::SemanticConventions::Trace::DB_OPERATION] || context_attributes['db.operation.name'], + @_otel_database_name, + config + ), + attributes: client_attributes(sql).merge!(context_attributes), + kind: :client, + record_exception: config[:record_exception] + ) 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 + rescue StandardError => e + set_error_attributes(span, e) + raise + end + end + + private + + def _build_otel_base_attributes + database_user = connection_options&.dig(:username) + mysql_host = connection_options&.fetch(:host, nil) || 'unknown sock' + mysql_port = connection_options&.dig(:port) + + # Include both old and stable attributes + attributes = { + # Old conventions + ::OpenTelemetry::SemanticConventions::Trace::DB_SYSTEM => 'mysql', + ::OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME => mysql_host, + # Stable conventions + 'db.system.name' => 'mysql', + 'server.address' => mysql_host + } + + attributes['server.port'] = mysql_port if mysql_port + + # Database name (old: db.name, stable: db.namespace) + if @_otel_database_name + attributes[::OpenTelemetry::SemanticConventions::Trace::DB_NAME] = @_otel_database_name + attributes['db.namespace'] = @_otel_database_name + end + + # db.user (old only - removed in stable) + attributes[::OpenTelemetry::SemanticConventions::Trace::DB_USER] = database_user if database_user + + # peer.service (same in both) + attributes[::OpenTelemetry::SemanticConventions::Trace::PEER_SERVICE] = config[:peer_service] unless config[:peer_service].nil? + attributes + end + + def client_attributes(sql = nil) + attributes = @_otel_base_attributes.dup + + attributes['db.instance.id'] = @connected_host unless @connected_host.nil? + + if sql + case config[:db_statement] + when :obfuscate + obfuscated = OpenTelemetry::Helpers::SqlProcessor.obfuscate_sql(sql, obfuscation_limit: config[:obfuscation_limit], adapter: :mysql) + # Old convention + attributes[::OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT] = obfuscated + # Stable convention + attributes['db.query.text'] = obfuscated + when :include + # Old convention + attributes[::OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT] = sql + # Stable convention + attributes['db.query.text'] = sql + end + end + + attributes + end + + def set_error_attributes(span, error) + span.set_attribute('error.type', error.class.name) + span.set_attribute('db.response.status_code', error.error_code.to_s) if error.error_code + end + + def tracer + Trilogy::Instrumentation.instance.tracer + end + + def config + Trilogy::Instrumentation.instance.config + end + + def propagator + Trilogy::Instrumentation.instance.propagator + end + end + end + end + end + end +end diff --git a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/old/client.rb b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/old/client.rb new file mode 100644 index 0000000000..f95bf8aa25 --- /dev/null +++ b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/old/client.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'opentelemetry-helpers-mysql' +require 'opentelemetry-helpers-sql-processor' + +module OpenTelemetry + module Instrumentation + module Trilogy + module Patches + module Old + # Module to prepend to Trilogy for instrumentation (old semantic conventions) + module Client + def initialize(options = {}) + @connection_options = options # This is normally done by Trilogy#initialize + @_otel_database_name = connection_options&.dig(:database) + @_otel_base_attributes = _build_otel_base_attributes.freeze + + tracer.in_span( + 'connect', + attributes: client_attributes.merge!(OpenTelemetry::Instrumentation::Trilogy.attributes), + kind: :client, + record_exception: config[:record_exception] + ) do + super + end + end + + def ping(...) + tracer.in_span( + 'ping', + attributes: client_attributes.merge!(OpenTelemetry::Instrumentation::Trilogy.attributes), + kind: :client, + record_exception: config[:record_exception] + ) do + super + end + end + + def query(sql) + context_attributes = OpenTelemetry::Instrumentation::Trilogy.attributes + + tracer.in_span( + OpenTelemetry::Helpers::MySQL.database_span_name( + sql, + context_attributes[OpenTelemetry::SemanticConventions::Trace::DB_OPERATION], + @_otel_database_name, + config + ), + attributes: client_attributes(sql).merge!(context_attributes), + kind: :client, + record_exception: config[:record_exception] + ) 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 + end + end + + private + + def _build_otel_base_attributes + database_user = connection_options&.dig(:username) + + attributes = { + ::OpenTelemetry::SemanticConventions::Trace::DB_SYSTEM => 'mysql', + ::OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME => connection_options&.fetch(:host, 'unknown sock') || 'unknown sock' + } + + attributes[::OpenTelemetry::SemanticConventions::Trace::DB_NAME] = @_otel_database_name if @_otel_database_name + attributes[::OpenTelemetry::SemanticConventions::Trace::DB_USER] = database_user if database_user + attributes[::OpenTelemetry::SemanticConventions::Trace::PEER_SERVICE] = config[:peer_service] unless config[:peer_service].nil? + attributes + end + + def client_attributes(sql = nil) + attributes = @_otel_base_attributes.dup + + attributes['db.instance.id'] = @connected_host unless @connected_host.nil? + + if sql + case config[:db_statement] + when :obfuscate + attributes[::OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT] = + OpenTelemetry::Helpers::SqlProcessor.obfuscate_sql(sql, obfuscation_limit: config[:obfuscation_limit], adapter: :mysql) + when :include + attributes[::OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT] = sql + end + end + + attributes + end + + def tracer + Trilogy::Instrumentation.instance.tracer + end + + def config + Trilogy::Instrumentation.instance.config + end + + def propagator + Trilogy::Instrumentation.instance.propagator + end + end + end + end + end + end +end diff --git a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb new file mode 100644 index 0000000000..9c9a412bdc --- /dev/null +++ b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'opentelemetry-helpers-mysql' +require 'opentelemetry-helpers-sql-processor' + +module OpenTelemetry + module Instrumentation + module Trilogy + module Patches + module Stable + # Module to prepend to Trilogy for instrumentation (stable semantic conventions) + module Client + + def initialize(options = {}) + @connection_options = options # This is normally done by Trilogy#initialize + @_otel_database_name = connection_options&.dig(:database) + @_otel_base_attributes = _build_otel_base_attributes.freeze + + tracer.in_span( + 'connect', + attributes: client_attributes.merge!(OpenTelemetry::Instrumentation::Trilogy.attributes), + kind: :client, + record_exception: config[:record_exception] + ) do |span| + super + rescue StandardError => e + set_error_attributes(span, e) + raise + end + end + + def ping(...) + tracer.in_span( + 'ping', + attributes: client_attributes.merge!(OpenTelemetry::Instrumentation::Trilogy.attributes), + kind: :client, + record_exception: config[:record_exception] + ) do |span| + super + rescue StandardError => e + set_error_attributes(span, e) + raise + end + end + + def query(sql) + context_attributes = OpenTelemetry::Instrumentation::Trilogy.attributes + + tracer.in_span( + OpenTelemetry::Helpers::MySQL.database_span_name( + sql, + context_attributes['db.operation.name'], + @_otel_database_name, + config + ), + attributes: client_attributes(sql).merge!(context_attributes), + kind: :client, + record_exception: config[:record_exception] + ) 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 + rescue StandardError => e + set_error_attributes(span, e) + raise + end + end + + private + + def _build_otel_base_attributes + mysql_host = connection_options&.fetch(:host, nil) || 'unknown sock' + mysql_port = connection_options&.dig(:port) + + attributes = { + 'db.system.name' => 'mysql', + 'server.address' => mysql_host + } + + attributes['server.port'] = mysql_port if mysql_port + + attributes['db.namespace'] = @_otel_database_name if @_otel_database_name + attributes[::OpenTelemetry::SemanticConventions::Trace::PEER_SERVICE] = config[:peer_service] unless config[:peer_service].nil? + attributes + end + + def client_attributes(sql = nil) + attributes = @_otel_base_attributes.dup + + if sql + case config[:db_statement] + when :obfuscate + attributes['db.query.text'] = + OpenTelemetry::Helpers::SqlProcessor.obfuscate_sql(sql, obfuscation_limit: config[:obfuscation_limit], adapter: :mysql) + when :include + attributes['db.query.text'] = sql + end + end + + attributes + end + + def set_error_attributes(span, error) + span.set_attribute('error.type', error.class.name) + span.set_attribute('db.response.status_code', error.error_code.to_s) if error.error_code + end + + def tracer + Trilogy::Instrumentation.instance.tracer + end + + def config + Trilogy::Instrumentation.instance.config + end + + def propagator + Trilogy::Instrumentation.instance.propagator + end + end + end + end + end + end +end diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/client_attributes_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/client_attributes_test.rb new file mode 100644 index 0000000000..55808bbe92 --- /dev/null +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/client_attributes_test.rb @@ -0,0 +1,185 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +require_relative '../../../../../../lib/opentelemetry/instrumentation/trilogy' +require_relative '../../../../../../lib/opentelemetry/instrumentation/trilogy/patches/dup/client' + +# Unit tests for the dup semantic conventions client_attributes. +# Verifies that both old and stable attributes are emitted. +describe OpenTelemetry::Instrumentation::Trilogy::Patches::Dup::Client do + # Helper to build a test client without a real MySQL connection. + def build_test_client(options) + c = Trilogy.allocate + c.instance_variable_set(:@connection_options, options) + c.instance_variable_set(:@_otel_database_name, options[:database]) + c.instance_variable_set(:@_otel_base_attributes, c.send(:_build_otel_base_attributes).freeze) + c + end + + let(:instrumentation) { OpenTelemetry::Instrumentation::Trilogy::Instrumentation.instance } + let(:exporter) { EXPORTER } + + let(:connection_options) do + { + host: 'db-primary.example.com', + port: 3307, + database: 'myapp_production', + username: 'app_user' + } + end + + let(:client) { build_test_client(connection_options) } + + before do + skip unless ENV['BUNDLE_GEMFILE'].include?('dup') + + exporter.reset + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install({ + db_statement: :omit, + span_name: :statement_type, + propagator: 'none', + record_exception: true, + obfuscation_limit: 2000, + peer_service: nil + }) + end + + after do + instrumentation.instance_variable_set(:@installed, false) + end + + describe '#client_attributes' do + describe 'includes both old and stable attributes' do + it 'includes both db.system (old) and db.system.name (stable)' do + attrs = client.send(:client_attributes) + assert_equal 'mysql', attrs[OpenTelemetry::SemanticConventions::Trace::DB_SYSTEM] + assert_equal 'mysql', attrs['db.system.name'] + end + + it 'includes both net.peer.name (old) and server.address (stable)' do + attrs = client.send(:client_attributes) + assert_equal 'db-primary.example.com', attrs[OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME] + assert_equal 'db-primary.example.com', attrs['server.address'] + end + + it 'includes both db.name (old) and db.namespace (stable)' do + attrs = client.send(:client_attributes) + assert_equal 'myapp_production', attrs[OpenTelemetry::SemanticConventions::Trace::DB_NAME] + assert_equal 'myapp_production', attrs['db.namespace'] + end + + it 'includes db.user (old only - removed in stable)' do + attrs = client.send(:client_attributes) + assert_equal 'app_user', attrs[OpenTelemetry::SemanticConventions::Trace::DB_USER] + end + + it 'includes server.port (stable) when present' do + attrs = client.send(:client_attributes) + assert_equal 3307, attrs['server.port'] + end + + it 'includes server.port even when default port (3306)' do + c = build_test_client({ host: 'h', port: 3306, database: 'test' }) + attrs = c.send(:client_attributes) + assert_equal 3306, attrs['server.port'] + end + + it 'does not include net.peer.port (was not in old)' do + attrs = client.send(:client_attributes) + refute attrs.key?(OpenTelemetry::SemanticConventions::Trace::NET_PEER_PORT) + end + end + + it 'includes db.instance.id when connected_host is set' do + client.instance_variable_set(:@connected_host, 'replica-3.internal') + attrs = client.send(:client_attributes) + assert_equal 'replica-3.internal', attrs['db.instance.id'] + end + + it 'includes peer_service when configured' do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.instance_variable_set(:@semconv, :dup) + instrumentation.install({ + db_statement: :omit, + span_name: :statement_type, + propagator: 'none', + record_exception: true, + obfuscation_limit: 2000, + peer_service: 'mysql-primary' + }) + attrs = client.send(:client_attributes) + assert_equal 'mysql-primary', attrs[OpenTelemetry::SemanticConventions::Trace::PEER_SERVICE] + end + + it 'returns independent hash instances on each call' do + a = client.send(:client_attributes) + b = client.send(:client_attributes) + refute_same a, b + a['extra'] = 'value' + refute b.key?('extra') + end + + describe 'with sql and db_statement config' do + before do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.instance_variable_set(:@semconv, :dup) + end + + it 'includes SQL in both db.statement (old) and db.query.text (stable) when db_statement is :include' do + instrumentation.install({ + db_statement: :include, + span_name: :statement_type, + propagator: 'none', + record_exception: true, + obfuscation_limit: 2000, + peer_service: nil + }) + attrs = client.send(:client_attributes, 'SELECT * FROM users') + assert_equal 'SELECT * FROM users', attrs[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT] + assert_equal 'SELECT * FROM users', attrs['db.query.text'] + end + + it 'omits both db.statement and db.query.text when db_statement is :omit' do + instrumentation.install({ + db_statement: :omit, + span_name: :statement_type, + propagator: 'none', + record_exception: true, + obfuscation_limit: 2000, + peer_service: nil + }) + attrs = client.send(:client_attributes, 'SELECT * FROM users') + refute attrs.key?(OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT) + refute attrs.key?('db.query.text') + end + + it 'obfuscates SQL in both attributes when db_statement is :obfuscate' do + instrumentation.install({ + db_statement: :obfuscate, + span_name: :statement_type, + propagator: 'none', + record_exception: true, + obfuscation_limit: 2000, + peer_service: nil + }) + attrs = client.send(:client_attributes, 'SELECT * FROM users WHERE id = 1') + + old_stmt = attrs[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT] + new_stmt = attrs['db.query.text'] + + assert old_stmt, 'expected db.statement to be present' + assert new_stmt, 'expected db.query.text to be present' + refute_includes old_stmt, '1' + refute_includes new_stmt, '1' + assert_equal old_stmt, new_stmt + end + end + end + +end diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb new file mode 100644 index 0000000000..1168aaa472 --- /dev/null +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb @@ -0,0 +1,382 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +require_relative '../../../../../../lib/opentelemetry/instrumentation/trilogy' +require_relative '../../../../../../lib/opentelemetry/instrumentation/trilogy/patches/dup/client' + +describe 'OpenTelemetry::Instrumentation::Trilogy (dup semconv)' do + let(:instrumentation) { OpenTelemetry::Instrumentation::Trilogy::Instrumentation.instance } + let(:exporter) { EXPORTER } + let(:span) { exporter.finished_spans[1] } + let(:config) { {} } + let(:driver_options) do + { + host: host, + port: port, + username: username, + password: password, + database: database, + ssl: false + } + end + let(:client) do + Trilogy.new(driver_options) + end + + let(:host) { ENV.fetch('TEST_MYSQL_HOST', '127.0.0.1') } + let(:port) { ENV.fetch('TEST_MYSQL_PORT', '3306').to_i } + let(:database) { ENV.fetch('TEST_MYSQL_DB', 'mysql') } + let(:username) { ENV.fetch('TEST_MYSQL_USER', 'root') } + let(:password) { ENV.fetch('TEST_MYSQL_PASSWORD', 'root') } + + before do + skip unless ENV['BUNDLE_GEMFILE']&.include?('dup') + + exporter.reset + end + + after do + # Force re-install of instrumentation + instrumentation.instance_variable_set(:@installed, false) + end + + describe '#install' do + it 'accepts peer service name from config' do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install(peer_service: 'readonly:mysql') + client.query('SELECT 1') + + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::PEER_SERVICE]).must_equal 'readonly:mysql' + end + + it 'omits peer service by default' do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install({}) + client.query('SELECT 1') + + _(span.attributes.keys).wont_include(OpenTelemetry::SemanticConventions::Trace::PEER_SERVICE) + end + end + + describe 'tracing' do + before do + instrumentation.install(config) + end + + describe '.attributes' do + let(:attributes) { { 'db.statement' => 'foobar' } } + + it 'returns an empty hash by default' do + _(OpenTelemetry::Instrumentation::Trilogy.attributes).must_equal({}) + end + + it 'returns the current attributes hash' do + OpenTelemetry::Instrumentation::Trilogy.with_attributes(attributes) do + _(OpenTelemetry::Instrumentation::Trilogy.attributes).must_equal(attributes) + end + end + + it 'sets span attributes according to with_attributes hash' do + OpenTelemetry::Instrumentation::Trilogy.with_attributes(attributes) do + client.query('SELECT 1') + end + + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_equal 'foobar' + end + end + + describe 'with default options' do + it 'obfuscates sql in both old and stable attributes' do + client.query('SELECT 1') + + _(span.name).must_equal 'select' + # Old attribute + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_equal 'SELECT ?' + # Stable attribute + _(span.attributes['db.query.text']).must_equal 'SELECT ?' + end + + it 'includes both old and stable database connection information' do + client.query('SELECT 1') + + _(span.name).must_equal 'select' + + # Old attributes + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_SYSTEM]).must_equal 'mysql' + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME]).must_equal(host) + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_NAME]).must_equal(database) + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_USER]).must_equal(username) + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_equal 'SELECT ?' + + # Stable attributes + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['server.address']).must_equal(host) + _(span.attributes['db.namespace']).must_equal(database) + _(span.attributes['db.query.text']).must_equal 'SELECT ?' + end + + it 'includes server.port (stable) but not net.peer.port (was not in old)' do + client.query('SELECT 1') + + _(span.attributes['server.port']).must_equal port + _(span.attributes.key?('net.peer.port')).must_equal false + end + + it 'extracts statement type' do + explain_sql = 'EXPLAIN SELECT 1' + client.query(explain_sql) + + _(span.name).must_equal 'explain' + + # Old attributes + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_SYSTEM]).must_equal 'mysql' + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_equal 'EXPLAIN SELECT ?' + + # Stable attributes + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.query.text']).must_equal 'EXPLAIN SELECT ?' + end + + it 'uses component.name and instance.name as span.name fallbacks with invalid sql' do + expect do + client.query('DESELECT 1') + end.must_raise Trilogy::Error + + _(span.name).must_equal 'mysql' + + # Old attributes + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_SYSTEM]).must_equal 'mysql' + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_equal 'DESELECT ?' + + # Stable attributes + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.query.text']).must_equal 'DESELECT ?' + end + end + + describe 'when connecting' do + let(:span) { exporter.finished_spans.first } + + it 'includes both old and stable attributes for connect span' do + _(client.connected_host).wont_be_nil + + _(span.name).must_equal 'connect' + + # Old attributes + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_SYSTEM]).must_equal 'mysql' + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME]).must_equal(host) + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_NAME]).must_equal(database) + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_USER]).must_equal(username) + + # Stable attributes + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['server.address']).must_equal(host) + _(span.attributes['db.namespace']).must_equal(database) + end + end + + describe 'when pinging' do + let(:span) { exporter.finished_spans[2] } + + it 'includes both old and stable attributes for ping span' do + _(client.connected_host).wont_be_nil + + client.ping + + _(span.name).must_equal 'ping' + + # Old attributes + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_SYSTEM]).must_equal 'mysql' + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME]).must_equal(host) + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_NAME]).must_equal(database) + + # Stable attributes + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['server.address']).must_equal(host) + _(span.attributes['db.namespace']).must_equal(database) + end + end + + describe 'when queries fail' do + it 'sets span status to error' do + expect do + client.query('SELECT INVALID') + end.must_raise Trilogy::Error + + _(span.name).must_equal 'select' + + # Old attributes + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_SYSTEM]).must_equal 'mysql' + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_equal 'SELECT INVALID' + + # Stable attributes + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.query.text']).must_equal 'SELECT INVALID' + + _(span.status.code).must_equal( + OpenTelemetry::Trace::Status::ERROR + ) + end + + it 'sets error.type to the exception class name' do + error = nil + begin + client.query('SELECT INVALID') + rescue Trilogy::Error => e + error = e + end + + _(error).wont_be_nil + _(span.attributes['error.type']).must_equal error.class.name + end + + it 'sets db.response.status_code when error has error_code' do + error = nil + begin + client.query('SELECT INVALID') + rescue Trilogy::Error => e + error = e + end + + _(error).wont_be_nil + if error.error_code + _(span.attributes['db.response.status_code']).must_equal error.error_code.to_s + end + end + + describe 'when record_exception is true' do + let(:config) { { record_exception: true } } + + it 'records the exception' do + expect do + client.query('SELECT INVALID') + end.must_raise Trilogy::Error + + _(span.events).wont_be_nil + _(span.events.first.name).must_equal 'exception' + _(span.events.first.attributes['exception.type']).must_match(/Trilogy.*Error/) + _(span.events.first.attributes['exception.message']).wont_be_nil + _(span.events.first.attributes['exception.stacktrace']).wont_be_nil + end + end + + describe 'when record_exception is false' do + let(:config) { { record_exception: false } } + + it 'does not record the exception' do + expect do + client.query('SELECT INVALID') + end.must_raise Trilogy::Error + + _(span.events).must_be_nil + end + end + end + + describe 'when db_statement is set to include' do + let(:config) { { db_statement: :include } } + + it 'includes the db query statement in both attributes' do + sql = 'SELECT * from users where users.id = 1 and users.email = "test@test.com"' + expect do + client.query(sql) + end.must_raise Trilogy::Error + + _(span.name).must_equal 'select' + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_equal sql + _(span.attributes['db.query.text']).must_equal sql + end + end + + describe 'when db_statement is set to obfuscate' do + let(:config) { { db_statement: :obfuscate } } + + it 'obfuscates SQL parameters in both attributes' do + sql = 'SELECT * from users where users.id = 1 and users.email = "test@test.com"' + obfuscated_sql = 'SELECT * from users where users.id = ? and users.email = ?' + expect do + client.query(sql) + end.must_raise Trilogy::Error + + _(span.name).must_equal 'select' + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_equal obfuscated_sql + _(span.attributes['db.query.text']).must_equal obfuscated_sql + end + + it 'encodes invalid byte sequences' do + # \255 is off-limits https://en.wikipedia.org/wiki/UTF-8#Codepage_layout + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com\255'" + obfuscated_sql = 'SELECT * from users where users.id = ? and users.email = ?' + + expect do + client.query(sql) + end.must_raise Trilogy::Error + + _(span.name).must_equal 'select' + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_equal obfuscated_sql + _(span.attributes['db.query.text']).must_equal obfuscated_sql + end + + describe 'with obfuscation_limit' do + let(:config) { { db_statement: :obfuscate, obfuscation_limit: 10 } } + + it 'returns a message when the limit is reached' do + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" + obfuscated_sql = 'SQL not obfuscated, query exceeds 10 characters' + expect do + client.query(sql) + end.must_raise Trilogy::Error + + _(span.attributes['db.statement']).must_equal obfuscated_sql + _(span.attributes['db.query.text']).must_equal obfuscated_sql + end + end + end + + describe 'when db_statement is set to omit' do + let(:config) { { db_statement: :omit } } + + it 'does not include SQL statement in either attribute' do + sql = 'SELECT * from users where users.id = 1 and users.email = "test@test.com"' + expect do + client.query(sql) + end.must_raise Trilogy::Error + + _(span.name).must_equal 'select' + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_be_nil + _(span.attributes['db.query.text']).must_be_nil + 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 and users.email = "test@test.com"' + original_sql = sql.dup + expect do + client.query(sql) + end.must_raise Trilogy::Error + _(sql).must_equal original_sql + end + end + + describe 'when propagator is set to nil' do + let(:config) { { propagator: nil } } + + it 'does not inject context' do + sql = +'SELECT * from users where users.id = 1 and users.email = "test@test.com"' + original_sql = sql.dup + expect do + client.query(sql) + end.must_raise Trilogy::Error + _(sql).must_equal original_sql + end + end + end +end diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/client_attributes_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/old/client_attributes_test.rb similarity index 95% rename from instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/client_attributes_test.rb rename to instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/old/client_attributes_test.rb index 3f9546f6b7..f8299ea588 100644 --- a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/client_attributes_test.rb +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/old/client_attributes_test.rb @@ -6,8 +6,8 @@ require 'test_helper' -require_relative '../../../../../lib/opentelemetry/instrumentation/trilogy' -require_relative '../../../../../lib/opentelemetry/instrumentation/trilogy/patches/client' +require_relative '../../../../../../lib/opentelemetry/instrumentation/trilogy' +require_relative '../../../../../../lib/opentelemetry/instrumentation/trilogy/patches/old/client' # Unit tests for the client_attributes hot path that do not require # a MySQL connection. We use Trilogy.allocate + manual ivar setup @@ -22,7 +22,7 @@ def build_test_client(options) c end -describe OpenTelemetry::Instrumentation::Trilogy::Patches::Client do +describe OpenTelemetry::Instrumentation::Trilogy::Patches::Old::Client do let(:instrumentation) { OpenTelemetry::Instrumentation::Trilogy::Instrumentation.instance } let(:exporter) { EXPORTER } @@ -37,6 +37,8 @@ def build_test_client(options) let(:client) { build_test_client(connection_options) } before do + skip unless ENV['BUNDLE_GEMFILE'].include?('old') + exporter.reset instrumentation.instance_variable_set(:@installed, false) instrumentation.install({ @@ -172,4 +174,4 @@ def build_test_client(options) end end end -end +end \ No newline at end of file diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/instrumentation_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/old/instrumentation_test.rb similarity index 98% rename from instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/instrumentation_test.rb rename to instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/old/instrumentation_test.rb index 8290018e0e..c3ea3bfff0 100644 --- a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/instrumentation_test.rb +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/old/instrumentation_test.rb @@ -6,10 +6,10 @@ require 'test_helper' -require_relative '../../../../lib/opentelemetry/instrumentation/trilogy' -require_relative '../../../../lib/opentelemetry/instrumentation/trilogy/patches/client' +require_relative '../../../../../../lib/opentelemetry/instrumentation/trilogy' +require_relative '../../../../../../lib/opentelemetry/instrumentation/trilogy/patches/old/client' -describe OpenTelemetry::Instrumentation::Trilogy do +describe 'OpenTelemetry::Instrumentation::Trilogy (old semconv)' do let(:instrumentation) { OpenTelemetry::Instrumentation::Trilogy::Instrumentation.instance } let(:exporter) { EXPORTER } let(:span) { exporter.finished_spans[1] } @@ -35,6 +35,8 @@ let(:password) { ENV.fetch('TEST_MYSQL_PASSWORD', 'root') } before do + skip unless ENV['BUNDLE_GEMFILE']&.include?('old') + exporter.reset end diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/client_attributes_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/client_attributes_test.rb new file mode 100644 index 0000000000..f0f927123f --- /dev/null +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/client_attributes_test.rb @@ -0,0 +1,205 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +require_relative '../../../../../../lib/opentelemetry/instrumentation/trilogy' +require_relative '../../../../../../lib/opentelemetry/instrumentation/trilogy/patches/stable/client' + +# Unit tests for the stable semantic conventions client_attributes. +# We use Trilogy.allocate + manual ivar setup to test attribute building in isolation. +describe OpenTelemetry::Instrumentation::Trilogy::Patches::Stable::Client do + # Helper to build a test client without a real MySQL connection. + def build_test_client(options) + c = Trilogy.allocate + c.instance_variable_set(:@connection_options, options) + c.instance_variable_set(:@_otel_database_name, options[:database]) + c.instance_variable_set(:@_otel_base_attributes, c.send(:_build_otel_base_attributes).freeze) + c + end + + let(:instrumentation) { OpenTelemetry::Instrumentation::Trilogy::Instrumentation.instance } + let(:exporter) { EXPORTER } + + let(:connection_options) do + { + host: 'db-primary.example.com', + port: 3307, + database: 'myapp_production', + username: 'app_user' + } + end + + let(:client) { build_test_client(connection_options) } + + before do + skip unless ENV['BUNDLE_GEMFILE'].include?('stable') + + exporter.reset + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install({ + db_statement: :omit, + span_name: :statement_type, + propagator: 'none', + record_exception: true, + obfuscation_limit: 2000, + peer_service: nil + }) + end + + after do + instrumentation.instance_variable_set(:@installed, false) + end + + describe '#client_attributes' do + it 'includes db.system.name as mysql' do + attrs = client.send(:client_attributes) + assert_equal 'mysql', attrs['db.system.name'] + end + + it 'includes server.address from host option' do + attrs = client.send(:client_attributes) + assert_equal 'db-primary.example.com', attrs['server.address'] + end + + it 'includes server.port when present' do + attrs = client.send(:client_attributes) + assert_equal 3307, attrs['server.port'] + end + + it 'includes server.port even when default (3306)' do + c = build_test_client({ host: 'h', port: 3306 }) + attrs = c.send(:client_attributes) + assert_equal 3306, attrs['server.port'] + end + + it 'includes db.namespace from database option' do + attrs = client.send(:client_attributes) + assert_equal 'myapp_production', attrs['db.namespace'] + end + + it 'falls back to unknown sock when host is nil' do + c = build_test_client({ database: 'test' }) + attrs = c.send(:client_attributes) + assert_equal 'unknown sock', attrs['server.address'] + end + + it 'omits db.namespace when database is nil' do + c = build_test_client({ host: 'h' }) + attrs = c.send(:client_attributes) + refute attrs.key?('db.namespace') + end + + it 'includes peer_service when configured' do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.instance_variable_set(:@semconv, :stable) + instrumentation.install({ + db_statement: :omit, + span_name: :statement_type, + propagator: 'none', + record_exception: true, + obfuscation_limit: 2000, + peer_service: 'mysql-primary' + }) + attrs = client.send(:client_attributes) + assert_equal 'mysql-primary', attrs[OpenTelemetry::SemanticConventions::Trace::PEER_SERVICE] + end + + it 'returns independent hash instances on each call' do + a = client.send(:client_attributes) + b = client.send(:client_attributes) + refute_same a, b + a['extra'] = 'value' + refute b.key?('extra') + end + + describe 'with sql and db_statement config' do + before do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.instance_variable_set(:@semconv, :stable) + end + + it 'includes SQL as db.query.text when db_statement is :include' do + instrumentation.install({ + db_statement: :include, + span_name: :statement_type, + propagator: 'none', + record_exception: true, + obfuscation_limit: 2000, + peer_service: nil + }) + attrs = client.send(:client_attributes, 'SELECT * FROM users') + assert_equal 'SELECT * FROM users', attrs['db.query.text'] + end + + it 'omits db.query.text when db_statement is :omit' do + instrumentation.install({ + db_statement: :omit, + span_name: :statement_type, + propagator: 'none', + record_exception: true, + obfuscation_limit: 2000, + peer_service: nil + }) + attrs = client.send(:client_attributes, 'SELECT * FROM users') + refute attrs.key?('db.query.text') + end + + it 'obfuscates SQL in db.query.text when db_statement is :obfuscate' do + instrumentation.install({ + db_statement: :obfuscate, + span_name: :statement_type, + propagator: 'none', + record_exception: true, + obfuscation_limit: 2000, + peer_service: nil + }) + attrs = client.send(:client_attributes, 'SELECT * FROM users WHERE id = 1') + stmt = attrs['db.query.text'] + assert stmt, 'expected db.query.text to be present' + refute_includes stmt, '1' + end + end + + describe 'does not include old attributes' do + it 'does not include db.system' do + attrs = client.send(:client_attributes) + refute attrs.key?(OpenTelemetry::SemanticConventions::Trace::DB_SYSTEM) + end + + it 'does not include net.peer.name' do + attrs = client.send(:client_attributes) + refute attrs.key?(OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME) + end + + it 'does not include db.name' do + attrs = client.send(:client_attributes) + refute attrs.key?(OpenTelemetry::SemanticConventions::Trace::DB_NAME) + end + + it 'does not include db.user (removed in stable)' do + attrs = client.send(:client_attributes) + refute attrs.key?(OpenTelemetry::SemanticConventions::Trace::DB_USER) + end + + it 'does not include db.statement' do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.instance_variable_set(:@semconv, :stable) + instrumentation.install({ + db_statement: :include, + span_name: :statement_type, + propagator: 'none', + record_exception: true, + obfuscation_limit: 2000, + peer_service: nil + }) + attrs = client.send(:client_attributes, 'SELECT * FROM users') + refute attrs.key?(OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT) + end + end + end + +end diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb new file mode 100644 index 0000000000..0c0c7baeb0 --- /dev/null +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb @@ -0,0 +1,343 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +require_relative '../../../../../../lib/opentelemetry/instrumentation/trilogy' +require_relative '../../../../../../lib/opentelemetry/instrumentation/trilogy/patches/stable/client' + +describe 'OpenTelemetry::Instrumentation::Trilogy (stable semconv)' do + let(:instrumentation) { OpenTelemetry::Instrumentation::Trilogy::Instrumentation.instance } + let(:exporter) { EXPORTER } + let(:span) { exporter.finished_spans[1] } + let(:config) { {} } + let(:driver_options) do + { + host: host, + port: port, + username: username, + password: password, + database: database, + ssl: false + } + end + let(:client) do + Trilogy.new(driver_options) + end + + let(:host) { ENV.fetch('TEST_MYSQL_HOST', '127.0.0.1') } + let(:port) { ENV.fetch('TEST_MYSQL_PORT', '3306').to_i } + let(:database) { ENV.fetch('TEST_MYSQL_DB', 'mysql') } + let(:username) { ENV.fetch('TEST_MYSQL_USER', 'root') } + let(:password) { ENV.fetch('TEST_MYSQL_PASSWORD', 'root') } + + before do + skip unless ENV['BUNDLE_GEMFILE']&.include?('stable') + + exporter.reset + end + + after do + # Force re-install of instrumentation + instrumentation.instance_variable_set(:@installed, false) + end + + describe '#install' do + it 'accepts peer service name from config' do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install(peer_service: 'readonly:mysql') + client.query('SELECT 1') + + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::PEER_SERVICE]).must_equal 'readonly:mysql' + end + + it 'omits peer service by default' do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install({}) + client.query('SELECT 1') + + _(span.attributes.keys).wont_include(OpenTelemetry::SemanticConventions::Trace::PEER_SERVICE) + end + end + + describe 'tracing' do + before do + instrumentation.install(config) + end + + describe '.attributes' do + let(:attributes) { { 'db.query.text' => 'foobar' } } + + it 'returns an empty hash by default' do + _(OpenTelemetry::Instrumentation::Trilogy.attributes).must_equal({}) + end + + it 'returns the current attributes hash' do + OpenTelemetry::Instrumentation::Trilogy.with_attributes(attributes) do + _(OpenTelemetry::Instrumentation::Trilogy.attributes).must_equal(attributes) + end + end + + it 'sets span attributes according to with_attributes hash' do + OpenTelemetry::Instrumentation::Trilogy.with_attributes(attributes) do + client.query('SELECT 1') + end + + _(span.attributes['db.query.text']).must_equal 'foobar' + end + end + + describe 'with default options' do + it 'obfuscates sql in db.query.text' do + client.query('SELECT 1') + + _(span.name).must_equal 'select' + _(span.attributes['db.query.text']).must_equal 'SELECT ?' + end + + it 'includes stable database connection information' do + client.query('SELECT 1') + + _(span.name).must_equal 'select' + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['server.address']).must_equal(host) + _(span.attributes['db.namespace']).must_equal(database) + _(span.attributes['db.query.text']).must_equal 'SELECT ?' + end + + it 'does not include old attribute names' do + client.query('SELECT 1') + + _(span.attributes.key?('db.system')).must_equal false + _(span.attributes.key?('net.peer.name')).must_equal false + _(span.attributes.key?('db.name')).must_equal false + _(span.attributes.key?('db.statement')).must_equal false + _(span.attributes.key?('db.user')).must_equal false + end + + it 'includes server.port when present' do + client.query('SELECT 1') + + _(span.attributes['server.port']).must_equal port + end + + it 'extracts statement type' do + explain_sql = 'EXPLAIN SELECT 1' + client.query(explain_sql) + + _(span.name).must_equal 'explain' + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.query.text']).must_equal 'EXPLAIN SELECT ?' + end + + it 'uses component.name and instance.name as span.name fallbacks with invalid sql' do + expect do + client.query('DESELECT 1') + end.must_raise Trilogy::Error + + _(span.name).must_equal 'mysql' + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.query.text']).must_equal 'DESELECT ?' + end + end + + describe 'when connecting' do + let(:span) { exporter.finished_spans.first } + + it 'uses stable attributes for connect span' do + _(client.connected_host).wont_be_nil + + _(span.name).must_equal 'connect' + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['server.address']).must_equal(host) + _(span.attributes['db.namespace']).must_equal(database) + end + end + + describe 'when pinging' do + let(:span) { exporter.finished_spans[2] } + + it 'uses stable attributes for ping span' do + _(client.connected_host).wont_be_nil + + client.ping + + _(span.name).must_equal 'ping' + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['server.address']).must_equal(host) + _(span.attributes['db.namespace']).must_equal(database) + end + end + + describe 'when queries fail' do + it 'sets span status to error' do + expect do + client.query('SELECT INVALID') + end.must_raise Trilogy::Error + + _(span.name).must_equal 'select' + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.query.text']).must_equal 'SELECT INVALID' + + _(span.status.code).must_equal( + OpenTelemetry::Trace::Status::ERROR + ) + end + + it 'sets error.type to the exception class name' do + error = nil + begin + client.query('SELECT INVALID') + rescue Trilogy::Error => e + error = e + end + + _(error).wont_be_nil + _(span.attributes['error.type']).must_equal error.class.name + end + + it 'sets db.response.status_code when error has error_code' do + error = nil + begin + client.query('SELECT INVALID') + rescue Trilogy::Error => e + error = e + end + + _(error).wont_be_nil + if error.error_code + _(span.attributes['db.response.status_code']).must_equal error.error_code.to_s + end + end + + describe 'when record_exception is true' do + let(:config) { { record_exception: true } } + + it 'records the exception' do + expect do + client.query('SELECT INVALID') + end.must_raise Trilogy::Error + + _(span.events).wont_be_nil + _(span.events.first.name).must_equal 'exception' + _(span.events.first.attributes['exception.type']).must_match(/Trilogy.*Error/) + _(span.events.first.attributes['exception.message']).wont_be_nil + _(span.events.first.attributes['exception.stacktrace']).wont_be_nil + end + end + + describe 'when record_exception is false' do + let(:config) { { record_exception: false } } + + it 'does not record the exception' do + expect do + client.query('SELECT INVALID') + end.must_raise Trilogy::Error + + _(span.events).must_be_nil + end + end + end + + describe 'when db_statement is set to include' do + let(:config) { { db_statement: :include } } + + it 'includes the query in db.query.text' do + sql = 'SELECT * from users where users.id = 1 and users.email = "test@test.com"' + expect do + client.query(sql) + end.must_raise Trilogy::Error + + _(span.name).must_equal 'select' + _(span.attributes['db.query.text']).must_equal sql + _(span.attributes.key?('db.statement')).must_equal false + end + end + + describe 'when db_statement is set to obfuscate' do + let(:config) { { db_statement: :obfuscate } } + + it 'obfuscates SQL parameters in db.query.text' do + sql = 'SELECT * from users where users.id = 1 and users.email = "test@test.com"' + obfuscated_sql = 'SELECT * from users where users.id = ? and users.email = ?' + expect do + client.query(sql) + end.must_raise Trilogy::Error + + _(span.name).must_equal 'select' + _(span.attributes['db.query.text']).must_equal obfuscated_sql + _(span.attributes.key?('db.statement')).must_equal false + end + + it 'encodes invalid byte sequences for db.query.text' do + # \255 is off-limits https://en.wikipedia.org/wiki/UTF-8#Codepage_layout + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com\255'" + obfuscated_sql = 'SELECT * from users where users.id = ? and users.email = ?' + + expect do + client.query(sql) + end.must_raise Trilogy::Error + + _(span.name).must_equal 'select' + _(span.attributes['db.query.text']).must_equal obfuscated_sql + end + + describe 'with obfuscation_limit' do + let(:config) { { db_statement: :obfuscate, obfuscation_limit: 10 } } + + it 'returns a message when the limit is reached' do + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" + obfuscated_sql = 'SQL not obfuscated, query exceeds 10 characters' + expect do + client.query(sql) + end.must_raise Trilogy::Error + + _(span.attributes['db.query.text']).must_equal obfuscated_sql + end + end + end + + describe 'when db_statement is set to omit' do + let(:config) { { db_statement: :omit } } + + it 'does not include SQL statement as db.query.text attribute' do + sql = 'SELECT * from users where users.id = 1 and users.email = "test@test.com"' + expect do + client.query(sql) + end.must_raise Trilogy::Error + + _(span.name).must_equal 'select' + _(span.attributes['db.query.text']).must_be_nil + 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 and users.email = "test@test.com"' + original_sql = sql.dup + expect do + client.query(sql) + end.must_raise Trilogy::Error + _(sql).must_equal original_sql + end + end + + describe 'when propagator is set to nil' do + let(:config) { { propagator: nil } } + + it 'does not inject context' do + sql = +'SELECT * from users where users.id = 1 and users.email = "test@test.com"' + original_sql = sql.dup + expect do + client.query(sql) + end.must_raise Trilogy::Error + _(sql).must_equal original_sql + end + end + end +end From 4d62100a102f900580dfdfe229739f4aefdbeab0 Mon Sep 17 00:00:00 2001 From: Hannah Ramadan Date: Wed, 18 Mar 2026 13:49:19 -0700 Subject: [PATCH 02/23] Rubocop --- .../instrumentation/trilogy/patches/dup/client.rb | 1 - .../instrumentation/trilogy/patches/stable/client.rb | 1 - .../trilogy/patches/dup/client_attributes_test.rb | 1 - .../trilogy/patches/dup/instrumentation_test.rb | 4 +--- .../trilogy/patches/old/client_attributes_test.rb | 2 +- .../trilogy/patches/stable/client_attributes_test.rb | 1 - .../trilogy/patches/stable/instrumentation_test.rb | 4 +--- 7 files changed, 3 insertions(+), 11 deletions(-) diff --git a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/dup/client.rb b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/dup/client.rb index b0bbef79f8..df19f3816a 100644 --- a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/dup/client.rb +++ b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/dup/client.rb @@ -14,7 +14,6 @@ module Patches module Dup # Module to prepend to Trilogy for instrumentation (emits both old and stable semantic conventions) module Client - def initialize(options = {}) @connection_options = options # This is normally done by Trilogy#initialize @_otel_database_name = connection_options&.dig(:database) diff --git a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb index 9c9a412bdc..611caa6539 100644 --- a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb +++ b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb @@ -14,7 +14,6 @@ module Patches module Stable # Module to prepend to Trilogy for instrumentation (stable semantic conventions) module Client - def initialize(options = {}) @connection_options = options # This is normally done by Trilogy#initialize @_otel_database_name = connection_options&.dig(:database) diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/client_attributes_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/client_attributes_test.rb index 55808bbe92..f87e92346f 100644 --- a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/client_attributes_test.rb +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/client_attributes_test.rb @@ -181,5 +181,4 @@ def build_test_client(options) end end end - end diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb index 1168aaa472..80aa437fe9 100644 --- a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb @@ -244,9 +244,7 @@ end _(error).wont_be_nil - if error.error_code - _(span.attributes['db.response.status_code']).must_equal error.error_code.to_s - end + _(span.attributes['db.response.status_code']).must_equal error.error_code.to_s if error.error_code end describe 'when record_exception is true' do diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/old/client_attributes_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/old/client_attributes_test.rb index f8299ea588..e75411494d 100644 --- a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/old/client_attributes_test.rb +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/old/client_attributes_test.rb @@ -174,4 +174,4 @@ def build_test_client(options) end end end -end \ No newline at end of file +end diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/client_attributes_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/client_attributes_test.rb index f0f927123f..b0242e6e6f 100644 --- a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/client_attributes_test.rb +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/client_attributes_test.rb @@ -201,5 +201,4 @@ def build_test_client(options) end end end - end diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb index 0c0c7baeb0..995ad7f775 100644 --- a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb @@ -208,9 +208,7 @@ end _(error).wont_be_nil - if error.error_code - _(span.attributes['db.response.status_code']).must_equal error.error_code.to_s - end + _(span.attributes['db.response.status_code']).must_equal error.error_code.to_s if error.error_code end describe 'when record_exception is true' do From 5298416288cdef24a13d3ff7c506182053877d24 Mon Sep 17 00:00:00 2001 From: Hannah Ramadan Date: Wed, 18 Mar 2026 13:51:43 -0700 Subject: [PATCH 03/23] File format Use double quotes in rubocop.yml --- instrumentation/trilogy/.rubocop.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/instrumentation/trilogy/.rubocop.yml b/instrumentation/trilogy/.rubocop.yml index 47a500e984..6a0260700e 100644 --- a/instrumentation/trilogy/.rubocop.yml +++ b/instrumentation/trilogy/.rubocop.yml @@ -2,5 +2,5 @@ inherit_from: ../../.rubocop.yml Metrics/ModuleLength: Exclude: - - 'lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb' - - 'lib/opentelemetry/instrumentation/trilogy/patches/dup/client.rb' + - "lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb" + - "lib/opentelemetry/instrumentation/trilogy/patches/dup/client.rb" From 3425850b82dea12d2a8650cec782ce7c36b55595 Mon Sep 17 00:00:00 2001 From: Hannah Ramadan Date: Wed, 18 Mar 2026 14:33:17 -0700 Subject: [PATCH 04/23] Update tests --- .../patches/dup/client_attributes_test.rb | 116 ++++++++++++------ .../patches/old/client_attributes_test.rb | 19 ++- .../patches/stable/client_attributes_test.rb | 90 +++++++------- 3 files changed, 131 insertions(+), 94 deletions(-) diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/client_attributes_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/client_attributes_test.rb index f87e92346f..99f63cc556 100644 --- a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/client_attributes_test.rb +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/client_attributes_test.rb @@ -10,9 +10,10 @@ require_relative '../../../../../../lib/opentelemetry/instrumentation/trilogy/patches/dup/client' # Unit tests for the dup semantic conventions client_attributes. -# Verifies that both old and stable attributes are emitted. +# We use Trilogy.allocate + manual ivar setup to test attribute building in isolation. describe OpenTelemetry::Instrumentation::Trilogy::Patches::Dup::Client do # Helper to build a test client without a real MySQL connection. + # Mirrors what initialize does for attribute setup. def build_test_client(options) c = Trilogy.allocate c.instance_variable_set(:@connection_options, options) @@ -36,7 +37,7 @@ def build_test_client(options) let(:client) { build_test_client(connection_options) } before do - skip unless ENV['BUNDLE_GEMFILE'].include?('dup') + skip unless ENV['BUNDLE_GEMFILE']&.include?('dup') exporter.reset instrumentation.instance_variable_set(:@installed, false) @@ -55,45 +56,69 @@ def build_test_client(options) end describe '#client_attributes' do - describe 'includes both old and stable attributes' do - it 'includes both db.system (old) and db.system.name (stable)' do - attrs = client.send(:client_attributes) - assert_equal 'mysql', attrs[OpenTelemetry::SemanticConventions::Trace::DB_SYSTEM] - assert_equal 'mysql', attrs['db.system.name'] - end + it 'includes db.system (old) as mysql' do + attrs = client.send(:client_attributes) + assert_equal 'mysql', attrs[OpenTelemetry::SemanticConventions::Trace::DB_SYSTEM] + end - it 'includes both net.peer.name (old) and server.address (stable)' do - attrs = client.send(:client_attributes) - assert_equal 'db-primary.example.com', attrs[OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME] - assert_equal 'db-primary.example.com', attrs['server.address'] - end + it 'includes db.system.name (stable) as mysql' do + attrs = client.send(:client_attributes) + assert_equal 'mysql', attrs['db.system.name'] + end - it 'includes both db.name (old) and db.namespace (stable)' do - attrs = client.send(:client_attributes) - assert_equal 'myapp_production', attrs[OpenTelemetry::SemanticConventions::Trace::DB_NAME] - assert_equal 'myapp_production', attrs['db.namespace'] - end + it 'includes net.peer.name (old) from host option' do + attrs = client.send(:client_attributes) + assert_equal 'db-primary.example.com', attrs[OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME] + end - it 'includes db.user (old only - removed in stable)' do - attrs = client.send(:client_attributes) - assert_equal 'app_user', attrs[OpenTelemetry::SemanticConventions::Trace::DB_USER] - end + it 'includes server.address (stable) from host option' do + attrs = client.send(:client_attributes) + assert_equal 'db-primary.example.com', attrs['server.address'] + end - it 'includes server.port (stable) when present' do - attrs = client.send(:client_attributes) - assert_equal 3307, attrs['server.port'] - end + it 'includes db.name (old) from database option' do + attrs = client.send(:client_attributes) + assert_equal 'myapp_production', attrs[OpenTelemetry::SemanticConventions::Trace::DB_NAME] + end - it 'includes server.port even when default port (3306)' do - c = build_test_client({ host: 'h', port: 3306, database: 'test' }) - attrs = c.send(:client_attributes) - assert_equal 3306, attrs['server.port'] - end + it 'includes db.namespace (stable) from database option' do + attrs = client.send(:client_attributes) + assert_equal 'myapp_production', attrs['db.namespace'] + end - it 'does not include net.peer.port (was not in old)' do - attrs = client.send(:client_attributes) - refute attrs.key?(OpenTelemetry::SemanticConventions::Trace::NET_PEER_PORT) - end + it 'includes db.user (old) from username option' do + attrs = client.send(:client_attributes) + assert_equal 'app_user', attrs[OpenTelemetry::SemanticConventions::Trace::DB_USER] + end + + it 'includes server.port (stable) when present' do + attrs = client.send(:client_attributes) + assert_equal 3307, attrs['server.port'] + end + + it 'does not include net.peer.port (was not in old)' do + attrs = client.send(:client_attributes) + refute attrs.key?(OpenTelemetry::SemanticConventions::Trace::NET_PEER_PORT) + end + + it 'falls back to unknown sock when host is nil' do + c = build_test_client({ database: 'test' }) + attrs = c.send(:client_attributes) + assert_equal 'unknown sock', attrs[OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME] + assert_equal 'unknown sock', attrs['server.address'] + end + + it 'omits db.name and db.namespace when database is nil' do + c = build_test_client({ host: 'h' }) + attrs = c.send(:client_attributes) + refute attrs.key?(OpenTelemetry::SemanticConventions::Trace::DB_NAME) + refute attrs.key?('db.namespace') + end + + it 'omits db.user when username is nil' do + c = build_test_client({ host: 'h' }) + attrs = c.send(:client_attributes) + refute attrs.key?(OpenTelemetry::SemanticConventions::Trace::DB_USER) end it 'includes db.instance.id when connected_host is set' do @@ -102,9 +127,13 @@ def build_test_client(options) assert_equal 'replica-3.internal', attrs['db.instance.id'] end + it 'omits db.instance.id when connected_host is nil' do + attrs = client.send(:client_attributes) + refute attrs.key?('db.instance.id') + end + it 'includes peer_service when configured' do instrumentation.instance_variable_set(:@installed, false) - instrumentation.instance_variable_set(:@semconv, :dup) instrumentation.install({ db_statement: :omit, span_name: :statement_type, @@ -128,10 +157,9 @@ def build_test_client(options) describe 'with sql and db_statement config' do before do instrumentation.instance_variable_set(:@installed, false) - instrumentation.instance_variable_set(:@semconv, :dup) end - it 'includes SQL in both db.statement (old) and db.query.text (stable) when db_statement is :include' do + it 'includes SQL in db.statement (old) when db_statement is :include' do instrumentation.install({ db_statement: :include, span_name: :statement_type, @@ -142,6 +170,18 @@ def build_test_client(options) }) attrs = client.send(:client_attributes, 'SELECT * FROM users') assert_equal 'SELECT * FROM users', attrs[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT] + end + + it 'includes SQL in db.query.text (stable) when db_statement is :include' do + instrumentation.install({ + db_statement: :include, + span_name: :statement_type, + propagator: 'none', + record_exception: true, + obfuscation_limit: 2000, + peer_service: nil + }) + attrs = client.send(:client_attributes, 'SELECT * FROM users') assert_equal 'SELECT * FROM users', attrs['db.query.text'] end diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/old/client_attributes_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/old/client_attributes_test.rb index e75411494d..3bf7195c92 100644 --- a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/old/client_attributes_test.rb +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/old/client_attributes_test.rb @@ -12,17 +12,16 @@ # Unit tests for the client_attributes hot path that do not require # a MySQL connection. We use Trilogy.allocate + manual ivar setup # to test attribute building in isolation. -# Helper to build a test client without a real MySQL connection. -# Mirrors what initialize does for attribute setup. -def build_test_client(options) - c = Trilogy.allocate - c.instance_variable_set(:@connection_options, options) - c.instance_variable_set(:@_otel_database_name, options[:database]) - c.instance_variable_set(:@_otel_base_attributes, c.send(:_build_otel_base_attributes).freeze) - c -end - describe OpenTelemetry::Instrumentation::Trilogy::Patches::Old::Client do + # Helper to build a test client without a real MySQL connection. + # Mirrors what initialize does for attribute setup. + def build_test_client(options) + c = Trilogy.allocate + c.instance_variable_set(:@connection_options, options) + c.instance_variable_set(:@_otel_database_name, options[:database]) + c.instance_variable_set(:@_otel_base_attributes, c.send(:_build_otel_base_attributes).freeze) + c + end let(:instrumentation) { OpenTelemetry::Instrumentation::Trilogy::Instrumentation.instance } let(:exporter) { EXPORTER } diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/client_attributes_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/client_attributes_test.rb index b0242e6e6f..d5d76243ec 100644 --- a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/client_attributes_test.rb +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/client_attributes_test.rb @@ -13,6 +13,7 @@ # We use Trilogy.allocate + manual ivar setup to test attribute building in isolation. describe OpenTelemetry::Instrumentation::Trilogy::Patches::Stable::Client do # Helper to build a test client without a real MySQL connection. + # Mirrors what initialize does for attribute setup. def build_test_client(options) c = Trilogy.allocate c.instance_variable_set(:@connection_options, options) @@ -36,7 +37,7 @@ def build_test_client(options) let(:client) { build_test_client(connection_options) } before do - skip unless ENV['BUNDLE_GEMFILE'].include?('stable') + skip unless ENV['BUNDLE_GEMFILE']&.include?('stable') exporter.reset instrumentation.instance_variable_set(:@installed, false) @@ -70,12 +71,6 @@ def build_test_client(options) assert_equal 3307, attrs['server.port'] end - it 'includes server.port even when default (3306)' do - c = build_test_client({ host: 'h', port: 3306 }) - attrs = c.send(:client_attributes) - assert_equal 3306, attrs['server.port'] - end - it 'includes db.namespace from database option' do attrs = client.send(:client_attributes) assert_equal 'myapp_production', attrs['db.namespace'] @@ -93,9 +88,19 @@ def build_test_client(options) refute attrs.key?('db.namespace') end + it 'does not include db.user (removed in stable)' do + attrs = client.send(:client_attributes) + refute attrs.key?(OpenTelemetry::SemanticConventions::Trace::DB_USER) + end + + it 'does not include db.instance.id (removed in stable)' do + client.instance_variable_set(:@connected_host, 'replica-3.internal') + attrs = client.send(:client_attributes) + refute attrs.key?('db.instance.id') + end + it 'includes peer_service when configured' do instrumentation.instance_variable_set(:@installed, false) - instrumentation.instance_variable_set(:@semconv, :stable) instrumentation.install({ db_statement: :omit, span_name: :statement_type, @@ -116,10 +121,40 @@ def build_test_client(options) refute b.key?('extra') end + describe 'does not include old attributes' do + it 'does not include db.system' do + attrs = client.send(:client_attributes) + refute attrs.key?(OpenTelemetry::SemanticConventions::Trace::DB_SYSTEM) + end + + it 'does not include net.peer.name' do + attrs = client.send(:client_attributes) + refute attrs.key?(OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME) + end + + it 'does not include db.name' do + attrs = client.send(:client_attributes) + refute attrs.key?(OpenTelemetry::SemanticConventions::Trace::DB_NAME) + end + + it 'does not include db.statement when db_statement is :include' do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install({ + db_statement: :include, + span_name: :statement_type, + propagator: 'none', + record_exception: true, + obfuscation_limit: 2000, + peer_service: nil + }) + attrs = client.send(:client_attributes, 'SELECT * FROM users') + refute attrs.key?(OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT) + end + end + describe 'with sql and db_statement config' do before do instrumentation.instance_variable_set(:@installed, false) - instrumentation.instance_variable_set(:@semconv, :stable) end it 'includes SQL as db.query.text when db_statement is :include' do @@ -163,42 +198,5 @@ def build_test_client(options) refute_includes stmt, '1' end end - - describe 'does not include old attributes' do - it 'does not include db.system' do - attrs = client.send(:client_attributes) - refute attrs.key?(OpenTelemetry::SemanticConventions::Trace::DB_SYSTEM) - end - - it 'does not include net.peer.name' do - attrs = client.send(:client_attributes) - refute attrs.key?(OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME) - end - - it 'does not include db.name' do - attrs = client.send(:client_attributes) - refute attrs.key?(OpenTelemetry::SemanticConventions::Trace::DB_NAME) - end - - it 'does not include db.user (removed in stable)' do - attrs = client.send(:client_attributes) - refute attrs.key?(OpenTelemetry::SemanticConventions::Trace::DB_USER) - end - - it 'does not include db.statement' do - instrumentation.instance_variable_set(:@installed, false) - instrumentation.instance_variable_set(:@semconv, :stable) - instrumentation.install({ - db_statement: :include, - span_name: :statement_type, - propagator: 'none', - record_exception: true, - obfuscation_limit: 2000, - peer_service: nil - }) - attrs = client.send(:client_attributes, 'SELECT * FROM users') - refute attrs.key?(OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT) - end - end end end From 459e99b0a9f27d7a52985925dc44546e1eb010f1 Mon Sep 17 00:00:00 2001 From: Hannah Ramadan Date: Wed, 18 Mar 2026 15:54:02 -0700 Subject: [PATCH 05/23] Use actual error code and resonse in tests --- .../patches/dup/instrumentation_test.rb | 21 +++++++------------ .../patches/stable/instrumentation_test.rb | 21 +++++++------------ 2 files changed, 14 insertions(+), 28 deletions(-) diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb index 80aa437fe9..6124653786 100644 --- a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb @@ -224,27 +224,20 @@ end it 'sets error.type to the exception class name' do - error = nil - begin + expect do client.query('SELECT INVALID') - rescue Trilogy::Error => e - error = e - end + end.must_raise Trilogy::Error - _(error).wont_be_nil - _(span.attributes['error.type']).must_equal error.class.name + _(span.attributes['error.type']).must_equal 'Trilogy::ProtocolError' end it 'sets db.response.status_code when error has error_code' do - error = nil - begin + expect do client.query('SELECT INVALID') - rescue Trilogy::Error => e - error = e - end + end.must_raise Trilogy::Error - _(error).wont_be_nil - _(span.attributes['db.response.status_code']).must_equal error.error_code.to_s if error.error_code + # 1054 is MySQL's "Unknown column" error code + _(span.attributes['db.response.status_code']).must_equal '1054' end describe 'when record_exception is true' do diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb index 995ad7f775..df0dbbbd2d 100644 --- a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb @@ -188,27 +188,20 @@ end it 'sets error.type to the exception class name' do - error = nil - begin + expect do client.query('SELECT INVALID') - rescue Trilogy::Error => e - error = e - end + end.must_raise Trilogy::Error - _(error).wont_be_nil - _(span.attributes['error.type']).must_equal error.class.name + _(span.attributes['error.type']).must_equal 'Trilogy::ProtocolError' end it 'sets db.response.status_code when error has error_code' do - error = nil - begin + expect do client.query('SELECT INVALID') - rescue Trilogy::Error => e - error = e - end + end.must_raise Trilogy::Error - _(error).wont_be_nil - _(span.attributes['db.response.status_code']).must_equal error.error_code.to_s if error.error_code + # 1054 is MySQL's "Unknown column" error code + _(span.attributes['db.response.status_code']).must_equal '1054' end describe 'when record_exception is true' do From f3da94cf8ab259cfa0882b919c92cc976c9cd5aa Mon Sep 17 00:00:00 2001 From: Hannah Ramadan Date: Wed, 18 Mar 2026 16:15:49 -0700 Subject: [PATCH 06/23] Match old tests more directly --- .../patches/dup/client_attributes_test.rb | 32 +- .../patches/dup/instrumentation_test.rb | 507 ++++++++++++++++-- .../patches/stable/client_attributes_test.rb | 21 +- .../patches/stable/instrumentation_test.rb | 456 ++++++++++++++-- 4 files changed, 896 insertions(+), 120 deletions(-) diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/client_attributes_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/client_attributes_test.rb index 99f63cc556..0f6b28123d 100644 --- a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/client_attributes_test.rb +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/client_attributes_test.rb @@ -56,52 +56,50 @@ def build_test_client(options) end describe '#client_attributes' do + # Old attributes it 'includes db.system (old) as mysql' do attrs = client.send(:client_attributes) assert_equal 'mysql', attrs[OpenTelemetry::SemanticConventions::Trace::DB_SYSTEM] end - it 'includes db.system.name (stable) as mysql' do - attrs = client.send(:client_attributes) - assert_equal 'mysql', attrs['db.system.name'] - end - it 'includes net.peer.name (old) from host option' do attrs = client.send(:client_attributes) assert_equal 'db-primary.example.com', attrs[OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME] end - it 'includes server.address (stable) from host option' do + it 'includes db.name (old) from database option' do attrs = client.send(:client_attributes) - assert_equal 'db-primary.example.com', attrs['server.address'] + assert_equal 'myapp_production', attrs[OpenTelemetry::SemanticConventions::Trace::DB_NAME] end - it 'includes db.name (old) from database option' do + it 'includes db.user (old) from username option' do attrs = client.send(:client_attributes) - assert_equal 'myapp_production', attrs[OpenTelemetry::SemanticConventions::Trace::DB_NAME] + assert_equal 'app_user', attrs[OpenTelemetry::SemanticConventions::Trace::DB_USER] end - it 'includes db.namespace (stable) from database option' do + # Stable attributes + it 'includes db.system.name (stable) as mysql' do attrs = client.send(:client_attributes) - assert_equal 'myapp_production', attrs['db.namespace'] + assert_equal 'mysql', attrs['db.system.name'] end - it 'includes db.user (old) from username option' do + it 'includes server.address (stable) from host option' do attrs = client.send(:client_attributes) - assert_equal 'app_user', attrs[OpenTelemetry::SemanticConventions::Trace::DB_USER] + assert_equal 'db-primary.example.com', attrs['server.address'] end - it 'includes server.port (stable) when present' do + it 'includes server.port (stable) from port option' do attrs = client.send(:client_attributes) assert_equal 3307, attrs['server.port'] end - it 'does not include net.peer.port (was not in old)' do + it 'includes db.namespace (stable) from database option' do attrs = client.send(:client_attributes) - refute attrs.key?(OpenTelemetry::SemanticConventions::Trace::NET_PEER_PORT) + assert_equal 'myapp_production', attrs['db.namespace'] end - it 'falls back to unknown sock when host is nil' do + # Fallbacks + it 'falls back to unknown sock when host is nil for both attributes' do c = build_test_client({ database: 'test' }) attrs = c.send(:client_attributes) assert_equal 'unknown sock', attrs[OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME] diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb index 6124653786..6e043fa9c4 100644 --- a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb @@ -45,6 +45,15 @@ instrumentation.instance_variable_set(:@installed, false) end + it 'has #name' do + _(instrumentation.name).must_equal 'OpenTelemetry::Instrumentation::Trilogy' + end + + it 'has #version' do + _(instrumentation.version).wont_be_nil + _(instrumentation.version).wont_be_empty + end + describe '#install' do it 'accepts peer service name from config' do instrumentation.instance_variable_set(:@installed, false) @@ -63,6 +72,31 @@ end end + describe '#compatible?' do + describe 'when an unsupported version is installed' do + it 'is incompatible' do + stub_const('Trilogy::VERSION', '2.2.0') + _(instrumentation.compatible?).must_equal false + + stub_const('Trilogy::VERSION', '2.3.0.beta') + _(instrumentation.compatible?).must_equal false + + stub_const('Trilogy::VERSION', '3.0.0') + _(instrumentation.compatible?).must_equal false + end + end + + describe 'when supported version is installed' do + it 'is compatible' do + stub_const('Trilogy::VERSION', '2.3.0') + _(instrumentation.compatible?).must_equal true + + stub_const('Trilogy::VERSION', '3.0.0.rc1') + _(instrumentation.compatible?).must_equal true + end + end + end + describe 'tracing' do before do instrumentation.install(config) @@ -112,21 +146,16 @@ _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_NAME]).must_equal(database) _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_USER]).must_equal(username) _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_equal 'SELECT ?' + _(span.attributes['db.instance.id']).must_be_nil # Stable attributes _(span.attributes['db.system.name']).must_equal 'mysql' _(span.attributes['server.address']).must_equal(host) + _(span.attributes['server.port']).must_equal(port) _(span.attributes['db.namespace']).must_equal(database) _(span.attributes['db.query.text']).must_equal 'SELECT ?' end - it 'includes server.port (stable) but not net.peer.port (was not in old)' do - client.query('SELECT 1') - - _(span.attributes['server.port']).must_equal port - _(span.attributes.key?('net.peer.port')).must_equal false - end - it 'extracts statement type' do explain_sql = 'EXPLAIN SELECT 1' client.query(explain_sql) @@ -134,10 +163,13 @@ _(span.name).must_equal 'explain' # Old attributes + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_NAME]).must_equal(database) + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_USER]).must_equal(username) _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_SYSTEM]).must_equal 'mysql' _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_equal 'EXPLAIN SELECT ?' # Stable attributes + _(span.attributes['db.namespace']).must_equal(database) _(span.attributes['db.system.name']).must_equal 'mysql' _(span.attributes['db.query.text']).must_equal 'EXPLAIN SELECT ?' end @@ -150,10 +182,13 @@ _(span.name).must_equal 'mysql' # Old attributes + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_NAME]).must_equal(database) + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_USER]).must_equal(username) _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_SYSTEM]).must_equal 'mysql' _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_equal 'DESELECT ?' # Stable attributes + _(span.attributes['db.namespace']).must_equal(database) _(span.attributes['db.system.name']).must_equal 'mysql' _(span.attributes['db.query.text']).must_equal 'DESELECT ?' end @@ -162,28 +197,29 @@ describe 'when connecting' do let(:span) { exporter.finished_spans.first } - it 'includes both old and stable attributes for connect span' do + it 'spans will include both old and stable database attributes' do _(client.connected_host).wont_be_nil _(span.name).must_equal 'connect' # Old attributes - _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_SYSTEM]).must_equal 'mysql' - _(span.attributes[OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME]).must_equal(host) _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_NAME]).must_equal(database) _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_USER]).must_equal(username) + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_SYSTEM]).must_equal 'mysql' + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME]).must_equal(host) + _(span.attributes['db.instance.id']).must_be_nil # Stable attributes + _(span.attributes['db.namespace']).must_equal(database) _(span.attributes['db.system.name']).must_equal 'mysql' _(span.attributes['server.address']).must_equal(host) - _(span.attributes['db.namespace']).must_equal(database) end end describe 'when pinging' do let(:span) { exporter.finished_spans[2] } - it 'includes both old and stable attributes for ping span' do + it 'spans will include both old and stable database attributes' do _(client.connected_host).wont_be_nil client.ping @@ -191,14 +227,108 @@ _(span.name).must_equal 'ping' # Old attributes + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_NAME]).must_equal(database) + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_USER]).must_equal(username) _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_SYSTEM]).must_equal 'mysql' _(span.attributes[OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME]).must_equal(host) + + # Stable attributes + _(span.attributes['db.namespace']).must_equal(database) + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['server.address']).must_equal(host) + end + end + + describe 'when quering for the connected host' do + it 'spans will include both old and stable attributes' do + _(client.connected_host).wont_be_nil + + _(span.name).must_equal 'select' + + # Old attributes _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_NAME]).must_equal(database) + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_USER]).must_equal(username) + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_SYSTEM]).must_equal 'mysql' + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_equal 'select @@hostname' + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME]).must_equal(host) + _(span.attributes['db.instance.id']).must_be_nil # Stable attributes + _(span.attributes['db.namespace']).must_equal(database) _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.query.text']).must_equal 'select @@hostname' _(span.attributes['server.address']).must_equal(host) + + client.query('SELECT 1') + + last_span = exporter.finished_spans.last + + _(last_span.name).must_equal 'select' + + # Old attributes on last span + _(last_span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_NAME]).must_equal(database) + _(last_span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_USER]).must_equal(username) + _(last_span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_SYSTEM]).must_equal 'mysql' + _(last_span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_equal 'SELECT ?' + _(last_span.attributes[OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME]).must_equal(host) + _(last_span.attributes['db.instance.id']).must_equal client.connected_host + + # Stable attributes on last span + _(last_span.attributes['db.namespace']).must_equal(database) + _(last_span.attributes['db.system.name']).must_equal 'mysql' + _(last_span.attributes['db.query.text']).must_equal 'SELECT ?' + _(last_span.attributes['server.address']).must_equal(host) + end + end + + describe 'when quering using unix domain socket' do + let(:client) do + Trilogy.new( + username: username, + password: password, + ssl: false + ) + end + + it 'spans will include both old and stable attributes' do + skip 'requires setup of a mysql host using uds connections' + _(client.connected_host).wont_be_nil + + _(span.name).must_equal 'select' + + # Old attributes + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_NAME]).must_equal(database) + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_USER]).must_equal(username) + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_SYSTEM]).must_equal 'mysql' + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_equal 'select @@hostname' + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME]).must_match(/sock/) + + # Stable attributes _(span.attributes['db.namespace']).must_equal(database) + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.query.text']).must_equal 'select @@hostname' + _(span.attributes['server.address']).must_match(/sock/) + + client.query('SELECT 1') + + last_span = exporter.finished_spans.last + + _(last_span.name).must_equal 'select' + + # Old attributes + _(last_span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_NAME]).must_equal(database) + _(last_span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_USER]).must_equal(username) + _(last_span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_SYSTEM]).must_equal 'mysql' + _(last_span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_equal 'SELECT ?' + _(last_span.attributes[OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME]).wont_equal(/sock/) + _(last_span.attributes[OpenTelemetry::SemanticConventions::Trace::NET_PEER_NAME]).must_equal client.connected_host + + # Stable attributes + _(last_span.attributes['db.namespace']).must_equal(database) + _(last_span.attributes['db.system.name']).must_equal 'mysql' + _(last_span.attributes['db.query.text']).must_equal 'SELECT ?' + _(last_span.attributes['server.address']).wont_equal(/sock/) + _(last_span.attributes['server.address']).must_equal client.connected_host end end @@ -211,16 +341,23 @@ _(span.name).must_equal 'select' # Old attributes + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_NAME]).must_equal(database) + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_USER]).must_equal(username) _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_SYSTEM]).must_equal 'mysql' _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_equal 'SELECT INVALID' # Stable attributes + _(span.attributes['db.namespace']).must_equal(database) _(span.attributes['db.system.name']).must_equal 'mysql' _(span.attributes['db.query.text']).must_equal 'SELECT INVALID' _(span.status.code).must_equal( OpenTelemetry::Trace::Status::ERROR ) + _(span.events.first.name).must_equal 'exception' + _(span.events.first.attributes['exception.type']).must_match(/Trilogy.*Error/) + _(span.events.first.attributes['exception.message']).wont_be_nil + _(span.events.first.attributes['exception.stacktrace']).wont_be_nil end it 'sets error.type to the exception class name' do @@ -240,26 +377,10 @@ _(span.attributes['db.response.status_code']).must_equal '1054' end - describe 'when record_exception is true' do - let(:config) { { record_exception: true } } - - it 'records the exception' do - expect do - client.query('SELECT INVALID') - end.must_raise Trilogy::Error - - _(span.events).wont_be_nil - _(span.events.first.name).must_equal 'exception' - _(span.events.first.attributes['exception.type']).must_match(/Trilogy.*Error/) - _(span.events.first.attributes['exception.message']).wont_be_nil - _(span.events.first.attributes['exception.stacktrace']).wont_be_nil - end - end - describe 'when record_exception is false' do let(:config) { { record_exception: false } } - it 'does not record the exception' do + it 'does not record exception when record_exception is false' do expect do client.query('SELECT INVALID') end.must_raise Trilogy::Error @@ -299,7 +420,7 @@ _(span.attributes['db.query.text']).must_equal obfuscated_sql end - it 'encodes invalid byte sequences' do + it 'encodes invalid byte sequences for both attributes' do # \255 is off-limits https://en.wikipedia.org/wiki/UTF-8#Codepage_layout sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com\255'" obfuscated_sql = 'SELECT * from users where users.id = ? and users.email = ?' @@ -329,21 +450,6 @@ end end - describe 'when db_statement is set to omit' do - let(:config) { { db_statement: :omit } } - - it 'does not include SQL statement in either attribute' do - sql = 'SELECT * from users where users.id = 1 and users.email = "test@test.com"' - expect do - client.query(sql) - end.must_raise Trilogy::Error - - _(span.name).must_equal 'select' - _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_be_nil - _(span.attributes['db.query.text']).must_be_nil - end - end - describe 'when propagator is set to none' do let(:config) { { propagator: :none } } @@ -369,5 +475,316 @@ _(sql).must_equal original_sql end end + + describe 'when propagator is set to vitess' do + let(:config) { { propagator: 'vitess' } } + + it 'does inject context on frozen strings' do + sql = 'SELECT * from users where users.id = 1 and users.email = "test@test.com"' + assert(sql.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] + assert(args[0].frozen?) + m.call(args[0]) + end + + allow(propagator).to receive(:inject).and_wrap_original do |m, *args| + arg_cache[:inject_input] = args[0] + refute(args[0].frozen?) + assert_match(sql, args[0]) + 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 + encoded = Base64.strict_encode64("{\"uber-trace-id\":\"#{span.hex_trace_id}:#{span.hex_span_id}:0:1\"}") + assert_equal(arg_cache[:inject_input], "/*VT_SPAN_CONTEXT=#{encoded}*/#{sql}") + + # arg_cache[:inject_input] is now frozen + assert(arg_cache[:inject_input].frozen?) + end + + it 'does inject 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"' + refute(sql.frozen?) + + # dup sql for comparison purposes, since propagator mutates it + cached_sql = sql.dup + + expect do + client.query(sql) + end.must_raise Trilogy::Error + + encoded = Base64.strict_encode64("{\"uber-trace-id\":\"#{span.hex_trace_id}:#{span.hex_span_id}:0:1\"}") + assert_equal(sql, "/*VT_SPAN_CONTEXT=#{encoded}*/#{cached_sql}") + refute(sql.frozen?) + 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 } } + + it 'does not include SQL statement in either attribute' do + sql = 'SELECT * from users where users.id = 1 and users.email = "test@test.com"' + expect do + client.query(sql) + end.must_raise Trilogy::Error + + _(span.name).must_equal 'select' + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_be_nil + _(span.attributes['db.query.text']).must_be_nil + end + end + + describe 'when db_statement is configured via environment variable' do + describe 'when db_statement set as omit' do + it 'omits both db.statement and db.query.text attributes' do + OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'db_statement=omit;') do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" + expect do + client.query(sql) + end.must_raise Trilogy::Error + + _(span.attributes['db.system']).must_equal 'mysql' + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.name).must_equal 'select' + _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_be_nil + _(span.attributes['db.query.text']).must_be_nil + end + end + end + + describe 'when db_statement set as obfuscate' do + it 'obfuscates SQL parameters in both attributes' do + OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'db_statement=obfuscate;') do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install + + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" + obfuscated_sql = 'SELECT * from users where users.id = ? and users.email = ?' + expect do + client.query(sql) + end.must_raise Trilogy::Error + + _(span.attributes['db.system']).must_equal 'mysql' + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.name).must_equal 'select' + _(span.attributes['db.statement']).must_equal obfuscated_sql + _(span.attributes['db.query.text']).must_equal obfuscated_sql + end + end + end + + describe 'when db_statement is set differently than local config' do + let(:config) { { db_statement: :omit } } + + it 'overrides local config and obfuscates SQL parameters in both attributes' do + OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'db_statement=obfuscate') do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install + + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" + obfuscated_sql = 'SELECT * from users where users.id = ? and users.email = ?' + expect do + client.query(sql) + end.must_raise Trilogy::Error + + _(span.attributes['db.system']).must_equal 'mysql' + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.name).must_equal 'select' + _(span.attributes['db.statement']).must_equal obfuscated_sql + _(span.attributes['db.query.text']).must_equal obfuscated_sql + end + end + end + end + + describe 'when span_name is set as statement_type' do + it 'sets span name to statement type' do + OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'span_name=statement_type') do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install + + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" + expect do + client.query(sql) + end.must_raise Trilogy::Error + + _(span.name).must_equal 'select' + end + end + + it 'sets span name to mysql when statement type is not recognized' do + OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'span_name=statement_type') do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install + + sql = 'DESELECT 1' + expect do + client.query(sql) + end.must_raise Trilogy::Error + + _(span.name).must_equal 'mysql' + end + end + end + + describe 'when span_name is set as db_name' do + it 'sets span name to db name' do + OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'span_name=db_name') do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install + + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" + expect do + client.query(sql) + end.must_raise Trilogy::Error + + _(span.name).must_equal 'mysql' + end + end + + describe 'when db name is nil' do + let(:database) { nil } + + it 'sets span name to mysql' do + OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'span_name=db_name') do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install + + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" + expect do + client.query(sql) + end.must_raise Trilogy::Error + + _(span.name).must_equal 'mysql' + end + end + end + end + + describe 'when span_name is set as db_operation_and_name' do + it 'sets span name to db operation and name' do + OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'span_name=db_operation_and_name') do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install + + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" + OpenTelemetry::Instrumentation::Trilogy.with_attributes('db.operation' => 'foo') do + expect do + client.query(sql) + end.must_raise Trilogy::Error + end + + _(span.name).must_equal 'foo mysql' + end + end + + it 'sets span name to db name when db.operation is not set' do + OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'span_name=db_operation_and_name') do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install + + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" + expect do + client.query(sql) + end.must_raise Trilogy::Error + + _(span.name).must_equal 'mysql' + end + end + + describe 'when db name is nil' do + let(:database) { nil } + + it 'sets span name to db operation' do + OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'span_name=db_operation_and_name') do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install + + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" + OpenTelemetry::Instrumentation::Trilogy.with_attributes('db.operation' => 'foo') do + expect do + client.query(sql) + end.must_raise Trilogy::Error + end + + _(span.name).must_equal 'foo' + end + end + + it 'sets span name to mysql when db.operation is not set' do + OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'span_name=db_operation_and_name') do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install + + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" + expect do + client.query(sql) + end.must_raise Trilogy::Error + + _(span.name).must_equal 'mysql' + end + end + end + end end end diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/client_attributes_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/client_attributes_test.rb index d5d76243ec..245b49d5d2 100644 --- a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/client_attributes_test.rb +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/client_attributes_test.rb @@ -66,7 +66,7 @@ def build_test_client(options) assert_equal 'db-primary.example.com', attrs['server.address'] end - it 'includes server.port when present' do + it 'includes server.port from port option' do attrs = client.send(:client_attributes) assert_equal 3307, attrs['server.port'] end @@ -136,9 +136,14 @@ def build_test_client(options) attrs = client.send(:client_attributes) refute attrs.key?(OpenTelemetry::SemanticConventions::Trace::DB_NAME) end + end - it 'does not include db.statement when db_statement is :include' do + describe 'with sql and db_statement config' do + before do instrumentation.instance_variable_set(:@installed, false) + end + + it 'includes SQL as db.query.text when db_statement is :include' do instrumentation.install({ db_statement: :include, span_name: :statement_type, @@ -148,16 +153,10 @@ def build_test_client(options) peer_service: nil }) attrs = client.send(:client_attributes, 'SELECT * FROM users') - refute attrs.key?(OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT) - end - end - - describe 'with sql and db_statement config' do - before do - instrumentation.instance_variable_set(:@installed, false) + assert_equal 'SELECT * FROM users', attrs['db.query.text'] end - it 'includes SQL as db.query.text when db_statement is :include' do + it 'does not include db.statement when db_statement is :include' do instrumentation.install({ db_statement: :include, span_name: :statement_type, @@ -167,7 +166,7 @@ def build_test_client(options) peer_service: nil }) attrs = client.send(:client_attributes, 'SELECT * FROM users') - assert_equal 'SELECT * FROM users', attrs['db.query.text'] + refute attrs.key?(OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT) end it 'omits db.query.text when db_statement is :omit' do diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb index df0dbbbd2d..3bdc5518ad 100644 --- a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb @@ -45,6 +45,15 @@ instrumentation.instance_variable_set(:@installed, false) end + it 'has #name' do + _(instrumentation.name).must_equal 'OpenTelemetry::Instrumentation::Trilogy' + end + + it 'has #version' do + _(instrumentation.version).wont_be_nil + _(instrumentation.version).wont_be_empty + end + describe '#install' do it 'accepts peer service name from config' do instrumentation.instance_variable_set(:@installed, false) @@ -63,6 +72,31 @@ end end + describe '#compatible?' do + describe 'when an unsupported version is installed' do + it 'is incompatible' do + stub_const('Trilogy::VERSION', '2.2.0') + _(instrumentation.compatible?).must_equal false + + stub_const('Trilogy::VERSION', '2.3.0.beta') + _(instrumentation.compatible?).must_equal false + + stub_const('Trilogy::VERSION', '3.0.0') + _(instrumentation.compatible?).must_equal false + end + end + + describe 'when supported version is installed' do + it 'is compatible' do + stub_const('Trilogy::VERSION', '2.3.0') + _(instrumentation.compatible?).must_equal true + + stub_const('Trilogy::VERSION', '3.0.0.rc1') + _(instrumentation.compatible?).must_equal true + end + end + end + describe 'tracing' do before do instrumentation.install(config) @@ -91,21 +125,22 @@ end describe 'with default options' do - it 'obfuscates sql in db.query.text' do + it 'obfuscates sql' do client.query('SELECT 1') _(span.name).must_equal 'select' _(span.attributes['db.query.text']).must_equal 'SELECT ?' end - it 'includes stable database connection information' do + it 'includes database connection information' do client.query('SELECT 1') _(span.name).must_equal 'select' - _(span.attributes['db.system.name']).must_equal 'mysql' - _(span.attributes['server.address']).must_equal(host) _(span.attributes['db.namespace']).must_equal(database) + _(span.attributes['db.system.name']).must_equal 'mysql' _(span.attributes['db.query.text']).must_equal 'SELECT ?' + _(span.attributes['server.address']).must_equal(host) + _(span.attributes['server.port']).must_equal(port) end it 'does not include old attribute names' do @@ -118,17 +153,12 @@ _(span.attributes.key?('db.user')).must_equal false end - it 'includes server.port when present' do - client.query('SELECT 1') - - _(span.attributes['server.port']).must_equal port - end - it 'extracts statement type' do explain_sql = 'EXPLAIN SELECT 1' client.query(explain_sql) _(span.name).must_equal 'explain' + _(span.attributes['db.namespace']).must_equal(database) _(span.attributes['db.system.name']).must_equal 'mysql' _(span.attributes['db.query.text']).must_equal 'EXPLAIN SELECT ?' end @@ -139,6 +169,7 @@ end.must_raise Trilogy::Error _(span.name).must_equal 'mysql' + _(span.attributes['db.namespace']).must_equal(database) _(span.attributes['db.system.name']).must_equal 'mysql' _(span.attributes['db.query.text']).must_equal 'DESELECT ?' end @@ -147,28 +178,82 @@ describe 'when connecting' do let(:span) { exporter.finished_spans.first } - it 'uses stable attributes for connect span' do + it 'spans will include database name' do _(client.connected_host).wont_be_nil _(span.name).must_equal 'connect' + _(span.attributes['db.namespace']).must_equal(database) _(span.attributes['db.system.name']).must_equal 'mysql' _(span.attributes['server.address']).must_equal(host) - _(span.attributes['db.namespace']).must_equal(database) end end describe 'when pinging' do let(:span) { exporter.finished_spans[2] } - it 'uses stable attributes for ping span' do + it 'spans will include database name' do _(client.connected_host).wont_be_nil client.ping _(span.name).must_equal 'ping' + _(span.attributes['db.namespace']).must_equal(database) _(span.attributes['db.system.name']).must_equal 'mysql' _(span.attributes['server.address']).must_equal(host) + end + end + + describe 'when quering for the connected host' do + it 'spans will include the server.address attribute' do + _(client.connected_host).wont_be_nil + + _(span.name).must_equal 'select' + _(span.attributes['db.namespace']).must_equal(database) + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.query.text']).must_equal 'select @@hostname' + _(span.attributes['server.address']).must_equal(host) + + client.query('SELECT 1') + + last_span = exporter.finished_spans.last + + _(last_span.name).must_equal 'select' + _(last_span.attributes['db.namespace']).must_equal(database) + _(last_span.attributes['db.system.name']).must_equal 'mysql' + _(last_span.attributes['db.query.text']).must_equal 'SELECT ?' + _(last_span.attributes['server.address']).must_equal(host) + end + end + + describe 'when quering using unix domain socket' do + let(:client) do + Trilogy.new( + username: username, + password: password, + ssl: false + ) + end + + it 'spans will include the server.address attribute' do + skip 'requires setup of a mysql host using uds connections' + _(client.connected_host).wont_be_nil + + _(span.name).must_equal 'select' _(span.attributes['db.namespace']).must_equal(database) + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.query.text']).must_equal 'select @@hostname' + _(span.attributes['server.address']).must_match(/sock/) + + client.query('SELECT 1') + + last_span = exporter.finished_spans.last + + _(last_span.name).must_equal 'select' + _(last_span.attributes['db.namespace']).must_equal(database) + _(last_span.attributes['db.system.name']).must_equal 'mysql' + _(last_span.attributes['db.query.text']).must_equal 'SELECT ?' + _(last_span.attributes['server.address']).wont_equal(/sock/) + _(last_span.attributes['server.address']).must_equal client.connected_host end end @@ -179,12 +264,17 @@ end.must_raise Trilogy::Error _(span.name).must_equal 'select' + _(span.attributes['db.namespace']).must_equal(database) _(span.attributes['db.system.name']).must_equal 'mysql' _(span.attributes['db.query.text']).must_equal 'SELECT INVALID' _(span.status.code).must_equal( OpenTelemetry::Trace::Status::ERROR ) + _(span.events.first.name).must_equal 'exception' + _(span.events.first.attributes['exception.type']).must_match(/Trilogy.*Error/) + _(span.events.first.attributes['exception.message']).wont_be_nil + _(span.events.first.attributes['exception.stacktrace']).wont_be_nil end it 'sets error.type to the exception class name' do @@ -204,26 +294,10 @@ _(span.attributes['db.response.status_code']).must_equal '1054' end - describe 'when record_exception is true' do - let(:config) { { record_exception: true } } - - it 'records the exception' do - expect do - client.query('SELECT INVALID') - end.must_raise Trilogy::Error - - _(span.events).wont_be_nil - _(span.events.first.name).must_equal 'exception' - _(span.events.first.attributes['exception.type']).must_match(/Trilogy.*Error/) - _(span.events.first.attributes['exception.message']).wont_be_nil - _(span.events.first.attributes['exception.stacktrace']).wont_be_nil - end - end - describe 'when record_exception is false' do let(:config) { { record_exception: false } } - it 'does not record the exception' do + it 'does not record exception when record_exception is false' do expect do client.query('SELECT INVALID') end.must_raise Trilogy::Error @@ -236,7 +310,7 @@ describe 'when db_statement is set to include' do let(:config) { { db_statement: :include } } - it 'includes the query in db.query.text' do + it 'includes the db query statement' do sql = 'SELECT * from users where users.id = 1 and users.email = "test@test.com"' expect do client.query(sql) @@ -244,7 +318,6 @@ _(span.name).must_equal 'select' _(span.attributes['db.query.text']).must_equal sql - _(span.attributes.key?('db.statement')).must_equal false end end @@ -260,7 +333,6 @@ _(span.name).must_equal 'select' _(span.attributes['db.query.text']).must_equal obfuscated_sql - _(span.attributes.key?('db.statement')).must_equal false end it 'encodes invalid byte sequences for db.query.text' do @@ -291,20 +363,6 @@ end end - describe 'when db_statement is set to omit' do - let(:config) { { db_statement: :omit } } - - it 'does not include SQL statement as db.query.text attribute' do - sql = 'SELECT * from users where users.id = 1 and users.email = "test@test.com"' - expect do - client.query(sql) - end.must_raise Trilogy::Error - - _(span.name).must_equal 'select' - _(span.attributes['db.query.text']).must_be_nil - end - end - describe 'when propagator is set to none' do let(:config) { { propagator: :none } } @@ -330,5 +388,309 @@ _(sql).must_equal original_sql end end + + describe 'when propagator is set to vitess' do + let(:config) { { propagator: 'vitess' } } + + it 'does inject context on frozen strings' do + sql = 'SELECT * from users where users.id = 1 and users.email = "test@test.com"' + assert(sql.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] + assert(args[0].frozen?) + m.call(args[0]) + end + + allow(propagator).to receive(:inject).and_wrap_original do |m, *args| + arg_cache[:inject_input] = args[0] + refute(args[0].frozen?) + assert_match(sql, args[0]) + 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 + encoded = Base64.strict_encode64("{\"uber-trace-id\":\"#{span.hex_trace_id}:#{span.hex_span_id}:0:1\"}") + assert_equal(arg_cache[:inject_input], "/*VT_SPAN_CONTEXT=#{encoded}*/#{sql}") + + # arg_cache[:inject_input] is now frozen + assert(arg_cache[:inject_input].frozen?) + end + + it 'does inject 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"' + refute(sql.frozen?) + + # dup sql for comparison purposes, since propagator mutates it + cached_sql = sql.dup + + expect do + client.query(sql) + end.must_raise Trilogy::Error + + encoded = Base64.strict_encode64("{\"uber-trace-id\":\"#{span.hex_trace_id}:#{span.hex_span_id}:0:1\"}") + assert_equal(sql, "/*VT_SPAN_CONTEXT=#{encoded}*/#{cached_sql}") + refute(sql.frozen?) + 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 } } + + it 'does not include SQL statement as db.query.text attribute' do + sql = 'SELECT * from users where users.id = 1 and users.email = "test@test.com"' + expect do + client.query(sql) + end.must_raise Trilogy::Error + + _(span.name).must_equal 'select' + _(span.attributes['db.query.text']).must_be_nil + end + end + + describe 'when db_statement is configured via environment variable' do + describe 'when db_statement set as omit' do + it 'omits db.query.text attribute' do + OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'db_statement=omit;') do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" + expect do + client.query(sql) + end.must_raise Trilogy::Error + + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.name).must_equal 'select' + _(span.attributes['db.query.text']).must_be_nil + end + end + end + + describe 'when db_statement set as obfuscate' do + it 'obfuscates SQL parameters in db.query.text' do + OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'db_statement=obfuscate;') do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install + + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" + obfuscated_sql = 'SELECT * from users where users.id = ? and users.email = ?' + expect do + client.query(sql) + end.must_raise Trilogy::Error + + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.name).must_equal 'select' + _(span.attributes['db.query.text']).must_equal obfuscated_sql + end + end + end + + describe 'when db_statement is set differently than local config' do + let(:config) { { db_statement: :omit } } + + it 'overrides local config and obfuscates SQL parameters in db.query.text' do + OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'db_statement=obfuscate') do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install + + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" + obfuscated_sql = 'SELECT * from users where users.id = ? and users.email = ?' + expect do + client.query(sql) + end.must_raise Trilogy::Error + + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.name).must_equal 'select' + _(span.attributes['db.query.text']).must_equal obfuscated_sql + end + end + end + end + + describe 'when span_name is set as statement_type' do + it 'sets span name to statement type' do + OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'span_name=statement_type') do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install + + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" + expect do + client.query(sql) + end.must_raise Trilogy::Error + + _(span.name).must_equal 'select' + end + end + + it 'sets span name to mysql when statement type is not recognized' do + OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'span_name=statement_type') do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install + + sql = 'DESELECT 1' + expect do + client.query(sql) + end.must_raise Trilogy::Error + + _(span.name).must_equal 'mysql' + end + end + end + + describe 'when span_name is set as db_name' do + it 'sets span name to db name' do + OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'span_name=db_name') do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install + + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" + expect do + client.query(sql) + end.must_raise Trilogy::Error + + _(span.name).must_equal 'mysql' + end + end + + describe 'when db name is nil' do + let(:database) { nil } + + it 'sets span name to mysql' do + OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'span_name=db_name') do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install + + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" + expect do + client.query(sql) + end.must_raise Trilogy::Error + + _(span.name).must_equal 'mysql' + end + end + end + end + + describe 'when span_name is set as db_operation_and_name' do + it 'sets span name to db operation and name' do + OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'span_name=db_operation_and_name') do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install + + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" + OpenTelemetry::Instrumentation::Trilogy.with_attributes('db.operation.name' => 'foo') do + expect do + client.query(sql) + end.must_raise Trilogy::Error + end + + _(span.name).must_equal 'foo mysql' + end + end + + it 'sets span name to db name when db.operation is not set' do + OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'span_name=db_operation_and_name') do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install + + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" + expect do + client.query(sql) + end.must_raise Trilogy::Error + + _(span.name).must_equal 'mysql' + end + end + + describe 'when db name is nil' do + let(:database) { nil } + + it 'sets span name to db operation' do + OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'span_name=db_operation_and_name') do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install + + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" + OpenTelemetry::Instrumentation::Trilogy.with_attributes('db.operation.name' => 'foo') do + expect do + client.query(sql) + end.must_raise Trilogy::Error + end + + _(span.name).must_equal 'foo' + end + end + + it 'sets span name to mysql when db.operation is not set' do + OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'span_name=db_operation_and_name') do + instrumentation.instance_variable_set(:@installed, false) + instrumentation.install + + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" + expect do + client.query(sql) + end.must_raise Trilogy::Error + + _(span.name).must_equal 'mysql' + end + end + end + end end end From 208eaa93a29b162b958363c60d55d50ea6b11ec2 Mon Sep 17 00:00:00 2001 From: Hannah Ramadan Date: Thu, 19 Mar 2026 11:19:13 -0700 Subject: [PATCH 07/23] Rescue all errors --- .../instrumentation/trilogy/patches/dup/client.rb | 6 +++--- .../instrumentation/trilogy/patches/stable/client.rb | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/dup/client.rb b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/dup/client.rb index df19f3816a..3e32721423 100644 --- a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/dup/client.rb +++ b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/dup/client.rb @@ -26,7 +26,7 @@ def initialize(options = {}) record_exception: config[:record_exception] ) do |span| super - rescue StandardError => e + rescue => e set_error_attributes(span, e) raise end @@ -40,7 +40,7 @@ def ping(...) record_exception: config[:record_exception] ) do |span| super - rescue StandardError => e + rescue => e set_error_attributes(span, e) raise end @@ -69,7 +69,7 @@ def query(sql) end super - rescue StandardError => e + rescue => e set_error_attributes(span, e) raise end diff --git a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb index 611caa6539..d666b13788 100644 --- a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb +++ b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb @@ -26,7 +26,7 @@ def initialize(options = {}) record_exception: config[:record_exception] ) do |span| super - rescue StandardError => e + rescue => e set_error_attributes(span, e) raise end @@ -40,7 +40,7 @@ def ping(...) record_exception: config[:record_exception] ) do |span| super - rescue StandardError => e + rescue => e set_error_attributes(span, e) raise end @@ -69,7 +69,7 @@ def query(sql) end super - rescue StandardError => e + rescue => e set_error_attributes(span, e) raise end From 9dab994ee8e8920d5324b6e4b1016acd7117dfca Mon Sep 17 00:00:00 2001 From: Hannah Ramadan Date: Thu, 19 Mar 2026 11:21:56 -0700 Subject: [PATCH 08/23] Rubocop: Avoid rescuing all errors --- .../instrumentation/trilogy/patches/dup/client.rb | 6 +++--- .../instrumentation/trilogy/patches/stable/client.rb | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/dup/client.rb b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/dup/client.rb index 3e32721423..df19f3816a 100644 --- a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/dup/client.rb +++ b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/dup/client.rb @@ -26,7 +26,7 @@ def initialize(options = {}) record_exception: config[:record_exception] ) do |span| super - rescue => e + rescue StandardError => e set_error_attributes(span, e) raise end @@ -40,7 +40,7 @@ def ping(...) record_exception: config[:record_exception] ) do |span| super - rescue => e + rescue StandardError => e set_error_attributes(span, e) raise end @@ -69,7 +69,7 @@ def query(sql) end super - rescue => e + rescue StandardError => e set_error_attributes(span, e) raise end diff --git a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb index d666b13788..611caa6539 100644 --- a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb +++ b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb @@ -26,7 +26,7 @@ def initialize(options = {}) record_exception: config[:record_exception] ) do |span| super - rescue => e + rescue StandardError => e set_error_attributes(span, e) raise end @@ -40,7 +40,7 @@ def ping(...) record_exception: config[:record_exception] ) do |span| super - rescue => e + rescue StandardError => e set_error_attributes(span, e) raise end @@ -69,7 +69,7 @@ def query(sql) end super - rescue => e + rescue StandardError => e set_error_attributes(span, e) raise end From c8e7951fc34960b1cd4d5a81d328ba51946c5be1 Mon Sep 17 00:00:00 2001 From: Hannah Ramadan Date: Thu, 19 Mar 2026 13:50:55 -0700 Subject: [PATCH 09/23] Update span name --- .../mysql/lib/opentelemetry/helpers/mysql.rb | 28 +++- helpers/mysql/test/helpers/mysql_test.rb | 45 +++++ .../trilogy/patches/dup/client.rb | 6 +- .../trilogy/patches/stable/client.rb | 6 +- .../patches/dup/instrumentation_test.rb | 151 +++++------------ .../patches/stable/instrumentation_test.rb | 155 +++++------------- 6 files changed, 152 insertions(+), 239 deletions(-) diff --git a/helpers/mysql/lib/opentelemetry/helpers/mysql.rb b/helpers/mysql/lib/opentelemetry/helpers/mysql.rb index 6e1b204f5c..07b8be81c6 100644 --- a/helpers/mysql/lib/opentelemetry/helpers/mysql.rb +++ b/helpers/mysql/lib/opentelemetry/helpers/mysql.rb @@ -2,7 +2,8 @@ # Copyright The OpenTelemetry Authors # -# SPDX-License-Identifier: Apache-2.0module OpenTelemetry +# SPDX-License-Identifier: Apache-2.0 + require 'opentelemetry-common' module OpenTelemetry @@ -66,6 +67,31 @@ def database_span_name(sql, operation, database_name, config) end || 'mysql' end + # Span naming following stable database semantic conventions. + # Per spec: {db.query.summary} -> {db.operation.name} {target} -> {target} -> {db.system.name} + # We don't have db.query.summary, so we use: + # {db.operation.name} {db.namespace} -> {db.namespace} -> mysql + # + # Note: Per spec, db.operation.name SHOULD NOT be extracted from db.query.text. + # The operation should only be used if explicitly provided by the application + # (e.g., via with_attributes). + # + # @param operation [String] The database operation (db.operation.name), if provided by the application. + # @param database_name [String] The name of the database (db.namespace). + # @return [String] The span name. + # @api private + def stable_database_span_name(operation, database_name) + if operation && database_name + "#{operation} #{database_name}" + elsif database_name + database_name + elsif operation + operation + else + 'mysql' + end + end + # @api private def extract_statement_type(sql) return unless sql diff --git a/helpers/mysql/test/helpers/mysql_test.rb b/helpers/mysql/test/helpers/mysql_test.rb index e301182c49..7352366e1e 100644 --- a/helpers/mysql/test/helpers/mysql_test.rb +++ b/helpers/mysql/test/helpers/mysql_test.rb @@ -55,6 +55,51 @@ end end + describe '.stable_database_span_name' do + let(:operation) { 'SELECT' } + let(:database_name) { 'mydb' } + let(:stable_span_name) { OpenTelemetry::Helpers::MySQL.stable_database_span_name(operation, database_name) } + + describe 'when operation and database_name are present' do + it 'returns "{operation} {database_name}"' do + assert_equal('SELECT mydb', stable_span_name) + end + end + + describe 'when only database_name is present' do + let(:operation) { nil } + + it 'returns database_name' do + assert_equal('mydb', stable_span_name) + end + end + + describe 'when only operation is present' do + let(:database_name) { nil } + + it 'returns operation' do + assert_equal('SELECT', stable_span_name) + end + end + + describe 'when both operation and database_name are nil' do + let(:operation) { nil } + let(:database_name) { nil } + + it 'returns mysql as fallback' do + assert_equal('mysql', stable_span_name) + end + end + + describe 'preserves operation case as provided' do + it 'does not normalize case' do + assert_equal('select mydb', OpenTelemetry::Helpers::MySQL.stable_database_span_name('select', 'mydb')) + assert_equal('SELECT mydb', OpenTelemetry::Helpers::MySQL.stable_database_span_name('SELECT', 'mydb')) + assert_equal('Select mydb', OpenTelemetry::Helpers::MySQL.stable_database_span_name('Select', 'mydb')) + end + end + end + describe '.db_operation_and_name' do let(:operation) { 'operation' } let(:database_name) { 'database_name' } diff --git a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/dup/client.rb b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/dup/client.rb index df19f3816a..df11d9708f 100644 --- a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/dup/client.rb +++ b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/dup/client.rb @@ -50,11 +50,9 @@ def query(sql) context_attributes = OpenTelemetry::Instrumentation::Trilogy.attributes tracer.in_span( - OpenTelemetry::Helpers::MySQL.database_span_name( - sql, + OpenTelemetry::Helpers::MySQL.stable_database_span_name( context_attributes[OpenTelemetry::SemanticConventions::Trace::DB_OPERATION] || context_attributes['db.operation.name'], - @_otel_database_name, - config + @_otel_database_name ), attributes: client_attributes(sql).merge!(context_attributes), kind: :client, diff --git a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb index 611caa6539..eb08427042 100644 --- a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb +++ b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb @@ -50,11 +50,9 @@ def query(sql) context_attributes = OpenTelemetry::Instrumentation::Trilogy.attributes tracer.in_span( - OpenTelemetry::Helpers::MySQL.database_span_name( - sql, + OpenTelemetry::Helpers::MySQL.stable_database_span_name( context_attributes['db.operation.name'], - @_otel_database_name, - config + @_otel_database_name ), attributes: client_attributes(sql).merge!(context_attributes), kind: :client, diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb index 6e043fa9c4..c62b7ac2f4 100644 --- a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb @@ -128,7 +128,8 @@ it 'obfuscates sql in both old and stable attributes' do client.query('SELECT 1') - _(span.name).must_equal 'select' + # Per stable semconv spec, span name uses db.namespace (not extracted from SQL) + _(span.name).must_equal database # Old attribute _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_equal 'SELECT ?' # Stable attribute @@ -138,7 +139,7 @@ it 'includes both old and stable database connection information' do client.query('SELECT 1') - _(span.name).must_equal 'select' + _(span.name).must_equal database # Old attributes _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_SYSTEM]).must_equal 'mysql' @@ -156,11 +157,12 @@ _(span.attributes['db.query.text']).must_equal 'SELECT ?' end - it 'extracts statement type' do + it 'uses db.namespace as span name per stable semconv spec' do explain_sql = 'EXPLAIN SELECT 1' client.query(explain_sql) - _(span.name).must_equal 'explain' + # Per stable semconv spec, span name is NOT extracted from SQL + _(span.name).must_equal database # Old attributes _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_NAME]).must_equal(database) @@ -243,7 +245,7 @@ it 'spans will include both old and stable attributes' do _(client.connected_host).wont_be_nil - _(span.name).must_equal 'select' + _(span.name).must_equal database # Old attributes _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_NAME]).must_equal(database) @@ -263,7 +265,7 @@ last_span = exporter.finished_spans.last - _(last_span.name).must_equal 'select' + _(last_span.name).must_equal database # Old attributes on last span _(last_span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_NAME]).must_equal(database) @@ -294,7 +296,7 @@ skip 'requires setup of a mysql host using uds connections' _(client.connected_host).wont_be_nil - _(span.name).must_equal 'select' + _(span.name).must_equal database # Old attributes _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_NAME]).must_equal(database) @@ -313,7 +315,7 @@ last_span = exporter.finished_spans.last - _(last_span.name).must_equal 'select' + _(last_span.name).must_equal database # Old attributes _(last_span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_NAME]).must_equal(database) @@ -338,7 +340,7 @@ client.query('SELECT INVALID') end.must_raise Trilogy::Error - _(span.name).must_equal 'select' + _(span.name).must_equal database # Old attributes _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_NAME]).must_equal(database) @@ -399,7 +401,7 @@ client.query(sql) end.must_raise Trilogy::Error - _(span.name).must_equal 'select' + _(span.name).must_equal database _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_equal sql _(span.attributes['db.query.text']).must_equal sql end @@ -415,7 +417,7 @@ client.query(sql) end.must_raise Trilogy::Error - _(span.name).must_equal 'select' + _(span.name).must_equal database _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_equal obfuscated_sql _(span.attributes['db.query.text']).must_equal obfuscated_sql end @@ -429,7 +431,7 @@ client.query(sql) end.must_raise Trilogy::Error - _(span.name).must_equal 'select' + _(span.name).must_equal database _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_equal obfuscated_sql _(span.attributes['db.query.text']).must_equal obfuscated_sql end @@ -586,7 +588,7 @@ client.query(sql) end.must_raise Trilogy::Error - _(span.name).must_equal 'select' + _(span.name).must_equal database _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_be_nil _(span.attributes['db.query.text']).must_be_nil end @@ -605,7 +607,7 @@ _(span.attributes['db.system']).must_equal 'mysql' _(span.attributes['db.system.name']).must_equal 'mysql' - _(span.name).must_equal 'select' + _(span.name).must_equal database _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_be_nil _(span.attributes['db.query.text']).must_be_nil end @@ -626,7 +628,7 @@ _(span.attributes['db.system']).must_equal 'mysql' _(span.attributes['db.system.name']).must_equal 'mysql' - _(span.name).must_equal 'select' + _(span.name).must_equal database _(span.attributes['db.statement']).must_equal obfuscated_sql _(span.attributes['db.query.text']).must_equal obfuscated_sql end @@ -649,7 +651,7 @@ _(span.attributes['db.system']).must_equal 'mysql' _(span.attributes['db.system.name']).must_equal 'mysql' - _(span.name).must_equal 'select' + _(span.name).must_equal database _(span.attributes['db.statement']).must_equal obfuscated_sql _(span.attributes['db.query.text']).must_equal obfuscated_sql end @@ -657,92 +659,47 @@ end end - describe 'when span_name is set as statement_type' do - it 'sets span name to statement type' do - OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'span_name=statement_type') do - instrumentation.instance_variable_set(:@installed, false) - instrumentation.install + # In dup semconv mode, span naming follows the stable spec regardless of span_name config: + # {db.operation.name} {db.namespace} -> {db.namespace} -> mysql + # The span_name config option is ignored for dup semconv (uses stable naming). - sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" - expect do - client.query(sql) - end.must_raise Trilogy::Error + describe 'span naming follows stable semconv spec' do + it 'uses db.namespace as span name by default' do + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" + expect do + client.query(sql) + end.must_raise Trilogy::Error - _(span.name).must_equal 'select' - end + # span_name config is ignored in dup semconv + _(span.name).must_equal database end - it 'sets span name to mysql when statement type is not recognized' do - OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'span_name=statement_type') do - instrumentation.instance_variable_set(:@installed, false) - instrumentation.install - - sql = 'DESELECT 1' + it 'uses db.operation.name and db.namespace when operation is provided via with_attributes' do + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" + OpenTelemetry::Instrumentation::Trilogy.with_attributes('db.operation' => 'SELECT') do expect do client.query(sql) end.must_raise Trilogy::Error - - _(span.name).must_equal 'mysql' end - end - end - - describe 'when span_name is set as db_name' do - it 'sets span name to db name' do - OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'span_name=db_name') do - instrumentation.instance_variable_set(:@installed, false) - instrumentation.install - - sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" - expect do - client.query(sql) - end.must_raise Trilogy::Error - _(span.name).must_equal 'mysql' - end + _(span.name).must_equal "SELECT #{database}" end describe 'when db name is nil' do let(:database) { nil } - it 'sets span name to mysql' do - OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'span_name=db_name') do - instrumentation.instance_variable_set(:@installed, false) - instrumentation.install - - sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" - expect do - client.query(sql) - end.must_raise Trilogy::Error - - _(span.name).must_equal 'mysql' - end - end - end - end - - describe 'when span_name is set as db_operation_and_name' do - it 'sets span name to db operation and name' do - OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'span_name=db_operation_and_name') do - instrumentation.instance_variable_set(:@installed, false) - instrumentation.install - + it 'uses db.operation.name when provided via with_attributes' do sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" - OpenTelemetry::Instrumentation::Trilogy.with_attributes('db.operation' => 'foo') do + OpenTelemetry::Instrumentation::Trilogy.with_attributes('db.operation' => 'SELECT') do expect do client.query(sql) end.must_raise Trilogy::Error end - _(span.name).must_equal 'foo mysql' + _(span.name).must_equal 'SELECT' end - end - - it 'sets span name to db name when db.operation is not set' do - OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'span_name=db_operation_and_name') do - instrumentation.instance_variable_set(:@installed, false) - instrumentation.install + it 'falls back to mysql when no operation or db name' do sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" expect do client.query(sql) @@ -751,40 +708,6 @@ _(span.name).must_equal 'mysql' end end - - describe 'when db name is nil' do - let(:database) { nil } - - it 'sets span name to db operation' do - OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'span_name=db_operation_and_name') do - instrumentation.instance_variable_set(:@installed, false) - instrumentation.install - - sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" - OpenTelemetry::Instrumentation::Trilogy.with_attributes('db.operation' => 'foo') do - expect do - client.query(sql) - end.must_raise Trilogy::Error - end - - _(span.name).must_equal 'foo' - end - end - - it 'sets span name to mysql when db.operation is not set' do - OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'span_name=db_operation_and_name') do - instrumentation.instance_variable_set(:@installed, false) - instrumentation.install - - sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" - expect do - client.query(sql) - end.must_raise Trilogy::Error - - _(span.name).must_equal 'mysql' - end - end - end end end end diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb index 3bdc5518ad..c3680a73a6 100644 --- a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb @@ -128,14 +128,15 @@ it 'obfuscates sql' do client.query('SELECT 1') - _(span.name).must_equal 'select' + # Per stable semconv spec, span name uses db.namespace (not extracted from SQL) + _(span.name).must_equal database _(span.attributes['db.query.text']).must_equal 'SELECT ?' end it 'includes database connection information' do client.query('SELECT 1') - _(span.name).must_equal 'select' + _(span.name).must_equal database _(span.attributes['db.namespace']).must_equal(database) _(span.attributes['db.system.name']).must_equal 'mysql' _(span.attributes['db.query.text']).must_equal 'SELECT ?' @@ -153,22 +154,23 @@ _(span.attributes.key?('db.user')).must_equal false end - it 'extracts statement type' do + it 'uses db.namespace as span name per stable semconv spec' do explain_sql = 'EXPLAIN SELECT 1' client.query(explain_sql) - _(span.name).must_equal 'explain' + # Per stable semconv spec, span name is NOT extracted from SQL + _(span.name).must_equal database _(span.attributes['db.namespace']).must_equal(database) _(span.attributes['db.system.name']).must_equal 'mysql' _(span.attributes['db.query.text']).must_equal 'EXPLAIN SELECT ?' end - it 'uses component.name and instance.name as span.name fallbacks with invalid sql' do + it 'uses db.system.name as span.name fallback when db.namespace is not available' do expect do client.query('DESELECT 1') end.must_raise Trilogy::Error - _(span.name).must_equal 'mysql' + _(span.name).must_equal database _(span.attributes['db.namespace']).must_equal(database) _(span.attributes['db.system.name']).must_equal 'mysql' _(span.attributes['db.query.text']).must_equal 'DESELECT ?' @@ -207,7 +209,7 @@ it 'spans will include the server.address attribute' do _(client.connected_host).wont_be_nil - _(span.name).must_equal 'select' + _(span.name).must_equal database _(span.attributes['db.namespace']).must_equal(database) _(span.attributes['db.system.name']).must_equal 'mysql' _(span.attributes['db.query.text']).must_equal 'select @@hostname' @@ -217,7 +219,7 @@ last_span = exporter.finished_spans.last - _(last_span.name).must_equal 'select' + _(last_span.name).must_equal database _(last_span.attributes['db.namespace']).must_equal(database) _(last_span.attributes['db.system.name']).must_equal 'mysql' _(last_span.attributes['db.query.text']).must_equal 'SELECT ?' @@ -238,7 +240,7 @@ skip 'requires setup of a mysql host using uds connections' _(client.connected_host).wont_be_nil - _(span.name).must_equal 'select' + _(span.name).must_equal database _(span.attributes['db.namespace']).must_equal(database) _(span.attributes['db.system.name']).must_equal 'mysql' _(span.attributes['db.query.text']).must_equal 'select @@hostname' @@ -248,7 +250,7 @@ last_span = exporter.finished_spans.last - _(last_span.name).must_equal 'select' + _(last_span.name).must_equal database _(last_span.attributes['db.namespace']).must_equal(database) _(last_span.attributes['db.system.name']).must_equal 'mysql' _(last_span.attributes['db.query.text']).must_equal 'SELECT ?' @@ -263,7 +265,7 @@ client.query('SELECT INVALID') end.must_raise Trilogy::Error - _(span.name).must_equal 'select' + _(span.name).must_equal database _(span.attributes['db.namespace']).must_equal(database) _(span.attributes['db.system.name']).must_equal 'mysql' _(span.attributes['db.query.text']).must_equal 'SELECT INVALID' @@ -316,7 +318,7 @@ client.query(sql) end.must_raise Trilogy::Error - _(span.name).must_equal 'select' + _(span.name).must_equal database _(span.attributes['db.query.text']).must_equal sql end end @@ -331,7 +333,7 @@ client.query(sql) end.must_raise Trilogy::Error - _(span.name).must_equal 'select' + _(span.name).must_equal database _(span.attributes['db.query.text']).must_equal obfuscated_sql end @@ -344,7 +346,7 @@ client.query(sql) end.must_raise Trilogy::Error - _(span.name).must_equal 'select' + _(span.name).must_equal database _(span.attributes['db.query.text']).must_equal obfuscated_sql end @@ -499,7 +501,7 @@ client.query(sql) end.must_raise Trilogy::Error - _(span.name).must_equal 'select' + _(span.name).must_equal database _(span.attributes['db.query.text']).must_be_nil end end @@ -516,7 +518,7 @@ end.must_raise Trilogy::Error _(span.attributes['db.system.name']).must_equal 'mysql' - _(span.name).must_equal 'select' + _(span.name).must_equal database _(span.attributes['db.query.text']).must_be_nil end end @@ -535,7 +537,7 @@ end.must_raise Trilogy::Error _(span.attributes['db.system.name']).must_equal 'mysql' - _(span.name).must_equal 'select' + _(span.name).must_equal database _(span.attributes['db.query.text']).must_equal obfuscated_sql end end @@ -556,99 +558,54 @@ end.must_raise Trilogy::Error _(span.attributes['db.system.name']).must_equal 'mysql' - _(span.name).must_equal 'select' + _(span.name).must_equal database _(span.attributes['db.query.text']).must_equal obfuscated_sql end end end end - describe 'when span_name is set as statement_type' do - it 'sets span name to statement type' do - OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'span_name=statement_type') do - instrumentation.instance_variable_set(:@installed, false) - instrumentation.install + # In stable semconv, span naming follows the spec regardless of span_name config: + # {db.operation.name} {db.namespace} -> {db.namespace} -> mysql + # The span_name config option is ignored for stable semconv. - sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" - expect do - client.query(sql) - end.must_raise Trilogy::Error + describe 'span naming follows stable semconv spec' do + it 'uses db.namespace as span name by default' do + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" + expect do + client.query(sql) + end.must_raise Trilogy::Error - _(span.name).must_equal 'select' - end + # span_name config is ignored in stable semconv + _(span.name).must_equal database end - it 'sets span name to mysql when statement type is not recognized' do - OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'span_name=statement_type') do - instrumentation.instance_variable_set(:@installed, false) - instrumentation.install - - sql = 'DESELECT 1' + it 'uses db.operation.name and db.namespace when operation is provided via with_attributes' do + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" + OpenTelemetry::Instrumentation::Trilogy.with_attributes('db.operation.name' => 'SELECT') do expect do client.query(sql) end.must_raise Trilogy::Error - - _(span.name).must_equal 'mysql' end - end - end - - describe 'when span_name is set as db_name' do - it 'sets span name to db name' do - OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'span_name=db_name') do - instrumentation.instance_variable_set(:@installed, false) - instrumentation.install - - sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" - expect do - client.query(sql) - end.must_raise Trilogy::Error - _(span.name).must_equal 'mysql' - end + _(span.name).must_equal "SELECT #{database}" end describe 'when db name is nil' do let(:database) { nil } - it 'sets span name to mysql' do - OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'span_name=db_name') do - instrumentation.instance_variable_set(:@installed, false) - instrumentation.install - - sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" - expect do - client.query(sql) - end.must_raise Trilogy::Error - - _(span.name).must_equal 'mysql' - end - end - end - end - - describe 'when span_name is set as db_operation_and_name' do - it 'sets span name to db operation and name' do - OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'span_name=db_operation_and_name') do - instrumentation.instance_variable_set(:@installed, false) - instrumentation.install - + it 'uses db.operation.name when provided via with_attributes' do sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" - OpenTelemetry::Instrumentation::Trilogy.with_attributes('db.operation.name' => 'foo') do + OpenTelemetry::Instrumentation::Trilogy.with_attributes('db.operation.name' => 'SELECT') do expect do client.query(sql) end.must_raise Trilogy::Error end - _(span.name).must_equal 'foo mysql' + _(span.name).must_equal 'SELECT' end - end - - it 'sets span name to db name when db.operation is not set' do - OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'span_name=db_operation_and_name') do - instrumentation.instance_variable_set(:@installed, false) - instrumentation.install + it 'falls back to mysql when no operation or db name' do sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" expect do client.query(sql) @@ -657,40 +614,6 @@ _(span.name).must_equal 'mysql' end end - - describe 'when db name is nil' do - let(:database) { nil } - - it 'sets span name to db operation' do - OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'span_name=db_operation_and_name') do - instrumentation.instance_variable_set(:@installed, false) - instrumentation.install - - sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" - OpenTelemetry::Instrumentation::Trilogy.with_attributes('db.operation.name' => 'foo') do - expect do - client.query(sql) - end.must_raise Trilogy::Error - end - - _(span.name).must_equal 'foo' - end - end - - it 'sets span name to mysql when db.operation is not set' do - OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_TRILOGY_CONFIG_OPTS' => 'span_name=db_operation_and_name') do - instrumentation.instance_variable_set(:@installed, false) - instrumentation.install - - sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" - expect do - client.query(sql) - end.must_raise Trilogy::Error - - _(span.name).must_equal 'mysql' - end - end - end end end end From 769b3e2c53a4ce6f14c05904a112f71a02af0b7a Mon Sep 17 00:00:00 2001 From: Hannah Ramadan Date: Tue, 24 Mar 2026 08:11:28 -0700 Subject: [PATCH 10/23] Remove peer.service --- .../trilogy/patches/stable/client.rb | 1 - .../patches/stable/client_attributes_test.rb | 13 ------------- .../patches/stable/instrumentation_test.rb | 16 ---------------- 3 files changed, 30 deletions(-) diff --git a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb index eb08427042..28bb1d2580 100644 --- a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb +++ b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb @@ -87,7 +87,6 @@ def _build_otel_base_attributes attributes['server.port'] = mysql_port if mysql_port attributes['db.namespace'] = @_otel_database_name if @_otel_database_name - attributes[::OpenTelemetry::SemanticConventions::Trace::PEER_SERVICE] = config[:peer_service] unless config[:peer_service].nil? attributes end diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/client_attributes_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/client_attributes_test.rb index 245b49d5d2..8ca82e9be2 100644 --- a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/client_attributes_test.rb +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/client_attributes_test.rb @@ -99,19 +99,6 @@ def build_test_client(options) refute attrs.key?('db.instance.id') end - it 'includes peer_service when configured' do - instrumentation.instance_variable_set(:@installed, false) - instrumentation.install({ - db_statement: :omit, - span_name: :statement_type, - propagator: 'none', - record_exception: true, - obfuscation_limit: 2000, - peer_service: 'mysql-primary' - }) - attrs = client.send(:client_attributes) - assert_equal 'mysql-primary', attrs[OpenTelemetry::SemanticConventions::Trace::PEER_SERVICE] - end it 'returns independent hash instances on each call' do a = client.send(:client_attributes) diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb index c3680a73a6..81f24f5bb3 100644 --- a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb @@ -55,22 +55,6 @@ end describe '#install' do - it 'accepts peer service name from config' do - instrumentation.instance_variable_set(:@installed, false) - instrumentation.install(peer_service: 'readonly:mysql') - client.query('SELECT 1') - - _(span.attributes[OpenTelemetry::SemanticConventions::Trace::PEER_SERVICE]).must_equal 'readonly:mysql' - end - - it 'omits peer service by default' do - instrumentation.instance_variable_set(:@installed, false) - instrumentation.install({}) - client.query('SELECT 1') - - _(span.attributes.keys).wont_include(OpenTelemetry::SemanticConventions::Trace::PEER_SERVICE) - end - end describe '#compatible?' do describe 'when an unsupported version is installed' do From 7180d60ec655d3b316e8701ecb3cd4317e9ff0f3 Mon Sep 17 00:00:00 2001 From: Hannah Ramadan Date: Tue, 24 Mar 2026 09:28:14 -0700 Subject: [PATCH 11/23] fix syntax error --- .../trilogy/patches/stable/instrumentation_test.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb index 81f24f5bb3..ed9f7b0af1 100644 --- a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb @@ -54,8 +54,6 @@ _(instrumentation.version).wont_be_empty end - describe '#install' do - describe '#compatible?' do describe 'when an unsupported version is installed' do it 'is incompatible' do From cfcfbe0bcb0f8abe24ad95c8221a65c59903cd50 Mon Sep 17 00:00:00 2001 From: Hannah Ramadan Date: Tue, 24 Mar 2026 09:31:02 -0700 Subject: [PATCH 12/23] file formatter: Remove extra blank line --- .../trilogy/patches/stable/client_attributes_test.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/client_attributes_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/client_attributes_test.rb index 8ca82e9be2..48c1dc4376 100644 --- a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/client_attributes_test.rb +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/client_attributes_test.rb @@ -99,7 +99,6 @@ def build_test_client(options) refute attrs.key?('db.instance.id') end - it 'returns independent hash instances on each call' do a = client.send(:client_attributes) b = client.send(:client_attributes) From 6171b0819a573b062258339b0158932673b19a9c Mon Sep 17 00:00:00 2001 From: Hannah Ramadan <76922290+hannahramadan@users.noreply.github.com> Date: Mon, 13 Apr 2026 19:02:35 -0700 Subject: [PATCH 13/23] Update instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb Co-authored-by: Robb Kidd --- .../instrumentation/trilogy/patches/stable/client.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb index 28bb1d2580..251074d59d 100644 --- a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb +++ b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb @@ -108,7 +108,7 @@ def client_attributes(sql = nil) def set_error_attributes(span, error) span.set_attribute('error.type', error.class.name) - span.set_attribute('db.response.status_code', error.error_code.to_s) if error.error_code + span.set_attribute('db.response.status_code', error.error_code.to_s) if error.respond_to?(:error_code) && error.error_code end def tracer From db681245275a0aab66f9850dc4b400ce07e1c25d Mon Sep 17 00:00:00 2001 From: Hannah Ramadan Date: Mon, 13 Apr 2026 19:26:27 -0700 Subject: [PATCH 14/23] Update README Include table for stable v old conventions --- instrumentation/trilogy/README.md | 36 +++++++++++++++++-------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/instrumentation/trilogy/README.md b/instrumentation/trilogy/README.md index 6bbb562125..e0f88c2f9c 100644 --- a/instrumentation/trilogy/README.md +++ b/instrumentation/trilogy/README.md @@ -53,22 +53,20 @@ end ## Semantic Conventions -This instrumentation generally uses [Database semantic conventions](https://opentelemetry.io/docs/specs/semconv/database/database-spans/). - -| Attribute Name | Type | Notes | -| - | - | - | -| `db.instance.id` | String | The name of the DB host executing the query e.g. `SELECT @@hostname` | -| `db.name` | String | The name of the database from connection_options | -| `db.statement` | String | SQL statement being executed | -| `db.user` | String | The username from connection_options | -| `db.system` | String | `mysql` | -| `net.peer.name` | String | The name of the remote host from connection_options | - -## How can I get involved? - -The `opentelemetry-instrumentation-trilogy` gem source is [on github][repo-github], along with related gems including `opentelemetry-api` and `opentelemetry-sdk`. - -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]. +This instrumentation generally uses [Database semantic conventions](https://opentelemetry.io/docs/specs/semconv/database/database-spans/). See the [Database semantic convention stability](#database-semantic-convention-stability) section for how to switch between stable and old conventions. + +| Stable Attribute Name | Old Attribute Name | Type | Notes | +| - | - | - | - | +| `db.namespace` | `db.name` | String | Database name from connection_options | +| `db.query.text` | `db.statement` | String | The database query being executed; set according to the `db_statement` config option | +| `db.response.status_code` | — | String | The Trilogy error code, if available | +| `db.system.name` | `db.system` | String | DBMS product identifier; always `mysql` | +| `error.type` | — | String | The exception class name when the operation fails | +| `server.address` | `net.peer.name` | String | Database host from connection_options | +| `server.port` | — | Integer | Database port from connection_options | +| — | `db.instance.id` | String | Connected host, e.g. result of `SELECT @@hostname` | +| — | `db.user` | String | Database username from connection_options | +| — | `peer.service` | String | Configured via the `peer_service` config option | ## Database semantic convention stability @@ -86,6 +84,12 @@ During the transition from old to stable conventions, Trilogy instrumentation co For additional information on migration, please refer to our [documentation](https://opentelemetry.io/docs/specs/semconv/non-normative/db-migration/). +## How can I get involved? + +The `opentelemetry-instrumentation-trilogy` gem source is [on github][repo-github], along with related gems including `opentelemetry-api` and `opentelemetry-sdk`. + +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]. + ## License The `opentelemetry-instrumentation-trilogy` gem is distributed under the Apache 2.0 license. See [LICENSE][license-github] for more information. From 76d80f4a34a84ff41a0a984bbcc2877802501f1e Mon Sep 17 00:00:00 2001 From: Hannah Ramadan Date: Mon, 13 Apr 2026 19:48:11 -0700 Subject: [PATCH 15/23] Add config table and doc comment --- instrumentation/trilogy/README.md | 11 ++++ .../trilogy/instrumentation.rb | 65 ++++++++++++++++++- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/instrumentation/trilogy/README.md b/instrumentation/trilogy/README.md index e0f88c2f9c..41345a52f3 100644 --- a/instrumentation/trilogy/README.md +++ b/instrumentation/trilogy/README.md @@ -51,6 +51,17 @@ OpenTelemetry::Instrumentation::Trilogy.with_attributes('pizzatoppings' => 'mush end ``` +## Configuration Options + +| Option | Default | Description | +| ------ | ------- | ----------- | +| `db_statement` | `:obfuscate` | Controls how SQL queries appear in spans. `:obfuscate` replaces literal values with `?`, `:include` records the raw SQL, `:omit` excludes the attribute entirely. | +| `obfuscation_limit` | `2000` | Maximum length of the obfuscated SQL statement. Statements exceeding this limit are truncated. | +| `peer_service` | `nil` | Sets the `peer.service` attribute on spans (old semantic conventions only). | +| `propagator` | `'none'` | Propagator for injecting trace context into SQL comments. `'none'` disables propagation, `'tracecontext'` uses W3C Trace Context, `'vitess'` uses Vitess-style propagation (requires `opentelemetry-propagator-vitess` gem). | +| `record_exception` | `true` | Records exceptions as span events when an error occurs. | +| `span_name` | `:statement_type` | Controls span naming (old semantic conventions only). `:statement_type` uses the SQL operation (e.g., `SELECT`), `:db_name` uses the database name, `:db_operation_and_name` combines both. | + ## Semantic Conventions This instrumentation generally uses [Database semantic conventions](https://opentelemetry.io/docs/specs/semconv/database/database-spans/). See the [Database semantic convention stability](#database-semantic-convention-stability) section for how to switch between stable and old conventions. diff --git a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/instrumentation.rb b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/instrumentation.rb index c69546ae37..e02362e7dd 100644 --- a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/instrumentation.rb +++ b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/instrumentation.rb @@ -7,7 +7,70 @@ module OpenTelemetry module Instrumentation module Trilogy - # The Instrumentation class contains logic to detect and install the Trilogy instrumentation + # The {OpenTelemetry::Instrumentation::Trilogy::Instrumentation} class contains logic to detect and install the Trilogy instrumentation + # + # Installation and configuration of this instrumentation is done within the + # {https://www.rubydoc.info/gems/opentelemetry-sdk/OpenTelemetry/SDK#configure-instance_method OpenTelemetry::SDK#configure} + # block, calling {https://www.rubydoc.info/gems/opentelemetry-sdk/OpenTelemetry%2FSDK%2FConfigurator:use use()} + # or {https://www.rubydoc.info/gems/opentelemetry-sdk/OpenTelemetry%2FSDK%2FConfigurator:use_all use_all()}. + # + # ## Configuration keys and options + # + # ### `:db_statement` + # + # Controls how SQL queries appear in spans. + # + # - `:obfuscate` **(default)** - Replaces literal values with `?` to prevent + # sensitive data from being recorded. + # - `:include` - Records the raw SQL query as-is. + # - `:omit` - Excludes the SQL query attribute entirely. + # + # ### `:obfuscation_limit` + # + # Maximum length of the obfuscated SQL statement. Statements exceeding this limit + # are truncated. Default is `2000`. + # + # ### `:peer_service` + # + # Sets the `peer.service` attribute on spans. Only applies when using old semantic + # conventions. Default is `nil`. + # + # ### `:propagator` + # + # Propagator for injecting trace context into SQL comments. + # + # - `'none'` **(default)** - Disables trace context propagation. + # - `'tracecontext'` - Uses W3C Trace Context format via SQL comments. + # - `'vitess'` - Uses Vitess-style propagation. Requires the + # `opentelemetry-propagator-vitess` gem. + # + # ### `:record_exception` + # + # Records exceptions as span events when an error occurs. Default is `true`. + # + # ### `:span_name` + # + # Controls how span names are generated. Only applies when using old semantic + # conventions; ignored for stable semantic conventions. + # + # - `:statement_type` **(default)** - Uses the SQL operation (e.g., `SELECT`). + # - `:db_name` - Uses the database name. + # - `:db_operation_and_name` - Combines the operation and database name. + # + # @example An explicit default configuration + # OpenTelemetry::SDK.configure do |c| + # c.use_all({ + # 'OpenTelemetry::Instrumentation::Trilogy' => { + # db_statement: :obfuscate, + # obfuscation_limit: 2000, + # peer_service: nil, + # propagator: 'none', + # record_exception: true, + # span_name: :statement_type, + # }, + # }) + # end + # class Instrumentation < OpenTelemetry::Instrumentation::Base install do |config| require_dependencies From 73149e53cfa50ccbb30cdc4fd108f61543980c24 Mon Sep 17 00:00:00 2001 From: Hannah Ramadan Date: Tue, 14 Apr 2026 22:12:48 -0700 Subject: [PATCH 16/23] Revert span names change in dup/stable --- .../mysql/lib/opentelemetry/helpers/mysql.rb | 25 ----- .../trilogy/patches/dup/client.rb | 8 +- .../trilogy/patches/stable/client.rb | 8 +- .../patches/dup/instrumentation_test.rb | 97 ++++++++--------- .../patches/stable/instrumentation_test.rb | 101 +++++++++--------- 5 files changed, 110 insertions(+), 129 deletions(-) diff --git a/helpers/mysql/lib/opentelemetry/helpers/mysql.rb b/helpers/mysql/lib/opentelemetry/helpers/mysql.rb index 07b8be81c6..7bad09f7a3 100644 --- a/helpers/mysql/lib/opentelemetry/helpers/mysql.rb +++ b/helpers/mysql/lib/opentelemetry/helpers/mysql.rb @@ -67,31 +67,6 @@ def database_span_name(sql, operation, database_name, config) end || 'mysql' end - # Span naming following stable database semantic conventions. - # Per spec: {db.query.summary} -> {db.operation.name} {target} -> {target} -> {db.system.name} - # We don't have db.query.summary, so we use: - # {db.operation.name} {db.namespace} -> {db.namespace} -> mysql - # - # Note: Per spec, db.operation.name SHOULD NOT be extracted from db.query.text. - # The operation should only be used if explicitly provided by the application - # (e.g., via with_attributes). - # - # @param operation [String] The database operation (db.operation.name), if provided by the application. - # @param database_name [String] The name of the database (db.namespace). - # @return [String] The span name. - # @api private - def stable_database_span_name(operation, database_name) - if operation && database_name - "#{operation} #{database_name}" - elsif database_name - database_name - elsif operation - operation - else - 'mysql' - end - end - # @api private def extract_statement_type(sql) return unless sql diff --git a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/dup/client.rb b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/dup/client.rb index df11d9708f..87b6eae045 100644 --- a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/dup/client.rb +++ b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/dup/client.rb @@ -50,9 +50,11 @@ def query(sql) context_attributes = OpenTelemetry::Instrumentation::Trilogy.attributes tracer.in_span( - OpenTelemetry::Helpers::MySQL.stable_database_span_name( - context_attributes[OpenTelemetry::SemanticConventions::Trace::DB_OPERATION] || context_attributes['db.operation.name'], - @_otel_database_name + OpenTelemetry::Helpers::MySQL.database_span_name( + sql, + context_attributes[OpenTelemetry::SemanticConventions::Trace::DB_OPERATION], + @_otel_database_name, + config ), attributes: client_attributes(sql).merge!(context_attributes), kind: :client, diff --git a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb index 251074d59d..c71ec91e64 100644 --- a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb +++ b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/stable/client.rb @@ -50,9 +50,11 @@ def query(sql) context_attributes = OpenTelemetry::Instrumentation::Trilogy.attributes tracer.in_span( - OpenTelemetry::Helpers::MySQL.stable_database_span_name( - context_attributes['db.operation.name'], - @_otel_database_name + OpenTelemetry::Helpers::MySQL.database_span_name( + sql, + context_attributes[OpenTelemetry::SemanticConventions::Trace::DB_OPERATION], + @_otel_database_name, + config ), attributes: client_attributes(sql).merge!(context_attributes), kind: :client, diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb index c62b7ac2f4..a707d80333 100644 --- a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb @@ -128,8 +128,7 @@ it 'obfuscates sql in both old and stable attributes' do client.query('SELECT 1') - # Per stable semconv spec, span name uses db.namespace (not extracted from SQL) - _(span.name).must_equal database + _(span.name).must_equal 'select' # Old attribute _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_equal 'SELECT ?' # Stable attribute @@ -139,7 +138,7 @@ it 'includes both old and stable database connection information' do client.query('SELECT 1') - _(span.name).must_equal database + _(span.name).must_equal 'select' # Old attributes _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_SYSTEM]).must_equal 'mysql' @@ -157,12 +156,11 @@ _(span.attributes['db.query.text']).must_equal 'SELECT ?' end - it 'uses db.namespace as span name per stable semconv spec' do + it 'extracts operation name from SQL for span name' do explain_sql = 'EXPLAIN SELECT 1' client.query(explain_sql) - # Per stable semconv spec, span name is NOT extracted from SQL - _(span.name).must_equal database + _(span.name).must_equal 'explain' # Old attributes _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_NAME]).must_equal(database) @@ -245,7 +243,7 @@ it 'spans will include both old and stable attributes' do _(client.connected_host).wont_be_nil - _(span.name).must_equal database + _(span.name).must_equal 'select' # Old attributes _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_NAME]).must_equal(database) @@ -265,7 +263,7 @@ last_span = exporter.finished_spans.last - _(last_span.name).must_equal database + _(last_span.name).must_equal 'select' # Old attributes on last span _(last_span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_NAME]).must_equal(database) @@ -296,7 +294,7 @@ skip 'requires setup of a mysql host using uds connections' _(client.connected_host).wont_be_nil - _(span.name).must_equal database + _(span.name).must_equal 'select' # Old attributes _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_NAME]).must_equal(database) @@ -315,7 +313,7 @@ last_span = exporter.finished_spans.last - _(last_span.name).must_equal database + _(last_span.name).must_equal 'select' # Old attributes _(last_span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_NAME]).must_equal(database) @@ -340,7 +338,7 @@ client.query('SELECT INVALID') end.must_raise Trilogy::Error - _(span.name).must_equal database + _(span.name).must_equal 'select' # Old attributes _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_NAME]).must_equal(database) @@ -401,7 +399,7 @@ client.query(sql) end.must_raise Trilogy::Error - _(span.name).must_equal database + _(span.name).must_equal 'select' _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_equal sql _(span.attributes['db.query.text']).must_equal sql end @@ -417,7 +415,7 @@ client.query(sql) end.must_raise Trilogy::Error - _(span.name).must_equal database + _(span.name).must_equal 'select' _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_equal obfuscated_sql _(span.attributes['db.query.text']).must_equal obfuscated_sql end @@ -431,7 +429,7 @@ client.query(sql) end.must_raise Trilogy::Error - _(span.name).must_equal database + _(span.name).must_equal 'select' _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_equal obfuscated_sql _(span.attributes['db.query.text']).must_equal obfuscated_sql end @@ -588,7 +586,7 @@ client.query(sql) end.must_raise Trilogy::Error - _(span.name).must_equal database + _(span.name).must_equal 'select' _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_be_nil _(span.attributes['db.query.text']).must_be_nil end @@ -607,7 +605,7 @@ _(span.attributes['db.system']).must_equal 'mysql' _(span.attributes['db.system.name']).must_equal 'mysql' - _(span.name).must_equal database + _(span.name).must_equal 'select' _(span.attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_be_nil _(span.attributes['db.query.text']).must_be_nil end @@ -628,7 +626,7 @@ _(span.attributes['db.system']).must_equal 'mysql' _(span.attributes['db.system.name']).must_equal 'mysql' - _(span.name).must_equal database + _(span.name).must_equal 'select' _(span.attributes['db.statement']).must_equal obfuscated_sql _(span.attributes['db.query.text']).must_equal obfuscated_sql end @@ -651,7 +649,7 @@ _(span.attributes['db.system']).must_equal 'mysql' _(span.attributes['db.system.name']).must_equal 'mysql' - _(span.name).must_equal database + _(span.name).must_equal 'select' _(span.attributes['db.statement']).must_equal obfuscated_sql _(span.attributes['db.query.text']).must_equal obfuscated_sql end @@ -659,53 +657,56 @@ end end - # In dup semconv mode, span naming follows the stable spec regardless of span_name config: - # {db.operation.name} {db.namespace} -> {db.namespace} -> mysql - # The span_name config option is ignored for dup semconv (uses stable naming). + describe 'span_name config option' do + describe 'when span_name is set to :statement_type (default)' do + let(:config) { { span_name: :statement_type } } - describe 'span naming follows stable semconv spec' do - it 'uses db.namespace as span name by default' do - sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" - expect do - client.query(sql) - end.must_raise Trilogy::Error - - # span_name config is ignored in dup semconv - _(span.name).must_equal database - end - - it 'uses db.operation.name and db.namespace when operation is provided via with_attributes' do - sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" - OpenTelemetry::Instrumentation::Trilogy.with_attributes('db.operation' => 'SELECT') do + it 'uses statement type extracted from SQL as span name' do + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" expect do client.query(sql) end.must_raise Trilogy::Error - end - _(span.name).must_equal "SELECT #{database}" + _(span.name).must_equal 'select' + end end - describe 'when db name is nil' do - let(:database) { nil } + describe 'when span_name is set to :db_name' do + let(:config) { { span_name: :db_name } } - it 'uses db.operation.name when provided via with_attributes' do + it 'uses database name as span name' do sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" - OpenTelemetry::Instrumentation::Trilogy.with_attributes('db.operation' => 'SELECT') do - expect do - client.query(sql) - end.must_raise Trilogy::Error - end + expect do + client.query(sql) + end.must_raise Trilogy::Error - _(span.name).must_equal 'SELECT' + _(span.name).must_equal database end + end + + describe 'when span_name is set to :db_operation_and_name' do + let(:config) { { span_name: :db_operation_and_name } } - it 'falls back to mysql when no operation or db name' do + it 'uses operation and database name as span name' do sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" expect do client.query(sql) end.must_raise Trilogy::Error - _(span.name).must_equal 'mysql' + _(span.name).must_equal "select #{database}" + end + + describe 'when db name is nil' do + let(:database) { nil } + + it 'uses only operation name when db name is nil' do + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" + expect do + client.query(sql) + end.must_raise Trilogy::Error + + _(span.name).must_equal 'select' + end end end end diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb index ed9f7b0af1..1f8efcb2b7 100644 --- a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb @@ -110,15 +110,14 @@ it 'obfuscates sql' do client.query('SELECT 1') - # Per stable semconv spec, span name uses db.namespace (not extracted from SQL) - _(span.name).must_equal database + _(span.name).must_equal 'select' _(span.attributes['db.query.text']).must_equal 'SELECT ?' end it 'includes database connection information' do client.query('SELECT 1') - _(span.name).must_equal database + _(span.name).must_equal 'select' _(span.attributes['db.namespace']).must_equal(database) _(span.attributes['db.system.name']).must_equal 'mysql' _(span.attributes['db.query.text']).must_equal 'SELECT ?' @@ -136,23 +135,22 @@ _(span.attributes.key?('db.user')).must_equal false end - it 'uses db.namespace as span name per stable semconv spec' do + it 'extracts operation name from SQL for span name' do explain_sql = 'EXPLAIN SELECT 1' client.query(explain_sql) - # Per stable semconv spec, span name is NOT extracted from SQL - _(span.name).must_equal database + _(span.name).must_equal 'explain' _(span.attributes['db.namespace']).must_equal(database) _(span.attributes['db.system.name']).must_equal 'mysql' _(span.attributes['db.query.text']).must_equal 'EXPLAIN SELECT ?' end - it 'uses db.system.name as span.name fallback when db.namespace is not available' do + it 'uses mysql as span.name fallback for invalid SQL' do expect do client.query('DESELECT 1') end.must_raise Trilogy::Error - _(span.name).must_equal database + _(span.name).must_equal 'mysql' _(span.attributes['db.namespace']).must_equal(database) _(span.attributes['db.system.name']).must_equal 'mysql' _(span.attributes['db.query.text']).must_equal 'DESELECT ?' @@ -191,7 +189,7 @@ it 'spans will include the server.address attribute' do _(client.connected_host).wont_be_nil - _(span.name).must_equal database + _(span.name).must_equal 'select' _(span.attributes['db.namespace']).must_equal(database) _(span.attributes['db.system.name']).must_equal 'mysql' _(span.attributes['db.query.text']).must_equal 'select @@hostname' @@ -201,7 +199,7 @@ last_span = exporter.finished_spans.last - _(last_span.name).must_equal database + _(last_span.name).must_equal 'select' _(last_span.attributes['db.namespace']).must_equal(database) _(last_span.attributes['db.system.name']).must_equal 'mysql' _(last_span.attributes['db.query.text']).must_equal 'SELECT ?' @@ -222,7 +220,7 @@ skip 'requires setup of a mysql host using uds connections' _(client.connected_host).wont_be_nil - _(span.name).must_equal database + _(span.name).must_equal 'select' _(span.attributes['db.namespace']).must_equal(database) _(span.attributes['db.system.name']).must_equal 'mysql' _(span.attributes['db.query.text']).must_equal 'select @@hostname' @@ -232,7 +230,7 @@ last_span = exporter.finished_spans.last - _(last_span.name).must_equal database + _(last_span.name).must_equal 'select' _(last_span.attributes['db.namespace']).must_equal(database) _(last_span.attributes['db.system.name']).must_equal 'mysql' _(last_span.attributes['db.query.text']).must_equal 'SELECT ?' @@ -247,7 +245,7 @@ client.query('SELECT INVALID') end.must_raise Trilogy::Error - _(span.name).must_equal database + _(span.name).must_equal 'select' _(span.attributes['db.namespace']).must_equal(database) _(span.attributes['db.system.name']).must_equal 'mysql' _(span.attributes['db.query.text']).must_equal 'SELECT INVALID' @@ -300,7 +298,7 @@ client.query(sql) end.must_raise Trilogy::Error - _(span.name).must_equal database + _(span.name).must_equal 'select' _(span.attributes['db.query.text']).must_equal sql end end @@ -315,7 +313,7 @@ client.query(sql) end.must_raise Trilogy::Error - _(span.name).must_equal database + _(span.name).must_equal 'select' _(span.attributes['db.query.text']).must_equal obfuscated_sql end @@ -328,7 +326,7 @@ client.query(sql) end.must_raise Trilogy::Error - _(span.name).must_equal database + _(span.name).must_equal 'select' _(span.attributes['db.query.text']).must_equal obfuscated_sql end @@ -483,7 +481,7 @@ client.query(sql) end.must_raise Trilogy::Error - _(span.name).must_equal database + _(span.name).must_equal 'select' _(span.attributes['db.query.text']).must_be_nil end end @@ -500,7 +498,7 @@ end.must_raise Trilogy::Error _(span.attributes['db.system.name']).must_equal 'mysql' - _(span.name).must_equal database + _(span.name).must_equal 'select' _(span.attributes['db.query.text']).must_be_nil end end @@ -519,7 +517,7 @@ end.must_raise Trilogy::Error _(span.attributes['db.system.name']).must_equal 'mysql' - _(span.name).must_equal database + _(span.name).must_equal 'select' _(span.attributes['db.query.text']).must_equal obfuscated_sql end end @@ -540,60 +538,63 @@ end.must_raise Trilogy::Error _(span.attributes['db.system.name']).must_equal 'mysql' - _(span.name).must_equal database + _(span.name).must_equal 'select' _(span.attributes['db.query.text']).must_equal obfuscated_sql end end end end - # In stable semconv, span naming follows the spec regardless of span_name config: - # {db.operation.name} {db.namespace} -> {db.namespace} -> mysql - # The span_name config option is ignored for stable semconv. + describe 'span_name config option' do + describe 'when span_name is set to :statement_type (default)' do + let(:config) { { span_name: :statement_type } } - describe 'span naming follows stable semconv spec' do - it 'uses db.namespace as span name by default' do - sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" - expect do - client.query(sql) - end.must_raise Trilogy::Error - - # span_name config is ignored in stable semconv - _(span.name).must_equal database - end - - it 'uses db.operation.name and db.namespace when operation is provided via with_attributes' do - sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" - OpenTelemetry::Instrumentation::Trilogy.with_attributes('db.operation.name' => 'SELECT') do + it 'uses statement type extracted from SQL as span name' do + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" expect do client.query(sql) end.must_raise Trilogy::Error - end - _(span.name).must_equal "SELECT #{database}" + _(span.name).must_equal 'select' + end end - describe 'when db name is nil' do - let(:database) { nil } + describe 'when span_name is set to :db_name' do + let(:config) { { span_name: :db_name } } - it 'uses db.operation.name when provided via with_attributes' do + it 'uses database name as span name' do sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" - OpenTelemetry::Instrumentation::Trilogy.with_attributes('db.operation.name' => 'SELECT') do - expect do - client.query(sql) - end.must_raise Trilogy::Error - end + expect do + client.query(sql) + end.must_raise Trilogy::Error - _(span.name).must_equal 'SELECT' + _(span.name).must_equal database end + end + + describe 'when span_name is set to :db_operation_and_name' do + let(:config) { { span_name: :db_operation_and_name } } - it 'falls back to mysql when no operation or db name' do + it 'uses operation and database name as span name' do sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" expect do client.query(sql) end.must_raise Trilogy::Error - _(span.name).must_equal 'mysql' + _(span.name).must_equal "select #{database}" + end + + describe 'when db name is nil' do + let(:database) { nil } + + it 'uses only operation name when db name is nil' do + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" + expect do + client.query(sql) + end.must_raise Trilogy::Error + + _(span.name).must_equal 'select' + end end end end From d47c59bed2fe71683017965aa4b4efcc4ef21be0 Mon Sep 17 00:00:00 2001 From: Hannah Ramadan Date: Tue, 14 Apr 2026 22:34:59 -0700 Subject: [PATCH 17/23] Update tests --- helpers/mysql/test/helpers/mysql_test.rb | 45 ---------------- .../patches/dup/instrumentation_test.rb | 53 ------------------- .../patches/stable/instrumentation_test.rb | 53 ------------------- 3 files changed, 151 deletions(-) diff --git a/helpers/mysql/test/helpers/mysql_test.rb b/helpers/mysql/test/helpers/mysql_test.rb index 7352366e1e..e301182c49 100644 --- a/helpers/mysql/test/helpers/mysql_test.rb +++ b/helpers/mysql/test/helpers/mysql_test.rb @@ -55,51 +55,6 @@ end end - describe '.stable_database_span_name' do - let(:operation) { 'SELECT' } - let(:database_name) { 'mydb' } - let(:stable_span_name) { OpenTelemetry::Helpers::MySQL.stable_database_span_name(operation, database_name) } - - describe 'when operation and database_name are present' do - it 'returns "{operation} {database_name}"' do - assert_equal('SELECT mydb', stable_span_name) - end - end - - describe 'when only database_name is present' do - let(:operation) { nil } - - it 'returns database_name' do - assert_equal('mydb', stable_span_name) - end - end - - describe 'when only operation is present' do - let(:database_name) { nil } - - it 'returns operation' do - assert_equal('SELECT', stable_span_name) - end - end - - describe 'when both operation and database_name are nil' do - let(:operation) { nil } - let(:database_name) { nil } - - it 'returns mysql as fallback' do - assert_equal('mysql', stable_span_name) - end - end - - describe 'preserves operation case as provided' do - it 'does not normalize case' do - assert_equal('select mydb', OpenTelemetry::Helpers::MySQL.stable_database_span_name('select', 'mydb')) - assert_equal('SELECT mydb', OpenTelemetry::Helpers::MySQL.stable_database_span_name('SELECT', 'mydb')) - assert_equal('Select mydb', OpenTelemetry::Helpers::MySQL.stable_database_span_name('Select', 'mydb')) - end - end - end - describe '.db_operation_and_name' do let(:operation) { 'operation' } let(:database_name) { 'database_name' } diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb index a707d80333..be7a810d99 100644 --- a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb @@ -657,58 +657,5 @@ end end - describe 'span_name config option' do - describe 'when span_name is set to :statement_type (default)' do - let(:config) { { span_name: :statement_type } } - - it 'uses statement type extracted from SQL as span name' do - sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" - expect do - client.query(sql) - end.must_raise Trilogy::Error - - _(span.name).must_equal 'select' - end - end - - describe 'when span_name is set to :db_name' do - let(:config) { { span_name: :db_name } } - - it 'uses database name as span name' do - sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" - expect do - client.query(sql) - end.must_raise Trilogy::Error - - _(span.name).must_equal database - end - end - - describe 'when span_name is set to :db_operation_and_name' do - let(:config) { { span_name: :db_operation_and_name } } - - it 'uses operation and database name as span name' do - sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" - expect do - client.query(sql) - end.must_raise Trilogy::Error - - _(span.name).must_equal "select #{database}" - end - - describe 'when db name is nil' do - let(:database) { nil } - - it 'uses only operation name when db name is nil' do - sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" - expect do - client.query(sql) - end.must_raise Trilogy::Error - - _(span.name).must_equal 'select' - end - end - end - end end end diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb index 1f8efcb2b7..119edbc0ea 100644 --- a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb @@ -545,58 +545,5 @@ end end - describe 'span_name config option' do - describe 'when span_name is set to :statement_type (default)' do - let(:config) { { span_name: :statement_type } } - - it 'uses statement type extracted from SQL as span name' do - sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" - expect do - client.query(sql) - end.must_raise Trilogy::Error - - _(span.name).must_equal 'select' - end - end - - describe 'when span_name is set to :db_name' do - let(:config) { { span_name: :db_name } } - - it 'uses database name as span name' do - sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" - expect do - client.query(sql) - end.must_raise Trilogy::Error - - _(span.name).must_equal database - end - end - - describe 'when span_name is set to :db_operation_and_name' do - let(:config) { { span_name: :db_operation_and_name } } - - it 'uses operation and database name as span name' do - sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" - expect do - client.query(sql) - end.must_raise Trilogy::Error - - _(span.name).must_equal "select #{database}" - end - - describe 'when db name is nil' do - let(:database) { nil } - - it 'uses only operation name when db name is nil' do - sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" - expect do - client.query(sql) - end.must_raise Trilogy::Error - - _(span.name).must_equal 'select' - end - end - end - end end end From 035ec6a868cc203b242c08a3606ccd845a7ae4e3 Mon Sep 17 00:00:00 2001 From: Hannah Ramadan Date: Tue, 14 Apr 2026 22:39:29 -0700 Subject: [PATCH 18/23] linter cop --- .../instrumentation/trilogy/patches/dup/instrumentation_test.rb | 1 - .../trilogy/patches/stable/instrumentation_test.rb | 1 - 2 files changed, 2 deletions(-) diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb index be7a810d99..67d513212d 100644 --- a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb @@ -656,6 +656,5 @@ end end end - end end diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb index 119edbc0ea..f37299d7d5 100644 --- a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb @@ -544,6 +544,5 @@ end end end - end end From 73f57784c93e8daf0f551baed442461361fefeaf Mon Sep 17 00:00:00 2001 From: Hannah Ramadan <76922290+hannahramadan@users.noreply.github.com> Date: Tue, 21 Apr 2026 13:40:08 -0700 Subject: [PATCH 19/23] Apply suggestions from code review Co-authored-by: Robb Kidd --- .../instrumentation/trilogy/patches/dup/client.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/dup/client.rb b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/dup/client.rb index 87b6eae045..0c13741b73 100644 --- a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/dup/client.rb +++ b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/patches/dup/client.rb @@ -103,7 +103,7 @@ def _build_otel_base_attributes # db.user (old only - removed in stable) attributes[::OpenTelemetry::SemanticConventions::Trace::DB_USER] = database_user if database_user - # peer.service (same in both) + # peer.service (old only - not stable and not a db attribute) attributes[::OpenTelemetry::SemanticConventions::Trace::PEER_SERVICE] = config[:peer_service] unless config[:peer_service].nil? attributes end @@ -134,7 +134,7 @@ def client_attributes(sql = nil) def set_error_attributes(span, error) span.set_attribute('error.type', error.class.name) - span.set_attribute('db.response.status_code', error.error_code.to_s) if error.error_code + span.set_attribute('db.response.status_code', error.error_code.to_s) if error.respond_to?(:error_code) && error.error_code end def tracer From 3c14c392073fc43bc0cc636206f9a4eb9dba35f5 Mon Sep 17 00:00:00 2001 From: Hannah Ramadan <76922290+hannahramadan@users.noreply.github.com> Date: Tue, 21 Apr 2026 13:46:31 -0700 Subject: [PATCH 20/23] Update README.md --- instrumentation/trilogy/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instrumentation/trilogy/README.md b/instrumentation/trilogy/README.md index 41345a52f3..67b6c89a00 100644 --- a/instrumentation/trilogy/README.md +++ b/instrumentation/trilogy/README.md @@ -57,7 +57,7 @@ end | ------ | ------- | ----------- | | `db_statement` | `:obfuscate` | Controls how SQL queries appear in spans. `:obfuscate` replaces literal values with `?`, `:include` records the raw SQL, `:omit` excludes the attribute entirely. | | `obfuscation_limit` | `2000` | Maximum length of the obfuscated SQL statement. Statements exceeding this limit are truncated. | -| `peer_service` | `nil` | Sets the `peer.service` attribute on spans (old semantic conventions only). | +| `peer_service` | `nil` | Deprecated with no replacement. Sets the `peer.service` attribute on spans (old semantic conventions only). | | `propagator` | `'none'` | Propagator for injecting trace context into SQL comments. `'none'` disables propagation, `'tracecontext'` uses W3C Trace Context, `'vitess'` uses Vitess-style propagation (requires `opentelemetry-propagator-vitess` gem). | | `record_exception` | `true` | Records exceptions as span events when an error occurs. | | `span_name` | `:statement_type` | Controls span naming (old semantic conventions only). `:statement_type` uses the SQL operation (e.g., `SELECT`), `:db_name` uses the database name, `:db_operation_and_name` combines both. | From bb0dc489747cafd40d134f833b8f75f46c23f6a7 Mon Sep 17 00:00:00 2001 From: Hannah Ramadan <76922290+hannahramadan@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:12:55 -0700 Subject: [PATCH 21/23] Update instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/instrumentation.rb Co-authored-by: Robb Kidd --- .../opentelemetry/instrumentation/trilogy/instrumentation.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/instrumentation.rb b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/instrumentation.rb index e02362e7dd..5ec58b86d1 100644 --- a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/instrumentation.rb +++ b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/instrumentation.rb @@ -32,8 +32,8 @@ module Trilogy # # ### `:peer_service` # - # Sets the `peer.service` attribute on spans. Only applies when using old semantic - # conventions. Default is `nil`. + # Sets the `peer.service` attribute on spans. Default is `nil`. + # Only applies when using old semantic conventions. Deprecated with no replacement. # # ### `:propagator` # From 35d406f7f5dc151a994a521570a0692f35641b98 Mon Sep 17 00:00:00 2001 From: Hannah Ramadan Date: Tue, 21 Apr 2026 15:58:37 -0700 Subject: [PATCH 22/23] rubocop and coverage --- instrumentation/trilogy/.simplecov | 16 ++++++++++++++++ .../instrumentation/trilogy/instrumentation.rb | 2 +- .../trilogy/patches/dup/instrumentation_test.rb | 12 ++++++------ .../patches/stable/instrumentation_test.rb | 12 ++++++------ 4 files changed, 29 insertions(+), 13 deletions(-) create mode 100644 instrumentation/trilogy/.simplecov diff --git a/instrumentation/trilogy/.simplecov b/instrumentation/trilogy/.simplecov new file mode 100644 index 0000000000..20df6c23dc --- /dev/null +++ b/instrumentation/trilogy/.simplecov @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'digest' + +digest = Digest::MD5.new +digest.update('test') +digest.update(ENV.fetch('BUNDLE_GEMFILE', 'gemfile')) + +ENV['ENABLE_COVERAGE'] ||= '1' + +if ENV['ENABLE_COVERAGE'].to_i.positive? + SimpleCov.command_name(digest.hexdigest) + SimpleCov.start do + add_filter %r{^/test/} + end +end diff --git a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/instrumentation.rb b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/instrumentation.rb index 5ec58b86d1..c2acaf113e 100644 --- a/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/instrumentation.rb +++ b/instrumentation/trilogy/lib/opentelemetry/instrumentation/trilogy/instrumentation.rb @@ -33,7 +33,7 @@ module Trilogy # ### `:peer_service` # # Sets the `peer.service` attribute on spans. Default is `nil`. - # Only applies when using old semantic conventions. Deprecated with no replacement. + # Only applies when using old semantic conventions. Deprecated with no replacement. # # ### `:propagator` # diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb index 67d513212d..ae21d7eb46 100644 --- a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/dup/instrumentation_test.rb @@ -481,19 +481,19 @@ it 'does inject context on frozen strings' do sql = 'SELECT * from users where users.id = 1 and users.email = "test@test.com"' - assert(sql.frozen?) + assert_predicate(sql, :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] - assert(args[0].frozen?) + assert_predicate(args[0], :frozen?) m.call(args[0]) end allow(propagator).to receive(:inject).and_wrap_original do |m, *args| arg_cache[:inject_input] = args[0] - refute(args[0].frozen?) + refute_predicate(args[0], :frozen?) assert_match(sql, args[0]) m.call(args[0], context: args[1][:context]) end @@ -507,13 +507,13 @@ assert_equal(arg_cache[:inject_input], "/*VT_SPAN_CONTEXT=#{encoded}*/#{sql}") # arg_cache[:inject_input] is now frozen - assert(arg_cache[:inject_input].frozen?) + assert_predicate(arg_cache[:inject_input], :frozen?) end it 'does inject 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"' - refute(sql.frozen?) + refute_predicate(sql, :frozen?) # dup sql for comparison purposes, since propagator mutates it cached_sql = sql.dup @@ -524,7 +524,7 @@ encoded = Base64.strict_encode64("{\"uber-trace-id\":\"#{span.hex_trace_id}:#{span.hex_span_id}:0:1\"}") assert_equal(sql, "/*VT_SPAN_CONTEXT=#{encoded}*/#{cached_sql}") - refute(sql.frozen?) + refute_predicate(sql, :frozen?) end end diff --git a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb index f37299d7d5..20cbc309d5 100644 --- a/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb +++ b/instrumentation/trilogy/test/opentelemetry/instrumentation/trilogy/patches/stable/instrumentation_test.rb @@ -376,19 +376,19 @@ it 'does inject context on frozen strings' do sql = 'SELECT * from users where users.id = 1 and users.email = "test@test.com"' - assert(sql.frozen?) + assert_predicate(sql, :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] - assert(args[0].frozen?) + assert_predicate(args[0], :frozen?) m.call(args[0]) end allow(propagator).to receive(:inject).and_wrap_original do |m, *args| arg_cache[:inject_input] = args[0] - refute(args[0].frozen?) + refute_predicate(args[0], :frozen?) assert_match(sql, args[0]) m.call(args[0], context: args[1][:context]) end @@ -402,13 +402,13 @@ assert_equal(arg_cache[:inject_input], "/*VT_SPAN_CONTEXT=#{encoded}*/#{sql}") # arg_cache[:inject_input] is now frozen - assert(arg_cache[:inject_input].frozen?) + assert_predicate(arg_cache[:inject_input], :frozen?) end it 'does inject 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"' - refute(sql.frozen?) + refute_predicate(sql, :frozen?) # dup sql for comparison purposes, since propagator mutates it cached_sql = sql.dup @@ -419,7 +419,7 @@ encoded = Base64.strict_encode64("{\"uber-trace-id\":\"#{span.hex_trace_id}:#{span.hex_span_id}:0:1\"}") assert_equal(sql, "/*VT_SPAN_CONTEXT=#{encoded}*/#{cached_sql}") - refute(sql.frozen?) + refute_predicate(sql, :frozen?) end end From 0d0eefccc92310360d4fe0b664699bb3664d027f Mon Sep 17 00:00:00 2001 From: Hannah Ramadan Date: Mon, 27 Apr 2026 14:10:27 -0700 Subject: [PATCH 23/23] Remove .simplecov overwrite --- instrumentation/trilogy/.simplecov | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 instrumentation/trilogy/.simplecov diff --git a/instrumentation/trilogy/.simplecov b/instrumentation/trilogy/.simplecov deleted file mode 100644 index 20df6c23dc..0000000000 --- a/instrumentation/trilogy/.simplecov +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -require 'digest' - -digest = Digest::MD5.new -digest.update('test') -digest.update(ENV.fetch('BUNDLE_GEMFILE', 'gemfile')) - -ENV['ENABLE_COVERAGE'] ||= '1' - -if ENV['ENABLE_COVERAGE'].to_i.positive? - SimpleCov.command_name(digest.hexdigest) - SimpleCov.start do - add_filter %r{^/test/} - end -end