From 2088497a9cd97f2853f6a5551809705c00d9cc3b Mon Sep 17 00:00:00 2001 From: Hannah Ramadan Date: Wed, 18 Mar 2026 13:43:53 -0700 Subject: [PATCH 01/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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)