lib/datadog/profiling/component.rb in ddtrace-1.10.1 vs lib/datadog/profiling/component.rb in ddtrace-1.11.0.beta1

- old
+ new

@@ -1,12 +1,15 @@ # frozen_string_literal: true module Datadog module Profiling - # Profiling component + # Responsible for wiring up the Profiler for execution module Component - def build_profiler(settings, agent_settings, tracer) + # Passing in a `nil` tracer is supported and will disable the following profiling features: + # * Code Hotspots panel in the trace viewer, as well as scoping a profile down to a span + # * Endpoint aggregation in the profiler UX, including normalization (resource per endpoint call) + def self.build_profiler_component(settings:, agent_settings:, optional_tracer:) return unless settings.profiling.enabled # Workaround for weird dependency direction: the Core::Configuration::Components class currently has a # dependency on individual products, in this case the Profiler. # (Note "currently": in the future we want to change this so core classes don't depend on specific products) @@ -59,73 +62,72 @@ # Load extensions needed to support some of the Profiling features Profiling::Tasks::Setup.new.run # NOTE: Please update the Initialization section of ProfilingDevelopment.md with any changes to this method - if settings.profiling.advanced.force_enable_new_profiler + if enable_new_profiler?(settings) print_new_profiler_warnings recorder = Datadog::Profiling::StackRecorder.new( cpu_time_enabled: RUBY_PLATFORM.include?('linux'), # Only supported on Linux currently alloc_samples_enabled: false, # Always disabled for now -- work in progress ) collector = Datadog::Profiling::Collectors::CpuAndWallTimeWorker.new( recorder: recorder, max_frames: settings.profiling.advanced.max_frames, - tracer: tracer, - gc_profiling_enabled: should_enable_gc_profiling?(settings), + tracer: optional_tracer, + endpoint_collection_enabled: settings.profiling.advanced.endpoint.collection.enabled, + gc_profiling_enabled: enable_gc_profiling?(settings), allocation_counting_enabled: settings.profiling.advanced.allocation_counting_enabled, ) else - trace_identifiers_helper = Profiling::TraceIdentifiers::Helper.new( - tracer: tracer, - endpoint_collection_enabled: settings.profiling.advanced.endpoint.collection.enabled - ) - recorder = build_profiler_old_recorder(settings) - collector = build_profiler_oldstack_collector(settings, recorder, trace_identifiers_helper) + collector = build_profiler_oldstack_collector(settings, recorder, optional_tracer) end exporter = build_profiler_exporter(settings, recorder) transport = build_profiler_transport(settings, agent_settings) scheduler = Profiling::Scheduler.new(exporter: exporter, transport: transport) Profiling::Profiler.new([collector], scheduler) end - private - - def build_profiler_old_recorder(settings) + private_class_method def self.build_profiler_old_recorder(settings) Profiling::OldRecorder.new([Profiling::Events::StackSample], settings.profiling.advanced.max_events) end - def build_profiler_exporter(settings, recorder) + private_class_method def self.build_profiler_exporter(settings, recorder) code_provenance_collector = (Profiling::Collectors::CodeProvenance.new if settings.profiling.advanced.code_provenance_enabled) Profiling::Exporter.new(pprof_recorder: recorder, code_provenance_collector: code_provenance_collector) end - def build_profiler_oldstack_collector(settings, old_recorder, trace_identifiers_helper) + private_class_method def self.build_profiler_oldstack_collector(settings, old_recorder, tracer) + trace_identifiers_helper = Profiling::TraceIdentifiers::Helper.new( + tracer: tracer, + endpoint_collection_enabled: settings.profiling.advanced.endpoint.collection.enabled + ) + Profiling::Collectors::OldStack.new( old_recorder, trace_identifiers_helper: trace_identifiers_helper, max_frames: settings.profiling.advanced.max_frames ) end - def build_profiler_transport(settings, agent_settings) + private_class_method def self.build_profiler_transport(settings, agent_settings) settings.profiling.exporter.transport || Profiling::HttpTransport.new( agent_settings: agent_settings, site: settings.site, api_key: settings.api_key, upload_timeout_seconds: settings.profiling.upload.timeout_seconds, ) end - def should_enable_gc_profiling?(settings) + private_class_method def self.enable_gc_profiling?(settings) # See comments on the setting definition for more context on why it exists. if settings.profiling.advanced.force_enable_gc_profiling if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3') Datadog.logger.debug( 'Profiling time/resources spent in Garbage Collection force enabled. Do not use Ractors in combination ' \ @@ -137,24 +139,96 @@ else false end end - def print_new_profiler_warnings - if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.6') + private_class_method def self.print_new_profiler_warnings + return if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.6') + + # For more details on the issue, see the "BIG Issue" comment on `gvl_owner` function in + # `private_vm_api_access.c`. + Datadog.logger.warn( + 'The new CPU Profiling 2.0 profiler has been force-enabled on a legacy Ruby version (< 2.6). This is not ' \ + 'recommended in production environments, as due to limitations in Ruby APIs, we suspect it may lead to crashes ' \ + 'in very rare situations. Please report any issues you run into to Datadog support or ' \ + 'via <https://github.com/datadog/dd-trace-rb/issues/new>!' + ) + end + + private_class_method def self.enable_new_profiler?(settings) + if settings.profiling.advanced.force_enable_legacy_profiler Datadog.logger.warn( - 'New Ruby profiler has been force-enabled. This is a beta feature. Please report any issues ' \ - 'you run into to Datadog support or via <https://github.com/datadog/dd-trace-rb/issues/new>!' + 'Legacy profiler has been force-enabled via configuration. Do not use unless instructed to by support.' ) - else - # For more details on the issue, see the "BIG Issue" comment on `gvl_owner` function in - # `private_vm_api_access.c`. + return false + end + + return true if settings.profiling.advanced.force_enable_new_profiler + + return false if RUBY_VERSION.start_with?('2.3.', '2.4.', '2.5.') + + if Gem.loaded_specs['mysql2'] && incompatible_libmysqlclient_version?(settings) Datadog.logger.warn( - 'New Ruby profiler has been force-enabled on a legacy Ruby version (< 2.6). This is not recommended in ' \ - 'production environments, as due to limitations in Ruby APIs, we suspect it may lead to crashes in very ' \ - 'rare situations. Please report any issues you run into to Datadog support or ' \ - 'via <https://github.com/datadog/dd-trace-rb/issues/new>!' + 'Falling back to legacy profiler because an incompatible version of the mysql2 gem is installed. ' \ + 'Older versions of libmysqlclient (the C ' \ + 'library used by the mysql2 gem) have a bug in their signal handling code that the new profiler can trigger. ' \ + 'This bug (https://bugs.mysql.com/bug.php?id=83109) is fixed in libmysqlclient versions 8.0.0 and above. ' ) + return false + end + + if Gem.loaded_specs['rugged'] + Datadog.logger.warn( + 'Falling back to legacy profiler because rugged gem is installed. Some operations on this gem are ' \ + 'currently incompatible with the new CPU Profiling 2.0 profiler, as detailed in ' \ + '<https://github.com/datadog/dd-trace-rb/issues/2721>. If you still want to try out the new profiler, you ' \ + 'can force-enable it by using the `DD_PROFILING_FORCE_ENABLE_NEW` environment variable or the ' \ + '`c.profiling.advanced.force_enable_new_profiler` setting.' + ) + return false + end + + true + end + + # Versions of libmysqlclient prior to 8.0.0 are known to have buggy handling of system call interruptions. + # The profiler can sometimes cause system call interruptions, and so this combination can cause queries to fail. + # + # See https://bugs.mysql.com/bug.php?id=83109 and + # https://docs.datadoghq.com/profiler/profiler_troubleshooting/ruby/#unexpected-run-time-failures-and-errors-from-ruby-gems-that-use-native-extensions-in-dd-trace-rb-1110 + # for details. + # + # The `mysql2` gem's `info` method can be used to determine which `libmysqlclient` version is in use, and thus to + # detect if it's safe for the profiler to use signals or if we need to employ a fallback. + private_class_method def self.incompatible_libmysqlclient_version?(settings) + return true if settings.profiling.advanced.skip_mysql2_check + + Datadog.logger.debug( + 'Requiring `mysql2` to check if the `libmysqlclient` version it uses is compatible with profiling' + ) + + begin + require 'mysql2' + + return true unless defined?(Mysql2::Client) && Mysql2::Client.respond_to?(:info) + + libmysqlclient_version = Gem::Version.new(Mysql2::Client.info[:version]) + + compatible = libmysqlclient_version >= Gem::Version.new('8.0.0') + + Datadog.logger.debug( + "The `mysql2` gem is using #{compatible ? 'a compatible' : 'an incompatible'} version of " \ + "the `libmysqlclient` library (#{libmysqlclient_version})" + ) + + !compatible + rescue StandardError, LoadError => e + Datadog.logger.warn( + 'Failed to probe `mysql2` gem information. ' \ + "Cause: #{e.class.name} #{e.message} Location: #{Array(e.backtrace).first}" + ) + + true end end end end end