Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Add Mysql2 and Trilogy db.collection.name attribute #1109

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions instrumentation/mysql2/example/mysql2.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@
client.query("SELECT * from information_schema.INNODB_TABLES; /**Dé**/").each do |row|
puts row
end

client.query('CREATE TABLE test_table (id SERIAL PRIMARY KEY, name VARCHAR(50), age INT)')
client.query('DROP TABLE test_table')
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class Instrumentation < OpenTelemetry::Instrumentation::Base
option :db_statement, default: :obfuscate, validate: %I[omit include obfuscate]
option :span_name, default: :statement_type, validate: %I[statement_type db_name db_operation_and_name]
option :obfuscation_limit, default: 2000, validate: :integer
option :db_sql_table, default: :omit, validate: %I[omit include]

private

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ module Mysql2
module Patches
# Module to prepend to Mysql2::Client for instrumentation
module Client
# Capture the first word (including letters, digits, underscores, & '.', ) that follows common table commands
TABLE_NAME = /\b(?:(?:FROM|INTO|UPDATE)|(?:(?:CREATE|DROP|ALTER)\s+TABLE(?:\s+IF\s+(?:NOT\s+)?EXISTS)?))\s+["]?([\w.]+)["]?/i

def query(sql, options = {})
tracer.in_span(
_otel_span_name(sql),
Expand Down Expand Up @@ -47,15 +50,20 @@ def _otel_span_name(sql)
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
)
attributes = _otel_client_attributes(sql)

if sql
attributes[SemanticConventions::Trace::DB_SQL_TABLE] = db_sql_table_name(sql) if config[:db_sql_table] == :include

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
end

attributes.merge!(OpenTelemetry::Instrumentation::Mysql2.attributes)
Expand All @@ -68,7 +76,7 @@ def _otel_database_name
(query_options[:database] || query_options[:dbname] || query_options[:db])&.to_s
end

def _otel_client_attributes
def _otel_client_attributes(sql)
# 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
Expand All @@ -83,9 +91,16 @@ def _otel_client_attributes

attributes[SemanticConventions::Trace::DB_NAME] = _otel_database_name
attributes[SemanticConventions::Trace::PEER_SERVICE] = config[:peer_service]

attributes
end

def db_sql_table_name(sql)
Regexp.last_match(1) if sql =~ TABLE_NAME
rescue StandardError
hannahramadan marked this conversation as resolved.
Show resolved Hide resolved
nil
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If an error occurs and the attribute is set to nil, then the SDK will report an error. It would be best to avoid setting any invalid attributes.

Please ensure that the attribute is not set in cases where the table name could not be extracted.

https://github.com/open-telemetry/opentelemetry-ruby/blob/555b062ef9421784c132aa9b97b29ec637b13b0f/sdk/lib/opentelemetry/sdk/trace/span.rb#L277

https://github.com/open-telemetry/opentelemetry-ruby/blob/555b062ef9421784c132aa9b97b29ec637b13b0f/sdk/lib/opentelemetry/sdk/internal.rb#L57

Copy link
Contributor Author

@hannahramadan hannahramadan Oct 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made some changes! Will log an error if trouble getting the table name and won't report. Is the error logging here okay? 4af3e2c

end

