From 658d87bcc81d3459d2b07ab922b57e0c594d7966 Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Mon, 10 Jul 2023 10:26:06 +0100 Subject: [PATCH] Fix profiler libmysqlclient version detection with mysql2-aurora gem **What does this PR do?**: This PR tweaks the libmysqlclient version detection code added in #2770 to also work when the `mysql2-aurora` gem is in use. **Motivation**: The way that the mysql2-aurora gem installs itself leads to the "no signals" workaround (#2873) being incorrectly enabled for customers that do have a modern version of libmysqlclient. **Additional Notes**: The mysql2-aurora gem likes to monkey patch itself in replacement of `Mysql2::Client`, and uses `method_missing` to delegate to the original BUT unfortunately does not implement `respond_to_missing?` and thus one of our checks (`respond_to?(:info)`) was incorrectly failing. **How to test the change?**: This change includes code coverage. This can also be reproduced easily by adding the `mysql2-aurora` gem to the `Gemfile` and then running a trivial Ruby app: ``` $ DD_PROFILING_ENABLED=true DD_TRACE_DEBUG=true bundle exec ruby -e "require 'mysql2/aurora'; require 'datadog/profiling/preload'" # Before DEBUG -- ddtrace: [ddtrace] Requiring `mysql2` to check if the `libmysqlclient` version it uses is compatible with profiling WARN -- ddtrace: [ddtrace] Enabling the profiling "no signals" workaround because an incompatible version of the mysql2 gem is installed. Profiling data will have lower quality. To fix this, upgrade the libmysqlclient in your OS image to version 8.0.0 or above. # After DEBUG -- ddtrace: [ddtrace] Requiring `mysql2` to check if the `libmysqlclient` version it uses is compatible with profiling DEBUG -- ddtrace: [ddtrace] The `mysql2` gem is using a compatible version of the `libmysqlclient` library (8.0.33) ``` --- Steepfile | 1 + lib/datadog/profiling/component.rb | 21 +++++++++++++--- spec/datadog/profiling/component_spec.rb | 26 ++++++++++++++++++++ vendor/rbs/mysql2-aurora/0/mysql2-aurora.rbs | 5 ++++ 4 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 vendor/rbs/mysql2-aurora/0/mysql2-aurora.rbs diff --git a/Steepfile b/Steepfile index 1c54e48bde1..37f9abc0309 100644 --- a/Steepfile +++ b/Steepfile @@ -659,6 +659,7 @@ target :ddtrace do library 'google-protobuf' library 'protobuf-cucumber' library 'mysql2' + library 'mysql2-aurora' library 'opentracing' library 'concurrent-ruby' library 'faraday' diff --git a/lib/datadog/profiling/component.rb b/lib/datadog/profiling/component.rb index daebc943a64..2a9c2baec7e 100644 --- a/lib/datadog/profiling/component.rb +++ b/lib/datadog/profiling/component.rb @@ -207,7 +207,7 @@ def self.build_profiler_component(settings:, agent_settings:, optional_tracer:) if Gem.loaded_specs['mysql2'] && incompatible_libmysqlclient_version?(settings) Datadog.logger.warn( 'Enabling the profiling "no signals" workaround because an incompatible version of the mysql2 gem is ' \ - 'installed. Profiling data will have lower quality.' \ + 'installed. Profiling data will have lower quality. ' \ 'To fix this, upgrade the libmysqlclient in your OS image to version 8.0.0 or above.' ) return true @@ -245,9 +245,22 @@ def self.build_profiler_component(settings:, agent_settings:, optional_tracer:) begin require 'mysql2' - return true unless defined?(Mysql2::Client) && Mysql2::Client.respond_to?(:info) - - libmysqlclient_version = Gem::Version.new(Mysql2::Client.info[:version]) + # The mysql2-aurora gem likes to monkey patch itself in replacement of Mysql2::Client, and uses + # `method_missing` to delegate to the original BUT unfortunately does not implement `respond_to_missing?` and + # thus our `respond_to?(:info)` below was failing. + # + # But on the bright side, the gem does stash a reference to the original Mysql2::Client class in a constant, + # so if that constant exists, we use that for our probing. + mysql2_client_class = + if defined?(Mysql2::Aurora::ORIGINAL_CLIENT_CLASS) + Mysql2::Aurora::ORIGINAL_CLIENT_CLASS + elsif defined?(Mysql2::Client) + Mysql2::Client + end + + return true unless mysql2_client_class && mysql2_client_class.respond_to?(:info) + + libmysqlclient_version = Gem::Version.new(mysql2_client_class.info[:version]) compatible = libmysqlclient_version >= Gem::Version.new('8.0.0') diff --git a/spec/datadog/profiling/component_spec.rb b/spec/datadog/profiling/component_spec.rb index 377b2bcef79..326f0ca32ab 100644 --- a/spec/datadog/profiling/component_spec.rb +++ b/spec/datadog/profiling/component_spec.rb @@ -551,6 +551,32 @@ no_signals_workaround_enabled? end end + + context 'when mysql2-aurora gem is loaded and libmysqlclient < 8.0.0' do + before do + fake_original_client = double('Fake original Mysql2::Client') + stub_const('Mysql2::Aurora::ORIGINAL_CLIENT_CLASS', fake_original_client) + expect(fake_original_client).to receive(:info).and_return({ version: '7.9.9' }) + + client_replaced_by_aurora = double('Fake Aurora Mysql2::Client') + stub_const('Mysql2::Client', client_replaced_by_aurora) + end + + it { is_expected.to be true } + end + + context 'when mysql2-aurora gem is loaded and libmysqlclient >= 8.0.0' do + before do + fake_original_client = double('Fake original Mysql2::Client') + stub_const('Mysql2::Aurora::ORIGINAL_CLIENT_CLASS', fake_original_client) + expect(fake_original_client).to receive(:info).and_return({ version: '8.0.0' }) + + client_replaced_by_aurora = double('Fake Aurora Mysql2::Client') + stub_const('Mysql2::Client', client_replaced_by_aurora) + end + + it { is_expected.to be false } + end end end diff --git a/vendor/rbs/mysql2-aurora/0/mysql2-aurora.rbs b/vendor/rbs/mysql2-aurora/0/mysql2-aurora.rbs new file mode 100644 index 00000000000..feb0e6e3b6d --- /dev/null +++ b/vendor/rbs/mysql2-aurora/0/mysql2-aurora.rbs @@ -0,0 +1,5 @@ +module Mysql2 + module Aurora + ORIGINAL_CLIENT_CLASS: untyped + end +end