diff --git a/instrumentation/mysql2/Appraisals b/instrumentation/mysql2/Appraisals index 71b5ab2255..bf45dbf52d 100644 --- a/instrumentation/mysql2/Appraisals +++ b/instrumentation/mysql2/Appraisals @@ -1,5 +1,13 @@ # frozen_string_literal: true -appraise 'mysql2-0.4.0' do - gem 'mysql2' +# 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 |stability| + appraise "mysql2-0.4.0-#{stability}" do + gem 'mysql2' + end end diff --git a/instrumentation/mysql2/README.md b/instrumentation/mysql2/README.md index 9c25a103c8..6a57e92f81 100644 --- a/instrumentation/mysql2/README.md +++ b/instrumentation/mysql2/README.md @@ -75,3 +75,19 @@ The `opentelemetry-instrumentation-mysql2` gem is distributed under the Apache 2 [community-meetings]: https://github.com/open-telemetry/community#community-meetings [slack-channel]: https://cloud-native.slack.com/archives/C01NWKKMKMY [discussions-url]: https://github.com/open-telemetry/opentelemetry-ruby/discussions + +## Database semantic convention stability + +In the OpenTelemetry ecosystem, database semantic conventions have now reached a stable state. However, the initial Mysql2 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, Mysql2 instrumentation code comes in three patch versions: `dup`, `old`, and `stable`. These versions are identical except for the attributes they send. Any changes to Mysql2 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/). diff --git a/instrumentation/mysql2/lib/opentelemetry/instrumentation/mysql2/instrumentation.rb b/instrumentation/mysql2/lib/opentelemetry/instrumentation/mysql2/instrumentation.rb index 9a7b78ccbc..7f1ff815e3 100644 --- a/instrumentation/mysql2/lib/opentelemetry/instrumentation/mysql2/instrumentation.rb +++ b/instrumentation/mysql2/lib/opentelemetry/instrumentation/mysql2/instrumentation.rb @@ -11,8 +11,9 @@ module Mysql2 # instrumentation class Instrumentation < OpenTelemetry::Instrumentation::Base install do |_config| - require_dependencies - patch_client + patch_type = determine_semconv + send(:"require_dependencies_#{patch_type}") + send(:"patch_client_#{patch_type}") end present do @@ -26,12 +27,41 @@ class Instrumentation < OpenTelemetry::Instrumentation::Base private - def require_dependencies - require_relative 'patches/client' + def determine_semconv + stability_opt_in = ENV.fetch('OTEL_SEMCONV_STABILITY_OPT_IN', '') + values = stability_opt_in.split(',').map(&:strip) + + if values.include?('database/dup') + 'dup' + elsif values.include?('database') + 'stable' + else + 'old' + end + end + + def require_dependencies_dup + require_relative 'patches/dup/client' + end + + def require_dependencies_old + require_relative 'patches/old/client' + end + + def require_dependencies_stable + require_relative 'patches/stable/client' + end + + def patch_client_dup + ::Mysql2::Client.prepend(Patches::Dup::Client) + end + + def patch_client_old + ::Mysql2::Client.prepend(Patches::Old::Client) end - def patch_client - ::Mysql2::Client.prepend(Patches::Client) + def patch_client_stable + ::Mysql2::Client.prepend(Patches::Stable::Client) end end end diff --git a/instrumentation/mysql2/lib/opentelemetry/instrumentation/mysql2/patches/client.rb b/instrumentation/mysql2/lib/opentelemetry/instrumentation/mysql2/patches/client.rb deleted file mode 100644 index c9dbdaddd9..0000000000 --- a/instrumentation/mysql2/lib/opentelemetry/instrumentation/mysql2/patches/client.rb +++ /dev/null @@ -1,100 +0,0 @@ -# frozen_string_literal: true - -# Copyright The OpenTelemetry Authors -# -# SPDX-License-Identifier: Apache-2.0 - -require 'opentelemetry-helpers-mysql' -require 'opentelemetry-helpers-sql-obfuscation' - -module OpenTelemetry - module Instrumentation - module Mysql2 - module Patches - # Module to prepend to Mysql2::Client for instrumentation - module Client - def query(sql, options = {}) - tracer.in_span( - _otel_span_name(sql), - attributes: _otel_span_attributes(sql), - kind: :client - ) do - super - end - end - - def prepare(sql) - tracer.in_span( - _otel_span_name(sql), - attributes: _otel_span_attributes(sql), - kind: :client - ) do - super - end - end - - private - - def _otel_span_name(sql) - OpenTelemetry::Helpers::MySQL.database_span_name( - sql, - OpenTelemetry::Instrumentation::Mysql2.attributes[ - SemanticConventions::Trace::DB_OPERATION - ], - _otel_database_name, - config - ) - end - - def _otel_span_attributes(sql) - attributes = _otel_client_attributes - case config[:db_statement] - when :include - attributes[SemanticConventions::Trace::DB_STATEMENT] = sql - when :obfuscate - attributes[SemanticConventions::Trace::DB_STATEMENT] = - OpenTelemetry::Helpers::SqlObfuscation.obfuscate_sql( - sql, obfuscation_limit: config[:obfuscation_limit], adapter: :mysql - ) - end - - attributes.merge!(OpenTelemetry::Instrumentation::Mysql2.attributes) - attributes.compact! - attributes - end - - def _otel_database_name - # https://github.com/brianmario/mysql2/blob/ca08712c6c8ea672df658bb25b931fea22555f27/lib/mysql2/client.rb#L78 - (query_options[:database] || query_options[:dbname] || query_options[:db])&.to_s - end - - def _otel_client_attributes - # The client specific attributes can be found via the query_options instance variable - # exposed on the mysql2 Client - # https://github.com/brianmario/mysql2/blob/ca08712c6c8ea672df658bb25b931fea22555f27/lib/mysql2/client.rb#L25-L26 - host = (query_options[:host] || query_options[:hostname]).to_s - port = query_options[:port].to_s - - attributes = { - SemanticConventions::Trace::DB_SYSTEM => 'mysql', - SemanticConventions::Trace::NET_PEER_NAME => host, - SemanticConventions::Trace::NET_PEER_PORT => port - } - - attributes[SemanticConventions::Trace::DB_NAME] = _otel_database_name - attributes[SemanticConventions::Trace::PEER_SERVICE] = config[:peer_service] - attributes - end - - def tracer - Mysql2::Instrumentation.instance.tracer - end - - def config - Mysql2::Instrumentation.instance.config - end - end - end - end - end -end diff --git a/instrumentation/mysql2/lib/opentelemetry/instrumentation/mysql2/patches/dup/client.rb b/instrumentation/mysql2/lib/opentelemetry/instrumentation/mysql2/patches/dup/client.rb new file mode 100644 index 0000000000..68a63f4fb3 --- /dev/null +++ b/instrumentation/mysql2/lib/opentelemetry/instrumentation/mysql2/patches/dup/client.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'opentelemetry-helpers-mysql' +require 'opentelemetry-helpers-sql-obfuscation' + +module OpenTelemetry + module Instrumentation + module Mysql2 + module Patches + module Dup + # Module to prepend to Mysql2::Client for instrumentation + module Client + def query(sql, options = {}) + tracer.in_span( + _otel_span_name(sql), + attributes: _otel_span_attributes(sql), + kind: :client + ) do + super + end + end + + def prepare(sql) + tracer.in_span( + _otel_span_name(sql), + attributes: _otel_span_attributes(sql), + kind: :client + ) do + super + end + end + + private + + def _otel_span_name(sql) + OpenTelemetry::Helpers::MySQL.database_span_name( + sql, + OpenTelemetry::Instrumentation::Mysql2.attributes[ + 'db.operation.name' + ], + _otel_database_name, + config + ) + end + + def _otel_span_attributes(sql) + attributes = _otel_client_attributes + case config[:db_statement] + when :include + attributes[SemanticConventions::Trace::DB_STATEMENT] = sql + attributes['db.query.text'] = sql + when :obfuscate + attributes[SemanticConventions::Trace::DB_STATEMENT] = + OpenTelemetry::Helpers::SqlObfuscation.obfuscate_sql( + sql, obfuscation_limit: config[:obfuscation_limit], adapter: :mysql + ) + attributes['db.query.text'] = + OpenTelemetry::Helpers::SqlObfuscation.obfuscate_sql( + sql, obfuscation_limit: config[:obfuscation_limit], adapter: :mysql + ) + end + + attributes.merge!(OpenTelemetry::Instrumentation::Mysql2.attributes) + attributes.compact! + attributes + end + + def _otel_database_name + # https://github.com/brianmario/mysql2/blob/ca08712c6c8ea672df658bb25b931fea22555f27/lib/mysql2/client.rb#L78 + (query_options[:database] || query_options[:dbname] || query_options[:db])&.to_s + end + + def _otel_client_attributes + # The client specific attributes can be found via the query_options instance variable + # exposed on the mysql2 Client + # https://github.com/brianmario/mysql2/blob/ca08712c6c8ea672df658bb25b931fea22555f27/lib/mysql2/client.rb#L25-L26 + host = (query_options[:host] || query_options[:hostname]).to_s + port = query_options[:port].to_s + + attributes = { + SemanticConventions::Trace::DB_SYSTEM => 'mysql', + SemanticConventions::Trace::NET_PEER_NAME => host, + SemanticConventions::Trace::NET_PEER_PORT => port, + 'db.system.name' => 'mysql', + 'server.address' => host, + 'server.port' => port + } + + attributes[SemanticConventions::Trace::DB_NAME] = _otel_database_name + attributes['db.namespace'] = _otel_database_name + attributes[SemanticConventions::Trace::PEER_SERVICE] = config[:peer_service] + attributes + end + + def tracer + Mysql2::Instrumentation.instance.tracer + end + + def config + Mysql2::Instrumentation.instance.config + end + end + end + end + end + end +end diff --git a/instrumentation/mysql2/lib/opentelemetry/instrumentation/mysql2/patches/old/client.rb b/instrumentation/mysql2/lib/opentelemetry/instrumentation/mysql2/patches/old/client.rb new file mode 100644 index 0000000000..a8ced8a5f7 --- /dev/null +++ b/instrumentation/mysql2/lib/opentelemetry/instrumentation/mysql2/patches/old/client.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'opentelemetry-helpers-mysql' +require 'opentelemetry-helpers-sql-obfuscation' + +module OpenTelemetry + module Instrumentation + module Mysql2 + module Patches + module Old + # Module to prepend to Mysql2::Client for instrumentation + module Client + def query(sql, options = {}) + tracer.in_span( + _otel_span_name(sql), + attributes: _otel_span_attributes(sql), + kind: :client + ) do + super + end + end + + def prepare(sql) + tracer.in_span( + _otel_span_name(sql), + attributes: _otel_span_attributes(sql), + kind: :client + ) do + super + end + end + + private + + def _otel_span_name(sql) + OpenTelemetry::Helpers::MySQL.database_span_name( + sql, + OpenTelemetry::Instrumentation::Mysql2.attributes[ + SemanticConventions::Trace::DB_OPERATION + ], + _otel_database_name, + config + ) + end + + def _otel_span_attributes(sql) + attributes = _otel_client_attributes + case config[:db_statement] + when :include + attributes[SemanticConventions::Trace::DB_STATEMENT] = sql + when :obfuscate + attributes[SemanticConventions::Trace::DB_STATEMENT] = + OpenTelemetry::Helpers::SqlObfuscation.obfuscate_sql( + sql, obfuscation_limit: config[:obfuscation_limit], adapter: :mysql + ) + end + + attributes.merge!(OpenTelemetry::Instrumentation::Mysql2.attributes) + attributes.compact! + attributes + end + + def _otel_database_name + # https://github.com/brianmario/mysql2/blob/ca08712c6c8ea672df658bb25b931fea22555f27/lib/mysql2/client.rb#L78 + (query_options[:database] || query_options[:dbname] || query_options[:db])&.to_s + end + + def _otel_client_attributes + # The client specific attributes can be found via the query_options instance variable + # exposed on the mysql2 Client + # https://github.com/brianmario/mysql2/blob/ca08712c6c8ea672df658bb25b931fea22555f27/lib/mysql2/client.rb#L25-L26 + host = (query_options[:host] || query_options[:hostname]).to_s + port = query_options[:port].to_s + + attributes = { + SemanticConventions::Trace::DB_SYSTEM => 'mysql', + SemanticConventions::Trace::NET_PEER_NAME => host, + SemanticConventions::Trace::NET_PEER_PORT => port + } + + attributes[SemanticConventions::Trace::DB_NAME] = _otel_database_name + attributes[SemanticConventions::Trace::PEER_SERVICE] = config[:peer_service] + attributes + end + + def tracer + Mysql2::Instrumentation.instance.tracer + end + + def config + Mysql2::Instrumentation.instance.config + end + end + end + end + end + end +end diff --git a/instrumentation/mysql2/lib/opentelemetry/instrumentation/mysql2/patches/stable/client.rb b/instrumentation/mysql2/lib/opentelemetry/instrumentation/mysql2/patches/stable/client.rb new file mode 100644 index 0000000000..893647dcf5 --- /dev/null +++ b/instrumentation/mysql2/lib/opentelemetry/instrumentation/mysql2/patches/stable/client.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'opentelemetry-helpers-mysql' +require 'opentelemetry-helpers-sql-obfuscation' + +module OpenTelemetry + module Instrumentation + module Mysql2 + module Patches + module Stable + # Module to prepend to Mysql2::Client for instrumentation + module Client + def query(sql, options = {}) + tracer.in_span( + _otel_span_name(sql), + attributes: _otel_span_attributes(sql), + kind: :client + ) do + super + end + end + + def prepare(sql) + tracer.in_span( + _otel_span_name(sql), + attributes: _otel_span_attributes(sql), + kind: :client + ) do + super + end + end + + private + + def _otel_span_name(sql) + OpenTelemetry::Helpers::MySQL.database_span_name( + sql, + OpenTelemetry::Instrumentation::Mysql2.attributes[ + 'db.operation.name' + ], + _otel_database_name, + config + ) + end + + def _otel_span_attributes(sql) + attributes = _otel_client_attributes + case config[:db_statement] + when :include + attributes['db.query.text'] = sql + when :obfuscate + attributes['db.query.text'] = + OpenTelemetry::Helpers::SqlObfuscation.obfuscate_sql( + sql, obfuscation_limit: config[:obfuscation_limit], adapter: :mysql + ) + end + + attributes.merge!(OpenTelemetry::Instrumentation::Mysql2.attributes) + attributes.compact! + attributes + end + + def _otel_database_name + # https://github.com/brianmario/mysql2/blob/ca08712c6c8ea672df658bb25b931fea22555f27/lib/mysql2/client.rb#L78 + (query_options[:database] || query_options[:dbname] || query_options[:db])&.to_s + end + + def _otel_client_attributes + # The client specific attributes can be found via the query_options instance variable + # exposed on the mysql2 Client + # https://github.com/brianmario/mysql2/blob/ca08712c6c8ea672df658bb25b931fea22555f27/lib/mysql2/client.rb#L25-L26 + host = (query_options[:host] || query_options[:hostname]).to_s + port = query_options[:port].to_s + + attributes = { + 'db.system.name' => 'mysql', + 'server.address' => host, + 'server.port' => port + } + + attributes['db.namespace'] = _otel_database_name + attributes[SemanticConventions::Trace::PEER_SERVICE] = config[:peer_service] if config[:peer_service] + attributes + end + + def tracer + Mysql2::Instrumentation.instance.tracer + end + + def config + Mysql2::Instrumentation.instance.config + end + end + end + end + end + end +end diff --git a/instrumentation/mysql2/test/opentelemetry/instrumentation/mysql2/dup/instrumentation_test.rb b/instrumentation/mysql2/test/opentelemetry/instrumentation/mysql2/dup/instrumentation_test.rb new file mode 100644 index 0000000000..49a123c622 --- /dev/null +++ b/instrumentation/mysql2/test/opentelemetry/instrumentation/mysql2/dup/instrumentation_test.rb @@ -0,0 +1,534 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' +require 'mysql2' + +require_relative '../../../../../lib/opentelemetry/instrumentation/mysql2' +require_relative '../../../../../lib/opentelemetry/instrumentation/mysql2/patches/dup/client' + +# This test suite requires a running mysql container and dedicated test container +# To run tests: +# 1. Build the opentelemetry/opentelemetry-ruby-contrib image +# - docker-compose build +# 2. Bundle install +# - docker-compose run ex-instrumentation-mysql2-test bundle exec appraisal install +# 3. Run test suite +# - docker-compose run ex-instrumentation-mysql2-test bundle exec appraisal rake test +describe OpenTelemetry::Instrumentation::Mysql2::Instrumentation do + let(:instrumentation) { OpenTelemetry::Instrumentation::Mysql2::Instrumentation.instance } + let(:exporter) { EXPORTER } + let(:span) { exporter.finished_spans.first } + let(:config) { { db_statement: :include } } + + before do + skip unless ENV['BUNDLE_GEMFILE'].include?('dup') + + ENV['OTEL_SEMCONV_STABILITY_OPT_IN'] = 'database/dup' + + exporter.reset + end + + after do + # Force re-install of instrumentation + instrumentation.instance_variable_set(:@installed, false) + end + + describe 'tracing' do + let(:client) do + Mysql2::Client.new( + host: host, + port: port, + database: database, + username: username, + password: password + ) + end + + let(:host) { ENV.fetch('TEST_MYSQL_HOST', '127.0.0.1') } + let(:port) { ENV.fetch('TEST_MYSQL_PORT', '3306') } + 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 + instrumentation.install(config) + end + + it 'before request' do + _(exporter.finished_spans.size).must_equal 0 + end + + 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['peer.service']).must_equal 'readonly:mysql' + end + + describe '.attributes' do + let(:attributes) { { 'db.statement' => 'foobar', 'db.query.text' => 'foobar' } } + + it 'returns an empty hash by default' do + _(OpenTelemetry::Instrumentation::Mysql2.attributes).must_equal({}) + end + + it 'returns the current attributes hash' do + OpenTelemetry::Instrumentation::Mysql2.with_attributes(attributes) do + _(OpenTelemetry::Instrumentation::Mysql2.attributes).must_equal(attributes) + end + end + + it 'sets span attributes according to with_attributes hash' do + OpenTelemetry::Instrumentation::Mysql2.with_attributes(attributes) do + client.query('SELECT 1') + end + + _(span.attributes['db.statement']).must_equal 'foobar' + _(span.attributes['db.query.text']).must_equal 'foobar' + end + end + + describe 'prepare statement' do + it 'after requests with prepare' do + client.prepare('SELECT 1') + + _(span.name).must_equal 'select' + _(span.attributes['db.system']).must_equal 'mysql' + _(span.attributes['db.name']).must_equal 'mysql' + _(span.attributes['db.statement']).must_equal 'SELECT 1' + _(span.attributes['net.peer.name']).must_equal host.to_s + _(span.attributes['net.peer.port']).must_equal port.to_s + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.namespace']).must_equal 'mysql' + _(span.attributes['db.query.text']).must_equal 'SELECT 1' + _(span.attributes['server.address']).must_equal host.to_s + _(span.attributes['server.port']).must_equal port.to_s + end + + it 'after requests with prepare select ?' do + client.prepare('SELECT ?') + + _(span.name).must_equal 'select' + _(span.attributes['db.system']).must_equal 'mysql' + _(span.attributes['db.name']).must_equal 'mysql' + _(span.attributes['db.statement']).must_equal 'SELECT ?' + _(span.attributes['net.peer.name']).must_equal host.to_s + _(span.attributes['net.peer.port']).must_equal port.to_s + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.namespace']).must_equal 'mysql' + _(span.attributes['db.query.text']).must_equal 'SELECT ?' + _(span.attributes['server.address']).must_equal host.to_s + _(span.attributes['server.port']).must_equal port.to_s + end + + it 'query ? sequences for db.statement with prepare' do + sql = 'SELECT * from users where users.id = ? and users.email = ?' + expect do + client.prepare(sql) + end.must_raise Mysql2::Error + + _(span.attributes['db.system']).must_equal 'mysql' + _(span.attributes['db.name']).must_equal 'mysql' + _(span.attributes['db.namespace']).must_equal 'mysql' + _(span.name).must_equal 'select' + _(span.attributes['db.statement']).must_equal sql + _(span.attributes['net.peer.name']).must_equal host.to_s + _(span.attributes['net.peer.port']).must_equal port.to_s + _(span.attributes['db.query.text']).must_equal sql + _(span.attributes['server.address']).must_equal host.to_s + _(span.attributes['server.port']).must_equal port.to_s + end + + it 'query invalid byte sequences for db.statement without prepare' do + sql = 'SELECT * from users where users.id = ? and users.email = ?' + expect do + client.query(sql) + end.must_raise Mysql2::Error + + _(span.events[0].attributes['exception.message'].slice(0, 37)).must_equal 'You have an error in your SQL syntax;' + end + end + + it 'after requests' do + client.query('SELECT 1') + + _(span.name).must_equal 'select' + _(span.attributes['db.system']).must_equal 'mysql' + _(span.attributes['db.name']).must_equal 'mysql' + _(span.attributes['db.statement']).must_equal 'SELECT 1' + _(span.attributes['net.peer.name']).must_equal host.to_s + _(span.attributes['net.peer.port']).must_equal port.to_s + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.namespace']).must_equal 'mysql' + _(span.attributes['db.query.text']).must_equal 'SELECT 1' + _(span.attributes['server.address']).must_equal host.to_s + _(span.attributes['server.port']).must_equal port.to_s + end + + it 'after error' do + expect do + client.query('SELECT INVALID') + end.must_raise Mysql2::Error + + _(span.name).must_equal 'select' + _(span.attributes['db.system']).must_equal 'mysql' + _(span.attributes['db.name']).must_equal 'mysql' + _(span.attributes['db.statement']).must_equal 'SELECT INVALID' + _(span.attributes['net.peer.name']).must_equal host.to_s + _(span.attributes['net.peer.port']).must_equal port.to_s + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.namespace']).must_equal 'mysql' + _(span.attributes['db.query.text']).must_equal 'SELECT INVALID' + _(span.attributes['server.address']).must_equal host.to_s + _(span.attributes['server.port']).must_equal port.to_s + + _(span.status.code).must_equal( + OpenTelemetry::Trace::Status::ERROR + ) + _(span.events.first.name).must_equal 'exception' + _(span.events.first.attributes['exception.type']).must_equal 'Mysql2::Error' + assert(!span.events.first.attributes['exception.message'].nil?) + assert(!span.events.first.attributes['exception.stacktrace'].nil?) + end + + it 'extracts statement type that begins the query' do + base_sql = 'SELECT 1' + explain = 'EXPLAIN' + explain_sql = "#{explain} #{base_sql}" + client.query(explain_sql) + + _(span.name).must_equal 'explain' + _(span.attributes['db.system']).must_equal 'mysql' + _(span.attributes['db.name']).must_equal 'mysql' + _(span.attributes['db.statement']).must_equal explain_sql + _(span.attributes['net.peer.name']).must_equal host.to_s + _(span.attributes['net.peer.port']).must_equal port.to_s + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.namespace']).must_equal 'mysql' + _(span.attributes['db.query.text']).must_equal explain_sql + _(span.attributes['server.address']).must_equal host.to_s + _(span.attributes['server.port']).must_equal port.to_s + 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 Mysql2::Error + + _(span.name).must_equal 'mysql' + _(span.attributes['db.system']).must_equal 'mysql' + _(span.attributes['db.name']).must_equal 'mysql' + _(span.attributes['db.statement']).must_equal 'DESELECT 1' + _(span.attributes['net.peer.name']).must_equal host.to_s + _(span.attributes['net.peer.port']).must_equal port.to_s + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.namespace']).must_equal 'mysql' + _(span.attributes['db.query.text']).must_equal 'DESELECT 1' + _(span.attributes['server.address']).must_equal host.to_s + _(span.attributes['server.port']).must_equal port.to_s + + _(span.status.code).must_equal( + OpenTelemetry::Trace::Status::ERROR + ) + _(span.events.first.name).must_equal 'exception' + _(span.events.first.attributes['exception.type']).must_equal 'Mysql2::Error' + assert(!span.events.first.attributes['exception.message'].nil?) + assert(!span.events.first.attributes['exception.stacktrace'].nil?) + end + + describe 'when db_statement set as obfuscate' do + let(:config) { { db_statement: :obfuscate } } + + it 'obfuscates SQL parameters in db.statement' 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 Mysql2::Error + + _(span.attributes['db.system']).must_equal 'mysql' + _(span.attributes['db.name']).must_equal 'mysql' + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.namespace']).must_equal 'mysql' + _(span.name).must_equal 'select' + _(span.attributes['db.statement']).must_equal obfuscated_sql + _(span.attributes['net.peer.name']).must_equal host.to_s + _(span.attributes['net.peer.port']).must_equal port.to_s + _(span.attributes['db.query.text']).must_equal obfuscated_sql + _(span.attributes['server.address']).must_equal host.to_s + _(span.attributes['server.port']).must_equal port.to_s + end + + it 'encodes invalid byte sequences for db.statement' 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 Mysql2::Error + + _(span.name).must_equal 'select' + _(span.attributes['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 Mysql2::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 set as omit' do + let(:config) { { db_statement: :omit } } + + it 'omits db.statement attribute' do + sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'" + expect do + client.query(sql) + end.must_raise Mysql2::Error + + _(span.attributes['db.system']).must_equal 'mysql' + _(span.attributes['db.name']).must_equal 'mysql' + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.namespace']).must_equal 'mysql' + _(span.name).must_equal 'select' + _(span.attributes).wont_include('db.statement') + _(span.attributes['net.peer.name']).must_equal host.to_s + _(span.attributes['net.peer.port']).must_equal port.to_s + _(span.attributes).wont_include('db.query.text') + _(span.attributes['server.address']).must_equal host.to_s + _(span.attributes['server.port']).must_equal port.to_s + end + end + + describe 'when db_statement is configured via environment variable' do + describe 'when db_statement set as omit' do + it 'omits db.statement attribute' do + OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_MYSQL2_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 Mysql2::Error + + _(span.attributes['db.system']).must_equal 'mysql' + _(span.attributes['db.name']).must_equal 'mysql' + _(span.name).must_equal 'select' + _(span.attributes).wont_include('db.statement') + _(span.attributes['net.peer.name']).must_equal host.to_s + _(span.attributes['net.peer.port']).must_equal port.to_s + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.namespace']).must_equal 'mysql' + _(span.name).must_equal 'select' + _(span.attributes).wont_include('db.query.text') + _(span.attributes['server.address']).must_equal host.to_s + _(span.attributes['server.port']).must_equal port.to_s + end + end + end + + describe 'when db_statement set as obfuscate' do + it 'obfuscates SQL parameters in db.statement' do + OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_MYSQL2_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 Mysql2::Error + + _(span.attributes['db.system']).must_equal 'mysql' + _(span.attributes['db.name']).must_equal 'mysql' + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.namespace']).must_equal 'mysql' + _(span.name).must_equal 'select' + _(span.attributes['db.statement']).must_equal obfuscated_sql + _(span.attributes['net.peer.name']).must_equal host.to_s + _(span.attributes['net.peer.port']).must_equal port.to_s + _(span.attributes['db.query.text']).must_equal obfuscated_sql + _(span.attributes['server.address']).must_equal host.to_s + _(span.attributes['server.port']).must_equal port.to_s + 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.statement' do + OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_MYSQL2_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 Mysql2::Error + + _(span.attributes['db.system']).must_equal 'mysql' + _(span.attributes['db.name']).must_equal 'mysql' + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.namespace']).must_equal 'mysql' + _(span.name).must_equal 'select' + _(span.attributes['db.statement']).must_equal obfuscated_sql + _(span.attributes['net.peer.name']).must_equal host.to_s + _(span.attributes['net.peer.port']).must_equal port.to_s + _(span.attributes['db.query.text']).must_equal obfuscated_sql + _(span.attributes['server.address']).must_equal host.to_s + _(span.attributes['server.port']).must_equal port.to_s + 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_MYSQL2_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 Mysql2::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_MYSQL2_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 Mysql2::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_MYSQL2_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 Mysql2::Error + + _(span.name).must_equal 'mysql' # TODO: change the db name so we can distinguish it from the default + 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_MYSQL2_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 Mysql2::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_MYSQL2_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::Mysql2.with_attributes('db.operation.name' => 'foo') do + expect do + client.query(sql) + end.must_raise Mysql2::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_MYSQL2_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 Mysql2::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_MYSQL2_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::Mysql2.with_attributes('db.operation.name' => 'foo') do + expect do + client.query(sql) + end.must_raise Mysql2::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_MYSQL2_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 Mysql2::Error + + _(span.name).must_equal 'mysql' + end + end + end + end + end + end unless ENV['OMIT_SERVICES'] +end diff --git a/instrumentation/mysql2/test/opentelemetry/instrumentation/mysql2/instrumentation_test.rb b/instrumentation/mysql2/test/opentelemetry/instrumentation/mysql2/old/instrumentation_test.rb similarity index 98% rename from instrumentation/mysql2/test/opentelemetry/instrumentation/mysql2/instrumentation_test.rb rename to instrumentation/mysql2/test/opentelemetry/instrumentation/mysql2/old/instrumentation_test.rb index 405b714ee3..77190345f8 100644 --- a/instrumentation/mysql2/test/opentelemetry/instrumentation/mysql2/instrumentation_test.rb +++ b/instrumentation/mysql2/test/opentelemetry/instrumentation/mysql2/old/instrumentation_test.rb @@ -7,17 +7,17 @@ require 'test_helper' require 'mysql2' -require_relative '../../../../lib/opentelemetry/instrumentation/mysql2' -require_relative '../../../../lib/opentelemetry/instrumentation/mysql2/patches/client' +require_relative '../../../../../lib/opentelemetry/instrumentation/mysql2' +require_relative '../../../../../lib/opentelemetry/instrumentation/mysql2/patches/old/client' # This test suite requires a running mysql container and dedicated test container # To run tests: # 1. Build the opentelemetry/opentelemetry-ruby-contrib image # - docker-compose build # 2. Bundle install -# - docker-compose run ex-instrumentation-mysql2-test bundle install +# - docker-compose run ex-instrumentation-mysql2-test bundle exec appraisal install # 3. Run test suite -# - docker-compose run ex-instrumentation-mysql2-test bundle exec rake test +# - docker-compose run ex-instrumentation-mysql2-test bundle exec appraisal rake test describe OpenTelemetry::Instrumentation::Mysql2::Instrumentation do let(:instrumentation) { OpenTelemetry::Instrumentation::Mysql2::Instrumentation.instance } let(:exporter) { EXPORTER } @@ -25,6 +25,8 @@ let(:config) { { db_statement: :include } } before do + skip unless ENV['BUNDLE_GEMFILE'].include?('old') + exporter.reset end diff --git a/instrumentation/mysql2/test/opentelemetry/instrumentation/mysql2/stable/instrumentation_test.rb b/instrumentation/mysql2/test/opentelemetry/instrumentation/mysql2/stable/instrumentation_test.rb new file mode 100644 index 0000000000..279ba8b686 --- /dev/null +++ b/instrumentation/mysql2/test/opentelemetry/instrumentation/mysql2/stable/instrumentation_test.rb @@ -0,0 +1,471 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' +require 'mysql2' + +require_relative '../../../../../lib/opentelemetry/instrumentation/mysql2' +require_relative '../../../../../lib/opentelemetry/instrumentation/mysql2/patches/stable/client' + +# This test suite requires a running mysql container and dedicated test container +# To run tests: +# 1. Build the opentelemetry/opentelemetry-ruby-contrib image +# - docker-compose build +# 2. Bundle install +# - docker-compose run ex-instrumentation-mysql2-test bundle exec appraisal install +# 3. Run test suite +# - docker-compose run ex-instrumentation-mysql2-test bundle exec appraisal rake test +describe OpenTelemetry::Instrumentation::Mysql2::Instrumentation do + let(:instrumentation) { OpenTelemetry::Instrumentation::Mysql2::Instrumentation.instance } + let(:exporter) { EXPORTER } + let(:span) { exporter.finished_spans.first } + let(:config) { { db_statement: :include } } + + before do + skip unless ENV['BUNDLE_GEMFILE'].include?('stable') + + ENV['OTEL_SEMCONV_STABILITY_OPT_IN'] = 'database' + + exporter.reset + end + + after do + # Force re-install of instrumentation + instrumentation.instance_variable_set(:@installed, false) + end + + describe 'tracing' do + let(:client) do + Mysql2::Client.new( + host: host, + port: port, + database: database, + username: username, + password: password + ) + end + + let(:host) { ENV.fetch('TEST_MYSQL_HOST', '127.0.0.1') } + let(:port) { ENV.fetch('TEST_MYSQL_PORT', '3306') } + 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 + instrumentation.install(config) + end + + it 'before request' do + _(exporter.finished_spans.size).must_equal 0 + end + + 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['peer.service']).must_equal 'readonly:mysql' + end + + describe '.attributes' do + let(:attributes) { { 'db.query.text' => 'foobar' } } + + it 'returns an empty hash by default' do + _(OpenTelemetry::Instrumentation::Mysql2.attributes).must_equal({}) + end + + it 'returns the current attributes hash' do + OpenTelemetry::Instrumentation::Mysql2.with_attributes(attributes) do + _(OpenTelemetry::Instrumentation::Mysql2.attributes).must_equal(attributes) + end + end + + it 'sets span attributes according to with_attributes hash' do + OpenTelemetry::Instrumentation::Mysql2.with_attributes(attributes) do + client.query('SELECT 1') + end + + _(span.attributes['db.query.text']).must_equal 'foobar' + end + end + + describe 'prepare statement' do + it 'after requests with prepare' do + client.prepare('SELECT 1') + + _(span.name).must_equal 'select' + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.namespace']).must_equal 'mysql' + _(span.attributes['db.query.text']).must_equal 'SELECT 1' + _(span.attributes['server.address']).must_equal host.to_s + _(span.attributes['server.port']).must_equal port.to_s + end + + it 'after requests with prepare select ?' do + client.prepare('SELECT ?') + + _(span.name).must_equal 'select' + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.namespace']).must_equal 'mysql' + _(span.attributes['db.query.text']).must_equal 'SELECT ?' + _(span.attributes['server.address']).must_equal host.to_s + _(span.attributes['server.port']).must_equal port.to_s + end + + it 'query ? sequences for db.query.text with prepare' do + sql = 'SELECT * from users where users.id = ? and users.email = ?' + expect do + client.prepare(sql) + end.must_raise Mysql2::Error + + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.namespace']).must_equal 'mysql' + _(span.name).must_equal 'select' + _(span.attributes['db.query.text']).must_equal sql + _(span.attributes['server.address']).must_equal host.to_s + _(span.attributes['server.port']).must_equal port.to_s + end + + it 'query invalid byte sequences for db.query.text without prepare' do + sql = 'SELECT * from users where users.id = ? and users.email = ?' + expect do + client.query(sql) + end.must_raise Mysql2::Error + + _(span.events[0].attributes['exception.message'].slice(0, 37)).must_equal 'You have an error in your SQL syntax;' + end + end + + it 'after requests' do + client.query('SELECT 1') + + _(span.name).must_equal 'select' + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.namespace']).must_equal 'mysql' + _(span.attributes['db.query.text']).must_equal 'SELECT 1' + _(span.attributes['server.address']).must_equal host.to_s + _(span.attributes['server.port']).must_equal port.to_s + end + + it 'after error' do + expect do + client.query('SELECT INVALID') + end.must_raise Mysql2::Error + + _(span.name).must_equal 'select' + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.namespace']).must_equal 'mysql' + _(span.attributes['db.query.text']).must_equal 'SELECT INVALID' + _(span.attributes['server.address']).must_equal host.to_s + _(span.attributes['server.port']).must_equal port.to_s + + _(span.status.code).must_equal( + OpenTelemetry::Trace::Status::ERROR + ) + _(span.events.first.name).must_equal 'exception' + _(span.events.first.attributes['exception.type']).must_equal 'Mysql2::Error' + assert(!span.events.first.attributes['exception.message'].nil?) + assert(!span.events.first.attributes['exception.stacktrace'].nil?) + end + + it 'extracts statement type that begins the query' do + base_sql = 'SELECT 1' + explain = 'EXPLAIN' + explain_sql = "#{explain} #{base_sql}" + client.query(explain_sql) + + _(span.name).must_equal 'explain' + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.namespace']).must_equal 'mysql' + _(span.attributes['db.query.text']).must_equal explain_sql + _(span.attributes['server.address']).must_equal host.to_s + _(span.attributes['server.port']).must_equal port.to_s + 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 Mysql2::Error + + _(span.name).must_equal 'mysql' + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.namespace']).must_equal 'mysql' + _(span.attributes['db.query.text']).must_equal 'DESELECT 1' + _(span.attributes['server.address']).must_equal host.to_s + _(span.attributes['server.port']).must_equal port.to_s + + _(span.status.code).must_equal( + OpenTelemetry::Trace::Status::ERROR + ) + _(span.events.first.name).must_equal 'exception' + _(span.events.first.attributes['exception.type']).must_equal 'Mysql2::Error' + assert(!span.events.first.attributes['exception.message'].nil?) + assert(!span.events.first.attributes['exception.stacktrace'].nil?) + end + + describe 'when db_statement set as 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 Mysql2::Error + + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.namespace']).must_equal 'mysql' + _(span.name).must_equal 'select' + _(span.attributes['db.query.text']).must_equal obfuscated_sql + _(span.attributes['server.address']).must_equal host.to_s + _(span.attributes['server.port']).must_equal port.to_s + 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 Mysql2::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 Mysql2::Error + + _(span.attributes['db.query.text']).must_equal obfuscated_sql + end + end + end + + describe 'when db_statement set as omit' do + let(:config) { { db_statement: :omit } } + + it 'omits 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 Mysql2::Error + + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.namespace']).must_equal 'mysql' + _(span.name).must_equal 'select' + _(span.attributes).wont_include('db.query.text') + _(span.attributes['server.address']).must_equal host.to_s + _(span.attributes['server.port']).must_equal port.to_s + 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_MYSQL2_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 Mysql2::Error + + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.namespace']).must_equal 'mysql' + _(span.name).must_equal 'select' + _(span.attributes).wont_include('db.query.text') + _(span.attributes['server.address']).must_equal host.to_s + _(span.attributes['server.port']).must_equal port.to_s + 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_MYSQL2_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 Mysql2::Error + + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.namespace']).must_equal 'mysql' + _(span.name).must_equal 'select' + _(span.attributes['db.query.text']).must_equal obfuscated_sql + _(span.attributes['server.address']).must_equal host.to_s + _(span.attributes['server.port']).must_equal port.to_s + 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_MYSQL2_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 Mysql2::Error + + _(span.attributes['db.system.name']).must_equal 'mysql' + _(span.attributes['db.namespace']).must_equal 'mysql' + _(span.name).must_equal 'select' + _(span.attributes['db.query.text']).must_equal obfuscated_sql + _(span.attributes['server.address']).must_equal host.to_s + _(span.attributes['server.port']).must_equal port.to_s + 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_MYSQL2_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 Mysql2::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_MYSQL2_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 Mysql2::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_MYSQL2_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 Mysql2::Error + + _(span.name).must_equal 'mysql' # TODO: change the db name so we can distinguish it from the default + 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_MYSQL2_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 Mysql2::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_MYSQL2_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::Mysql2.with_attributes('db.operation.name' => 'foo') do + expect do + client.query(sql) + end.must_raise Mysql2::Error + end + + _(span.name).must_equal 'foo mysql' + end + end + + it 'sets span name to db name when db.operation.name is not set' do + OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_MYSQL2_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 Mysql2::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_MYSQL2_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::Mysql2.with_attributes('db.operation.name' => 'foo') do + expect do + client.query(sql) + end.must_raise Mysql2::Error + end + + _(span.name).must_equal 'foo' + end + end + + it 'sets span name to mysql when db.operation.name is not set' do + OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_INSTRUMENTATION_MYSQL2_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 Mysql2::Error + + _(span.name).must_equal 'mysql' + end + end + end + end + end + end unless ENV['OMIT_SERVICES'] +end