diff --git a/lib/solarwinds_apm.rb b/lib/solarwinds_apm.rb index 393c1a4b..04117e83 100644 --- a/lib/solarwinds_apm.rb +++ b/lib/solarwinds_apm.rb @@ -20,7 +20,7 @@ begin if RUBY_PLATFORM.include?('linux') - require 'solarwinds_apm/config' + require 'solarwinds_apm/config' # initialize is called at end of config.rb file require 'solarwinds_apm/oboe_init_options' # setup oboe reporter options if !SolarWindsAPM::OboeInitOptions.instance.service_key_ok? && !SolarWindsAPM::OboeInitOptions.instance.lambda_env SolarWindsAPM.logger.warn '==============================================================' @@ -60,7 +60,6 @@ require 'solarwinds_apm/api' require 'solarwinds_apm/support' require 'solarwinds_apm/opentelemetry' - require 'solarwinds_apm/patch' require 'solarwinds_apm/otel_config' if ENV['SW_APM_AUTO_CONFIGURE'] != 'false' diff --git a/lib/solarwinds_apm/opentelemetry/otlp_processor.rb b/lib/solarwinds_apm/opentelemetry/otlp_processor.rb index 951c2651..17286eae 100644 --- a/lib/solarwinds_apm/opentelemetry/otlp_processor.rb +++ b/lib/solarwinds_apm/opentelemetry/otlp_processor.rb @@ -22,7 +22,7 @@ def initialize # @param [Context] parent_context the # started span. def on_start(span, parent_context) - SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] processor on_start span: #{span.inspect}" } + SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] processor on_start span: #{span.to_span_data.inspect}" } return if non_entry_span(parent_context: parent_context) diff --git a/lib/solarwinds_apm/opentelemetry/solarwinds_processor.rb b/lib/solarwinds_apm/opentelemetry/solarwinds_processor.rb index 5b582a31..0355d695 100644 --- a/lib/solarwinds_apm/opentelemetry/solarwinds_processor.rb +++ b/lib/solarwinds_apm/opentelemetry/solarwinds_processor.rb @@ -30,7 +30,7 @@ def initialize(txn_manager) # started span. def on_start(span, parent_context) SolarWindsAPM.logger.debug do - "[#{self.class}/#{__method__}] processor on_start span: #{span.inspect}, parent_context: #{parent_context.inspect}" + "[#{self.class}/#{__method__}] processor on_start span: #{span.to_span_data.inspect}" end return if non_entry_span(parent_context: parent_context) @@ -47,7 +47,7 @@ def on_start(span, parent_context) # # @param [Span] span the {Span} that just ended. def on_finish(span) - SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] processor on_finish span: #{span.inspect}" } + SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] processor on_finish span: #{span.to_span_data.inspect}" } return if non_entry_span(span: span) diff --git a/lib/solarwinds_apm/opentelemetry/solarwinds_propagator.rb b/lib/solarwinds_apm/opentelemetry/solarwinds_propagator.rb index d77d6306..524fd4dc 100644 --- a/lib/solarwinds_apm/opentelemetry/solarwinds_propagator.rb +++ b/lib/solarwinds_apm/opentelemetry/solarwinds_propagator.rb @@ -54,7 +54,7 @@ def inject(carrier, context: ::OpenTelemetry::Context.current, SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] inject context: #{context.inspect}" } span_context = ::OpenTelemetry::Trace.current_span(context)&.context - SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] span_context #{span_context.inspect}" } + SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] span_context: #{span_context.inspect}" } return unless span_context&.valid? trace_flag = span_context.trace_flags.sampled? ? 1 : 0 diff --git a/lib/solarwinds_apm/opentelemetry/solarwinds_response_propagator.rb b/lib/solarwinds_apm/opentelemetry/solarwinds_response_propagator.rb index 589d6950..7e82debf 100644 --- a/lib/solarwinds_apm/opentelemetry/solarwinds_response_propagator.rb +++ b/lib/solarwinds_apm/opentelemetry/solarwinds_response_propagator.rb @@ -35,9 +35,10 @@ def extract(carrier, context: ::OpenTelemetry::Context.current, getter: ::OpenTe def inject(carrier, context: ::OpenTelemetry::Context.current, setter: ::OpenTelemetry::Context::Propagation.text_map_setter) span_context = ::OpenTelemetry::Trace.current_span(context).context - SolarWindsAPM.logger.debug do - "[#{self.class}/#{__method__}] context: #{context.inspect}; span_context: #{span_context.inspect}" - end + + SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] context current_span: #{context.instance_variable_get(:@entries)&.values&.first.inspect}" } + SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] span_context: #{span_context.inspect}" } + return unless span_context&.valid? x_trace = Utils.traceparent_from_context(span_context) diff --git a/lib/solarwinds_apm/otel_config.rb b/lib/solarwinds_apm/otel_config.rb index b5380ee6..823b367c 100644 --- a/lib/solarwinds_apm/otel_config.rb +++ b/lib/solarwinds_apm/otel_config.rb @@ -126,6 +126,15 @@ def self.initialize # configure sampler afterwards ::OpenTelemetry.tracer_provider.sampler = @@config[:sampler] + + require_relative 'patches/tag_sql_patch' if SolarWindsAPM::Config[:tag_sql] + + if ENV['SW_APM_AUTO_CONFIGURE'] == 'false' + SolarWindsAPM.logger.info '===================================================================' + SolarWindsAPM.logger.info "\e[1mSolarWindsAPM manual initialization was successful.\e[0m" + SolarWindsAPM.logger.info '===================================================================' + end + nil end diff --git a/lib/solarwinds_apm/patch.rb b/lib/solarwinds_apm/patch.rb deleted file mode 100644 index 6fde8960..00000000 --- a/lib/solarwinds_apm/patch.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -# © 2023 SolarWinds Worldwide, LLC. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at:http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - -# This file is for loading any customized patch for upstream - -# e.g. -# require_relative './patch/dummy_patch' -# OpenTelemetry::Instrumentation::Registry.prepend(SolarWindsAPM::Patch::DummyPatch) if defined? OpenTelemetry::Instrumentation::Registry && OpenTelemetry::Instrumentation::Registry::VERSION <= '0.3.0' diff --git a/lib/solarwinds_apm/patches/README.md b/lib/solarwinds_apm/patches/README.md new file mode 100644 index 00000000..e69de29b diff --git a/lib/solarwinds_apm/patch/dummy_patch.rb b/lib/solarwinds_apm/patches/dummy_patch.rb similarity index 100% rename from lib/solarwinds_apm/patch/dummy_patch.rb rename to lib/solarwinds_apm/patches/dummy_patch.rb diff --git a/lib/solarwinds_apm/patches/mysql2_client_patch.rb b/lib/solarwinds_apm/patches/mysql2_client_patch.rb new file mode 100644 index 00000000..f5859f6a --- /dev/null +++ b/lib/solarwinds_apm/patches/mysql2_client_patch.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module SolarWindsAPM + module Patches + module SWOMysql2ClientPatch + # this method is just save the traceparent comment before sanitization + def _otel_span_attributes(sql) + # if omit, then no need to append any statement + # if include, then comments won't be removed + # only obfuscate need to add the original comments back + # because of obfuscation method removed the comments + # This module need to injected after Mysql2::Patches::Client prepended (aka after solarwinds_apm initialized) + # check ::Mysql2::Client.included_modules or ::Mysql2::Client.ancestors to see the order of calling + if config[:db_statement] == :obfuscate + extracted_comments = sql.match(TagSqlConstants::TRACEPARENT_REGEX) + attributes = super + attributes[::OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT] = attributes[::OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT] + extracted_comments&.match(0).to_s + attributes + else + super + end + end + end + end +end + +Mysql2::Client.prepend(SolarWindsAPM::Patches::SWOMysql2ClientPatch) if defined?(Mysql2::Client) && defined?(OpenTelemetry::Instrumentation::Mysql2::Patches::Client) diff --git a/lib/solarwinds_apm/patches/pg_connection_patch.rb b/lib/solarwinds_apm/patches/pg_connection_patch.rb new file mode 100644 index 00000000..1490f35b --- /dev/null +++ b/lib/solarwinds_apm/patches/pg_connection_patch.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module SolarWindsAPM + module Patches + module SWOPgConnectionPatch + # this method is just save the traceparent comment before sanitization + def obfuscate_sql(sql) + if config[:db_statement] == :obfuscate + extracted_comments = sql.match(TagSqlConstants::TRACEPARENT_REGEX) + super + extracted_comments&.match(0).to_s + else + super + end + end + end + end +end + +PG::Connection.prepend(SolarWindsAPM::Patches::SWOPgConnectionPatch) if defined?(PG::Connection) && defined?(OpenTelemetry::Instrumentation::PG::Patches::Connection) diff --git a/lib/solarwinds_apm/patches/tag_sql_constants.rb b/lib/solarwinds_apm/patches/tag_sql_constants.rb new file mode 100644 index 00000000..2412f66f --- /dev/null +++ b/lib/solarwinds_apm/patches/tag_sql_constants.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module SolarWindsAPM + module Patches + module TagSqlConstants + TRACEPARENT_REGEX = %r{/\*\s*traceparent=?'?[0-9a-f]{2}-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}'?\s*\*/} + end + end +end diff --git a/lib/solarwinds_apm/patches/tag_sql_patch.rb b/lib/solarwinds_apm/patches/tag_sql_patch.rb new file mode 100644 index 00000000..addee63f --- /dev/null +++ b/lib/solarwinds_apm/patches/tag_sql_patch.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require_relative 'tag_sql_constants' +require_relative 'mysql2_client_patch' +require_relative 'pg_connection_patch' +require_relative 'trilogy_client_patch' diff --git a/lib/solarwinds_apm/patches/trilogy_client_patch.rb b/lib/solarwinds_apm/patches/trilogy_client_patch.rb new file mode 100644 index 00000000..9cb2d00c --- /dev/null +++ b/lib/solarwinds_apm/patches/trilogy_client_patch.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module SolarWindsAPM + module Patches + module SWOTrilogyClientPatch + # this method is just save the traceparent comment before sanitization + def client_attributes(sql = nil) + if sql && config[:db_statement] == :obfuscate + extracted_comments = sql.match(TagSqlConstants::TRACEPARENT_REGEX) + attributes = super + attributes[::OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT] = attributes[::OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT] + extracted_comments&.match(0).to_s + attributes + else + super + end + end + end + end +end + +Trilogy.prepend(SolarWindsAPM::Patches::SWOTrilogyClientPatch) if defined?(Trilogy) && defined?(OpenTelemetry::Instrumentation::Trilogy::Patches::Client) diff --git a/lib/solarwinds_apm/support/swomarginalia/README.md b/lib/solarwinds_apm/support/swomarginalia/README.md index c4a9dfa1..c346f683 100644 --- a/lib/solarwinds_apm/support/swomarginalia/README.md +++ b/lib/solarwinds_apm/support/swomarginalia/README.md @@ -33,6 +33,10 @@ This folder contains the code that is copied from original [marginalia](https:// 1. (new file) prepend the ActiveRecordInstrumentation to activerecord adapter +### annotation.rb +1. (new file) utilize comment file append the generated traceparent data into sql +2. If sql already contains the same comments, then it won't inject again + ## Example ### Sample output of rails application diff --git a/lib/solarwinds_apm/support/swomarginalia/annotation.rb b/lib/solarwinds_apm/support/swomarginalia/annotation.rb new file mode 100644 index 00000000..adf7f0e5 --- /dev/null +++ b/lib/solarwinds_apm/support/swomarginalia/annotation.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require_relative 'comment' + +module SolarWindsAPM + module SWOMarginalia + module Annotation + def annotate_sql(sql) + SWOMarginalia::Comment.update_adapter!(self) # switch to current sql adapter + comment = SWOMarginalia::Comment.construct_comment # comment will include traceparent + if comment.present? && !sql.include?(comment) + sql = if SWOMarginalia::Comment.prepend_comment + "/*#{comment}*/ #{sql}" + else + "#{sql} /*#{comment}*/" + end + end + + inline_comment = SWOMarginalia::Comment.construct_inline_comment # this is for customized_swo_inline_annotations (user-defined value) + if inline_comment.present? && !sql.include?(inline_comment) + sql = if SWOMarginalia::Comment.prepend_comment + "/*#{inline_comment}*/ #{sql}" + else + "#{sql} /*#{inline_comment}*/" + end + end + + sql + end + + # We don't want to trace framework caches. + # Only instrument SQL that directly hits the database. + def ignore_payload?(name) + %w[SCHEMA EXPLAIN CACHE].include?(name.to_s) + end + end + end +end diff --git a/lib/solarwinds_apm/support/swomarginalia/load_swomarginalia.rb b/lib/solarwinds_apm/support/swomarginalia/load_swomarginalia.rb index 214f7dc8..bece825c 100644 --- a/lib/solarwinds_apm/support/swomarginalia/load_swomarginalia.rb +++ b/lib/solarwinds_apm/support/swomarginalia/load_swomarginalia.rb @@ -41,14 +41,10 @@ def self.insert_into_action_controller end def self.insert_into_active_record - ActiveRecord::ConnectionAdapters::Mysql2Adapter.prepend(SWOMarginalia::ActiveRecordInstrumentation) if defined? ActiveRecord::ConnectionAdapters::Mysql2Adapter - ActiveRecord::ConnectionAdapters::MysqlAdapter.prepend(SWOMarginalia::ActiveRecordInstrumentation) if defined? ActiveRecord::ConnectionAdapters::MysqlAdapter + ActiveRecord::ConnectionAdapters::Mysql2Adapter.prepend(SWOMarginalia::ActiveRecordInstrumentation) if defined? ActiveRecord::ConnectionAdapters::Mysql2Adapter ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(SWOMarginalia::ActiveRecordInstrumentation) if defined? ActiveRecord::ConnectionAdapters::PostgreSQLAdapter - return unless defined? ActiveRecord::ConnectionAdapters::SQLite3Adapter - - return unless defined? ActiveRecord::ConnectionAdapters::SQLite3Adapter - - ActiveRecord::ConnectionAdapters::SQLite3Adapter.prepend(SWOMarginalia::ActiveRecordInstrumentation) + ActiveRecord::ConnectionAdapters::SQLite3Adapter.prepend(SWOMarginalia::ActiveRecordInstrumentation) if defined? ActiveRecord::ConnectionAdapters::SQLite3Adapter + ActiveRecord::ConnectionAdapters::TrilogyAdapter.prepend(SWOMarginalia::ActiveRecordInstrumentation) if defined? ActiveRecord::ConnectionAdapters::TrilogyAdapter end end end diff --git a/lib/solarwinds_apm/support/swomarginalia/swomarginalia.rb b/lib/solarwinds_apm/support/swomarginalia/swomarginalia.rb index 2b76275a..bb042a09 100644 --- a/lib/solarwinds_apm/support/swomarginalia/swomarginalia.rb +++ b/lib/solarwinds_apm/support/swomarginalia/swomarginalia.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative 'comment' +require_relative 'annotation' module SolarWindsAPM module SWOMarginalia @@ -9,6 +9,8 @@ module SWOMarginalia # This ActiveRecordInstrumentation should only work for activerecord < 7.0 since after rails 7 # this module won't be prepend to activerecord module ActiveRecordInstrumentation + include Annotation + def execute(sql, *args, **options) super(annotate_sql(sql), *args, **options) end @@ -18,10 +20,18 @@ def execute_and_clear(sql, *args, **options) super(annotate_sql(sql), *args, **options) end + # For activerecord < 7.1.0 def exec_query(sql, *args, **options) super(annotate_sql(sql), *args, **options) end + # This patch is for non-rails app (e.g. sinatra) that use activerecord >= 7.1.0 + # From 7.1.0, query() changed calling from exec_query() to internal_exec_query() + # Psql is not affected because in psql, internal_exec_query calls execute_and_clear + def internal_exec_query(sql, *args, **options) + super(annotate_sql(sql), *args, **options) + end + def exec_delete(sql, *args) super(annotate_sql(sql), *args) end @@ -29,35 +39,6 @@ def exec_delete(sql, *args) def exec_update(sql, *args) super(annotate_sql(sql), *args) end - - def annotate_sql(sql) - SWOMarginalia::Comment.update_adapter!(self) # switch to current sql adapter - comment = SWOMarginalia::Comment.construct_comment # comment will include traceparent - if comment.present? && !sql.include?(comment) - sql = if SWOMarginalia::Comment.prepend_comment - "/*#{comment}*/ #{sql}" - else - "#{sql} /*#{comment}*/" - end - end - - inline_comment = SWOMarginalia::Comment.construct_inline_comment # this is for customized_swo_inline_annotations (user-defined value) - if inline_comment.present? && !sql.include?(inline_comment) - sql = if SWOMarginalia::Comment.prepend_comment - "/*#{inline_comment}*/ #{sql}" - else - "#{sql} /*#{inline_comment}*/" - end - end - - sql - end - - # We don't want to trace framework caches. - # Only instrument SQL that directly hits the database. - def ignore_payload?(name) - %w[SCHEMA EXPLAIN CACHE].include?(name.to_s) - end end module ActionControllerInstrumentation diff --git a/test/minitest_helper.rb b/test/minitest_helper.rb index 1163d5ec..e204828f 100644 --- a/test/minitest_helper.rb +++ b/test/minitest_helper.rb @@ -359,3 +359,15 @@ def clean_old_setting ENV.delete('OTEL_TRACES_EXPORTER') ENV.delete('SW_APM_ENABLED') end + +## +# Some Marco +# + +module OpenTelemetry + module SemanticConventions + module Trace + DB_STATEMENT = 'db.statement' + end + end +end diff --git a/test/patches/mysql2_client_patch_test.rb b/test/patches/mysql2_client_patch_test.rb new file mode 100644 index 00000000..5e572582 --- /dev/null +++ b/test/patches/mysql2_client_patch_test.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +# Copyright (c) 2023 SolarWinds, LLC. +# All rights reserved. + +# rubocop:disable Lint/ConstantDefinitionInBlock +require 'minitest_helper' +require './lib/solarwinds_apm/patches/tag_sql_constants' + +describe 'mysql2_client_patch' do + before do + module Mysql2 + class Client + def config + { db_statement: :obfuscate } + end + + def mock_query(sql); end + end + end + + module OpenTelemetry + module Instrumentation + module Mysql2 + module Patches + module Client + # mocked client based on otel mysql2 instrumentation + def mock_query(sql) + _otel_span_attributes(sql) + end + + def _otel_span_attributes(sql) + attributes = {} + case config[:db_statement] + when :include + attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT] = sql + when :obfuscate + attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT] = "SELECT `customers`.* FROM `customers` WHERE `customers`.`contactLastName` = '?' LIMIT '?'" + end + attributes + end + end + end + end + end + Mysql2::Client.prepend(OpenTelemetry::Instrumentation::Mysql2::Patches::Client) + end + end + + it 'mysql2_should_patch_when_exist_mysql2_and_instrumentation' do + load File.expand_path('../../lib/solarwinds_apm/patches/mysql2_client_patch.rb', __dir__) + _(Mysql2::Client.ancestors[0]).must_equal SolarWindsAPM::Patches::SWOMysql2ClientPatch + end + + it 'should_keep_traceparent_when_obfuscate' do + load File.expand_path('../../lib/solarwinds_apm/patches/mysql2_client_patch.rb', __dir__) + client = Mysql2::Client.new + attributes = client.mock_query("SELECT `customers`.* FROM `customers` WHERE `customers`.`contactLastName` = 'Schmitt' LIMIT 1 /*traceparent='00-f0ebd771266f8c359af8b10c1c57e623-aecd3d0c5c4f9a94-01'*/") + _(attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_equal "SELECT `customers`.* FROM `customers` WHERE `customers`.`contactLastName` = '?' LIMIT '?'/*traceparent='00-f0ebd771266f8c359af8b10c1c57e623-aecd3d0c5c4f9a94-01'*/" + end + + it 'mysql2_should_not_change_anything_when_non_obfuscate' do + module Mysql2 + class Client + def config + { db_statement: :include } + end + end + end + + load File.expand_path('../../lib/solarwinds_apm/patches/mysql2_client_patch.rb', __dir__) + client = Mysql2::Client.new + attributes = client.mock_query("SELECT `customers`.* FROM `customers` WHERE `customers`.`contactLastName` = 'Schmitt' LIMIT 1 /*traceparent='00-f0ebd771266f8c359af8b10c1c57e623-aecd3d0c5c4f9a94-01'*/") + _(attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_equal "SELECT `customers`.* FROM `customers` WHERE `customers`.`contactLastName` = 'Schmitt' LIMIT 1 /*traceparent='00-f0ebd771266f8c359af8b10c1c57e623-aecd3d0c5c4f9a94-01'*/" + end + + it 'mysql2_should_no_db_statement_include' do + module Mysql2 + class Client + def config + { db_statement: :omit } + end + end + end + + load File.expand_path('../../lib/solarwinds_apm/patches/mysql2_client_patch.rb', __dir__) + client = Mysql2::Client.new + attributes = client.mock_query("SELECT `customers`.* FROM `customers` WHERE `customers`.`contactLastName` = 'Schmitt' LIMIT 1 /*traceparent='00-f0ebd771266f8c359af8b10c1c57e623-aecd3d0c5c4f9a94-01'*/") + assert_nil(attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]) + end +end + +# rubocop:enable Lint/ConstantDefinitionInBlock diff --git a/test/patches/pg_connection_patch_test.rb b/test/patches/pg_connection_patch_test.rb new file mode 100644 index 00000000..426c9283 --- /dev/null +++ b/test/patches/pg_connection_patch_test.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +# Copyright (c) 2023 SolarWinds, LLC. +# All rights reserved. + +# rubocop:disable Lint/ConstantDefinitionInBlock +require 'minitest_helper' +require './lib/solarwinds_apm/patches/tag_sql_constants' + +describe 'pg_connection_patch' do + before do + module PG + class Connection + def config + { db_statement: :obfuscate } + end + + def mock_query(sql); end + end + end + + module OpenTelemetry + module Instrumentation + module PG + module Patches + module Connection + # mocked client based on otel pg instrumentation + def mock_query(sql) + obfuscate_sql(sql) + end + + def obfuscate_sql(sql) + return sql unless config[:db_statement] == :obfuscate + + "SELECT `customers`.* FROM `customers` WHERE `customers`.`contactLastName` = '?' LIMIT '?'" + end + end + end + end + end + PG::Connection.prepend(OpenTelemetry::Instrumentation::PG::Patches::Connection) + end + end + + it 'pg_should_patch_when_exist_pg_and_instrumentation' do + load File.expand_path('../../lib/solarwinds_apm/patches/pg_connection_patch.rb', __dir__) + _(PG::Connection.ancestors[0]).must_equal SolarWindsAPM::Patches::SWOPgConnectionPatch + end + + it 'should_keep_traceparent_when_obfuscate' do + load File.expand_path('../../lib/solarwinds_apm/patches/pg_connection_patch.rb', __dir__) + client = PG::Connection.new + pg_sql = client.mock_query("SELECT `customers`.* FROM `customers` WHERE `customers`.`contactLastName` = 'Schmitt' LIMIT 1 /*traceparent='00-f0ebd771266f8c359af8b10c1c57e623-aecd3d0c5c4f9a94-01'*/") + _(pg_sql).must_equal "SELECT `customers`.* FROM `customers` WHERE `customers`.`contactLastName` = '?' LIMIT '?'/*traceparent='00-f0ebd771266f8c359af8b10c1c57e623-aecd3d0c5c4f9a94-01'*/" + end + + it 'pg_should_not_change_anything_when_non_obfuscate' do + module PG + class Connection + def config + { db_statement: :include } + end + end + end + + load File.expand_path('../../lib/solarwinds_apm/patches/pg_connection_patch.rb', __dir__) + client = PG::Connection.new + pg_sql = client.mock_query("SELECT `customers`.* FROM `customers` WHERE `customers`.`contactLastName` = 'Schmitt' LIMIT 1 /*traceparent='00-f0ebd771266f8c359af8b10c1c57e623-aecd3d0c5c4f9a94-01'*/") + _(pg_sql).must_equal "SELECT `customers`.* FROM `customers` WHERE `customers`.`contactLastName` = 'Schmitt' LIMIT 1 /*traceparent='00-f0ebd771266f8c359af8b10c1c57e623-aecd3d0c5c4f9a94-01'*/" + end + + # pg obfuscate_sql function doesn't test for omit, span_attrs will omit the sql +end + +# rubocop:enable Lint/ConstantDefinitionInBlock diff --git a/test/patches/tag_sql_patch_test.rb b/test/patches/tag_sql_patch_test.rb new file mode 100644 index 00000000..e1aa74c9 --- /dev/null +++ b/test/patches/tag_sql_patch_test.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +# Copyright (c) 2023 SolarWinds, LLC. +# All rights reserved. + +# rubocop:disable Lint/ConstantDefinitionInBlock +require 'minitest_helper' + +describe 'trilogy_client_patch' do + it 'should_not_patch_if_tag_sql_is_false' do + load File.expand_path('../../lib/solarwinds_apm/patches/tag_sql_constants.rb', __dir__) + + _(Trilogy.ancestors[0]).must_equal SolarWindsAPM::Patches::SWOTrilogyClientPatch + end + + it 'should_keep_traceparent_when_obfuscate' do + load File.expand_path('../../lib/solarwinds_apm/patches/trilogy_client_patch.rb', __dir__) + client = Trilogy.new + attributes = client.mock_query("SELECT `customers`.* FROM `customers` WHERE `customers`.`contactLastName` = 'Schmitt' LIMIT 1 /*traceparent='00-f0ebd771266f8c359af8b10c1c57e623-aecd3d0c5c4f9a94-01'*/") + _(attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_equal "SELECT `customers`.* FROM `customers` WHERE `customers`.`contactLastName` = '?' LIMIT '?'/*traceparent='00-f0ebd771266f8c359af8b10c1c57e623-aecd3d0c5c4f9a94-01'*/" + end + + it 'trilogy_should_not_change_anything_when_non_obfuscate' do + class Trilogy + def config + { db_statement: :include } + end + end + + load File.expand_path('../../lib/solarwinds_apm/patches/trilogy_client_patch.rb', __dir__) + client = Trilogy.new + attributes = client.mock_query("SELECT `customers`.* FROM `customers` WHERE `customers`.`contactLastName` = 'Schmitt' LIMIT 1 /*traceparent='00-f0ebd771266f8c359af8b10c1c57e623-aecd3d0c5c4f9a94-01'*/") + _(attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_equal "SELECT `customers`.* FROM `customers` WHERE `customers`.`contactLastName` = 'Schmitt' LIMIT 1 /*traceparent='00-f0ebd771266f8c359af8b10c1c57e623-aecd3d0c5c4f9a94-01'*/" + end + + it 'trilogy_should_no_db_statement_include' do + class Trilogy + def config + { db_statement: :omit } + end + end + + load File.expand_path('../../lib/solarwinds_apm/patches/trilogy_client_patch.rb', __dir__) + client = Trilogy.new + attributes = client.mock_query("SELECT `customers`.* FROM `customers` WHERE `customers`.`contactLastName` = 'Schmitt' LIMIT 1 /*traceparent='00-f0ebd771266f8c359af8b10c1c57e623-aecd3d0c5c4f9a94-01'*/") + assert_nil(attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]) + end +end + +# rubocop:enable Lint/ConstantDefinitionInBlock diff --git a/test/patches/traceparent_regex_test.rb b/test/patches/traceparent_regex_test.rb new file mode 100644 index 00000000..cf05a834 --- /dev/null +++ b/test/patches/traceparent_regex_test.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +# Copyright (c) 2023 SolarWinds, LLC. +# All rights reserved. + +require 'minitest_helper' +require 'minitest/mock' +require './lib/solarwinds_apm/patches/tag_sql_constants' + +describe 'traceparent_regex_test' do + let(:traceparent_regex) { SolarWindsAPM::Patches::TagSqlConstants::TRACEPARENT_REGEX } + + it 'standard_sql_comments' do + sql = "SELECT `a`.* FROM `a` WHERE `a`.`b` = 'abc' LIMIT 1 /*traceparent='00-f0ebd771266f8c359af8b10c1c57e623-aecd3d0c5c4f9a94-01'*/" + matches = sql.match(traceparent_regex) + _(matches.match(0)).must_equal "/*traceparent='00-f0ebd771266f8c359af8b10c1c57e623-aecd3d0c5c4f9a94-01'*/" + end + + it 'sql_with_space' do + sql = "SELECT `a`.* FROM `a` WHERE `a`.`b` = 'abc' LIMIT 1 /* traceparent='00-f0ebd771266f8c359af8b10c1c57e623-aecd3d0c5c4f9a94-01' */" + matches = sql.match(traceparent_regex) + _(matches.match(0)).must_equal "/* traceparent='00-f0ebd771266f8c359af8b10c1c57e623-aecd3d0c5c4f9a94-01' */" + end + + it 'sql_without_single_quote' do + sql = "SELECT `a`.* FROM `a` WHERE `a`.`b` = 'abc' LIMIT 1 /*traceparent=00-f0ebd771266f8c359af8b10c1c57e623-aecd3d0c5c4f9a94-01*/" + matches = sql.match(traceparent_regex) + _(matches.match(0)).must_equal '/*traceparent=00-f0ebd771266f8c359af8b10c1c57e623-aecd3d0c5c4f9a94-01*/' + end + + it 'sql_without_single_quote_with_space' do + sql = "SELECT `a`.* FROM `a` WHERE `a`.`b` = 'abc' LIMIT 1 /* traceparent=00-f0ebd771266f8c359af8b10c1c57e623-aecd3d0c5c4f9a94-01 */" + matches = sql.match(traceparent_regex) + _(matches.match(0)).must_equal '/* traceparent=00-f0ebd771266f8c359af8b10c1c57e623-aecd3d0c5c4f9a94-01 */' + end + + it 'sql_with_wrong_format' do + sql = "SELECT `a`.* FROM `a` WHERE `a`.`b` = 'abc' LIMIT 1 /*trace='00-f0ebd771266f8c359af8b10c1c57e623-aecd3d0c5c4f9a94-01'*/" + matches = sql.match(traceparent_regex) + assert_nil(matches) + end + + it 'sql_with_wrong_format_on_context' do + sql = "SELECT `a`.* FROM `a` WHERE `a`.`b` = 'abc' LIMIT 1 /*traceparent='00-aecd3d0c5c4f9a94-f0ebd771266f8c359af8b10c1c57e623-01'*/" + matches = sql.match(traceparent_regex) + assert_nil(matches) + end +end diff --git a/test/patches/trilogy_client_patch_test.rb b/test/patches/trilogy_client_patch_test.rb new file mode 100644 index 00000000..0f97e86c --- /dev/null +++ b/test/patches/trilogy_client_patch_test.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +# Copyright (c) 2023 SolarWinds, LLC. +# All rights reserved. + +# rubocop:disable Lint/ConstantDefinitionInBlock +require 'minitest_helper' +require './lib/solarwinds_apm/patches/tag_sql_constants' + +describe 'tag_sql_patch_test' do + before do + class Trilogy + def config + { db_statement: :obfuscate } + end + + def mock_query(sql); end + end + + module OpenTelemetry + module Instrumentation + module Trilogy + module Patches + module Client + # mocked client based on otel trilogy instrumentation + def mock_query(sql) + client_attributes(sql) + end + + def client_attributes(sql) + attributes = {} + if sql + case config[:db_statement] + when :obfuscate + attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT] = "SELECT `customers`.* FROM `customers` WHERE `customers`.`contactLastName` = '?' LIMIT '?'" + when :include + attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT] = sql + end + end + + attributes + end + end + end + end + end + Trilogy.prepend(OpenTelemetry::Instrumentation::Trilogy::Patches::Client) + end + end + + it 'trilogy_should_patch_when_exist_trilogy_and_instrumentation' do + load File.expand_path('../../lib/solarwinds_apm/patches/trilogy_client_patch.rb', __dir__) + _(Trilogy.ancestors[0]).must_equal SolarWindsAPM::Patches::SWOTrilogyClientPatch + end + + it 'should_keep_traceparent_when_obfuscate' do + load File.expand_path('../../lib/solarwinds_apm/patches/trilogy_client_patch.rb', __dir__) + client = Trilogy.new + attributes = client.mock_query("SELECT `customers`.* FROM `customers` WHERE `customers`.`contactLastName` = 'Schmitt' LIMIT 1 /*traceparent='00-f0ebd771266f8c359af8b10c1c57e623-aecd3d0c5c4f9a94-01'*/") + _(attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_equal "SELECT `customers`.* FROM `customers` WHERE `customers`.`contactLastName` = '?' LIMIT '?'/*traceparent='00-f0ebd771266f8c359af8b10c1c57e623-aecd3d0c5c4f9a94-01'*/" + end + + it 'trilogy_should_not_change_anything_when_non_obfuscate' do + class Trilogy + def config + { db_statement: :include } + end + end + + load File.expand_path('../../lib/solarwinds_apm/patches/trilogy_client_patch.rb', __dir__) + client = Trilogy.new + attributes = client.mock_query("SELECT `customers`.* FROM `customers` WHERE `customers`.`contactLastName` = 'Schmitt' LIMIT 1 /*traceparent='00-f0ebd771266f8c359af8b10c1c57e623-aecd3d0c5c4f9a94-01'*/") + _(attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]).must_equal "SELECT `customers`.* FROM `customers` WHERE `customers`.`contactLastName` = 'Schmitt' LIMIT 1 /*traceparent='00-f0ebd771266f8c359af8b10c1c57e623-aecd3d0c5c4f9a94-01'*/" + end + + it 'trilogy_should_no_db_statement_include' do + class Trilogy + def config + { db_statement: :omit } + end + end + + load File.expand_path('../../lib/solarwinds_apm/patches/trilogy_client_patch.rb', __dir__) + client = Trilogy.new + attributes = client.mock_query("SELECT `customers`.* FROM `customers` WHERE `customers`.`contactLastName` = 'Schmitt' LIMIT 1 /*traceparent='00-f0ebd771266f8c359af8b10c1c57e623-aecd3d0c5c4f9a94-01'*/") + assert_nil(attributes[OpenTelemetry::SemanticConventions::Trace::DB_STATEMENT]) + end +end + +# rubocop:enable Lint/ConstantDefinitionInBlock diff --git a/test/solarwinds_apm/otel_config_test.rb b/test/solarwinds_apm/otel_config_test.rb index ffa50072..ed93cb92 100644 --- a/test/solarwinds_apm/otel_config_test.rb +++ b/test/solarwinds_apm/otel_config_test.rb @@ -98,4 +98,35 @@ _(OpenTelemetry.logger.level).must_equal 4 end end + + describe 'check_if_tag_sql_patch' do + it 'tag_sql_is_off' do + ENV['SW_APM_TAG_SQL'] = 'false' + SolarWindsAPM::Config.initialize + _(SolarWindsAPM::Config[:tag_sql]).must_equal false + + SolarWindsAPM::OTelConfig.initialize + assert_nil(defined?(SolarWindsAPM::Patches::SWOMysql2ClientPatch)) + assert_nil(defined?(SolarWindsAPM::Patches::SWOPgConnectionPatch)) + assert_nil(defined?(SolarWindsAPM::Patches::SWOTrilogyClientPatch)) + + ENV.delete('SW_APM_TAG_SQL') + end + + it 'tag_sql_is_on' do + ENV['SW_APM_TAG_SQL'] = 'true' + SolarWindsAPM::Config.initialize + _(SolarWindsAPM::Config[:tag_sql]).must_equal true + + SolarWindsAPM::OTelConfig.initialize + _(defined?(SolarWindsAPM::Patches::SWOMysql2ClientPatch)).must_equal 'constant' + _(defined?(SolarWindsAPM::Patches::SWOPgConnectionPatch)).must_equal 'constant' + _(defined?(SolarWindsAPM::Patches::SWOTrilogyClientPatch)).must_equal 'constant' + + SolarWindsAPM::Patches.send(:remove_const, :SWOMysql2ClientPatch) + SolarWindsAPM::Patches.send(:remove_const, :SWOPgConnectionPatch) + SolarWindsAPM::Patches.send(:remove_const, :SWOTrilogyClientPatch) + ENV.delete('SW_APM_TAG_SQL') + end + end end