def tracer
Mysql2::Instrumentation.instance.tracer
end
Expand Down
58 changes: 58 additions & 0 deletions instrumentation/mysql2/test/fixtures/sql_table_name.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
[
{
"name": "from",
"sql": "SELECT * FROM test_table"
},
{
"name": "select_count_from",
"sql": "SELECT COUNT(*) FROM test_table WHERE condition"
},
{
"name": "from_with_subquery",
"sql": "SELECT * FROM (SELECT * FROM test_table) AS table_alias"
},
{
"name": "insert_into",
"sql": "INSERT INTO test_table (column1, column2) VALUES (value1, value2)"
},
{
"name": "update",
"sql": "UPDATE test_table SET column1 = value1 WHERE condition"
},
{
"name": "delete_from",
"sql": "DELETE FROM test_table WHERE condition"
},
{
"name": "create_table",
"sql": "CREATE TABLE test_table (column1 datatype, column2 datatype)"
},
{
"name": "create_table_if_not_exists",
"sql": "CREATE TABLE IF NOT EXISTS test_table (column1 datatype, column2 datatype)"
},
{
"name": "alter_table",
"sql": "ALTER TABLE test_table ADD column_name datatype"
},
{
"name": "drop_table",
"sql": "DROP TABLE test_table"
},
{
"name": "drop_table_if_exists",
"sql": "DROP TABLE IF EXISTS test_table"
},
{
"name": "insert_into",
"sql": "INSERT INTO test_table values('', 'a''b c',0, 1 , 'd''e f''s h')"
},
{
"name": "from_with_join",
"sql": "SELECT columns FROM test_table JOIN table2 ON test_table.column = table2.column"
},
{
"name": "table_name_with_double_quotes",
"sql": "SELECT columns FROM \"test_table\""
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
# 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 }
Expand Down Expand Up @@ -473,6 +473,24 @@
end
end
end

describe '#connection_name' do
def self.load_fixture
data = File.read("#{Dir.pwd}/test/fixtures/sql_table_name.json")
JSON.parse(data)
end

load_fixture.each do |test_case|
name = test_case['name']
query = test_case['sql']

it "returns the table name for #{name}" do
table_name = client.send(:db_sql_table_name, query)

expect(table_name).must_equal('test_table')
end
end
end
end
end unless ENV['OMIT_SERVICES']
end
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ module Patches
# Module to prepend to PG::Connection for instrumentation
module Connection # rubocop:disable Metrics/ModuleLength
# Capture the first word (including letters, digits, underscores, & '.', ) that follows common table commands
TABLE_NAME = /\b(?:FROM|INTO|UPDATE|CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?|DROP\s+TABLE(?:\s+IF\s+EXISTS)?|ALTER\s+TABLE(?:\s+IF\s+EXISTS)?)\s+([\w\.]+)/i
TABLE_NAME = /\b(?:(?:FROM|INTO|UPDATE)|(?:(?:CREATE|DROP|ALTER)\s+TABLE(?:\s+IF\s+(?:NOT\s+)?EXISTS)?))\s+["']?([\w.]+)["']?/i

PG::Constants::EXEC_ISH_METHODS.each do |method|
define_method method do |*args, &block|
Expand Down Expand Up @@ -132,7 +132,7 @@ def validated_operation(operation)
end

def collection_name(text)
text.scan(TABLE_NAME).flatten[0]
Regexp.last_match(1) if text =~ TABLE_NAME
rescue StandardError
nil
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class Instrumentation < OpenTelemetry::Instrumentation::Base
option :span_name, default: :statement_type, validate: %I[statement_type db_name db_operation_and_name]
option :obfuscation_limit, default: 2000, validate: :integer
option :propagator, default: nil, validate: :string
option :db_sql_table, default: :omit, validate: %I[omit include]

attr_reader :propagator

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ module Trilogy
module Patches
# Module to prepend to Trilogy for instrumentation
module Client
# Capture the first word (including letters, digits, underscores, & '.', ) that follows common table commands
TABLE_NAME = /\b(?:(?:FROM|INTO|UPDATE)|(?:(?:CREATE|DROP|ALTER)\s+TABLE(?:\s+IF\s+(?:NOT\s+)?EXISTS)?))\s+["]?([\w.]+)["]?/i

def initialize(options = {})
@connection_options = options # This is normally done by Trilogy#initialize

Expand Down Expand Up @@ -76,6 +79,9 @@ def client_attributes(sql = nil)
attributes['db.instance.id'] = @connected_host unless @connected_host.nil?

if sql
sql_table_name = db_sql_table_name(sql)
attributes[SemanticConventions::Trace::DB_SQL_TABLE] = sql_table_name if sql_table_name && config[:db_sql_table] == :include

case config[:db_statement]
when :obfuscate
attributes[::OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT] =
Expand All @@ -88,6 +94,12 @@ def client_attributes(sql = nil)
attributes
end

def db_sql_table_name(sql)
Regexp.last_match(1) if sql =~ TABLE_NAME
rescue StandardError
nil
end

def database_name
connection_options[:database]
end
Expand Down
58 changes: 58 additions & 0 deletions instrumentation/trilogy/test/fixtures/sql_table_name.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
[
{
"name": "from",
"sql": "SELECT * FROM test_table"
},
{
"name": "select_count_from",
"sql": "SELECT COUNT(*) FROM test_table WHERE condition"
},
{
"name": "from_with_subquery",
"sql": "SELECT * FROM (SELECT * FROM test_table) AS table_alias"
},
{
"name": "insert_into",
"sql": "INSERT INTO test_table (column1, column2) VALUES (value1, value2)"
},
{
"name": "update",
"sql": "UPDATE test_table SET column1 = value1 WHERE condition"
},
{
"name": "delete_from",
"sql": "DELETE FROM test_table WHERE condition"
},
{
"name": "create_table",
"sql": "CREATE TABLE test_table (column1 datatype, column2 datatype)"
},
{
"name": "create_table_if_not_exists",
"sql": "CREATE TABLE IF NOT EXISTS test_table (column1 datatype, column2 datatype)"
},
{
"name": "alter_table",
"sql": "ALTER TABLE test_table ADD column_name datatype"
},
{
"name": "drop_table",
"sql": "DROP TABLE test_table"
},
{
"name": "drop_table_if_exists",
"sql": "DROP TABLE IF EXISTS test_table"
},
{
"name": "insert_into",
"sql": "INSERT INTO test_table values('', 'a''b c',0, 1 , 'd''e f''s h')"
},
{
"name": "from_with_join",
"sql": "SELECT columns FROM test_table JOIN table2 ON test_table.column = table2.column"
},
{
"name": "table_name_with_double_quotes",
"sql": "SELECT columns FROM \"test_table\""
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@
require_relative '../../../../lib/opentelemetry/instrumentation/trilogy'
require_relative '../../../../lib/opentelemetry/instrumentation/trilogy/patches/client'

# This test suite requires a running mysql service within a dedicated test container. We can use the same
# docker-compose service as the mysql2 instrumentation tests. The test container should run the mysql client.
# To run tests locally:
# 1. Build the opentelemetry/opentelemetry-ruby-contrib image
# - docker-compose build
# 2. Open a bash shell in the test container and cd to the trilogy directory
# - docker-compose run ex-instrumentation-mysql2-test bash -c 'cd ../trilogy && bash'
# 3. Bundle install and run tests with the Appraisals gem
# - bundle exec appraisal install && bundle exec appraisal rake test

describe OpenTelemetry::Instrumentation::Trilogy do
let(:instrumentation) { OpenTelemetry::Instrumentation::Trilogy::Instrumentation.instance }
let(:exporter) { EXPORTER }
Expand Down Expand Up @@ -626,5 +636,23 @@
end
end
end

describe '#connection_name' do
def self.load_fixture
data = File.read("#{Dir.pwd}/test/fixtures/sql_table_name.json")
JSON.parse(data)
end

load_fixture.each do |test_case|
name = test_case['name']
query = test_case['sql']

it "returns the table name for #{name}" do
table_name = client.send(:db_sql_table_name, query)

expect(table_name).must_equal('test_table')
end
end
end
end
end
Loading