# typed: true require 'datadog/core/environment/variable_helpers' require 'ddtrace/utils/only_once' module Datadog # Contains profiler for generating stack profiles, etc. module Profiling # rubocop:disable Metrics/ModuleLength GOOGLE_PROTOBUF_MINIMUM_VERSION = Gem::Version.new('3.0') private_constant :GOOGLE_PROTOBUF_MINIMUM_VERSION SKIPPED_NATIVE_EXTENSION_ONLY_ONCE = Datadog::Utils::OnlyOnce.new private_constant :SKIPPED_NATIVE_EXTENSION_ONLY_ONCE def self.supported? unsupported_reason.nil? end def self.unsupported_reason # NOTE: Only the first matching reason is returned, so try to keep a nice order on reasons -- e.g. tell users # first that they can't use this on JRuby before telling them that they are missing protobuf ruby_engine_unsupported? || native_library_failed_to_load? || protobuf_gem_unavailable? || protobuf_version_unsupported? || protobuf_failed_to_load? end private_class_method def self.ruby_engine_unsupported? 'JRuby is not supported' if RUBY_ENGINE == 'jruby' end private_class_method def self.protobuf_gem_unavailable? # NOTE: On environments where protobuf is already loaded, we skip the check. This allows us to support environments # where no Gem.loaded_version is NOT available but customers are able to load protobuf; see for instance # https://github.com/teamcapybara/capybara/commit/caf3bcd7664f4f2691d0ca9ef3be9a2a954fecfb if !defined?(::Google::Protobuf) && Gem.loaded_specs['google-protobuf'].nil? "Missing google-protobuf dependency; please add `gem 'google-protobuf', '~> 3.0'` to your Gemfile or gems.rb file" end end private_class_method def self.protobuf_version_unsupported? # See above for why we skip the check when protobuf is already loaded; note that when protobuf was already loaded # we skip the version check to avoid the call to Gem.loaded_specs. Unfortunately, protobuf does not seem to # expose the gem version constant elsewhere, so in that setup we are not able to check the version. if !defined?(::Google::Protobuf) && Gem.loaded_specs['google-protobuf'].version < GOOGLE_PROTOBUF_MINIMUM_VERSION 'Your google-protobuf is too old; ensure that you have google-protobuf >= 3.0 by ' \ "adding `gem 'google-protobuf', '~> 3.0'` to your Gemfile or gems.rb file" end end private_class_method def self.protobuf_failed_to_load? unless protobuf_loaded_successfully? 'There was an error loading the google-protobuf library; see previous warning message for details' end end # The `google-protobuf` gem depends on a native component, and its creators helpfully tried to provide precompiled # versions of this extension on rubygems.org. # # Unfortunately, for a long time, the supported Ruby versions metadata on these precompiled versions of the extension # was not correctly set. (This is fixed in newer versions -- but not all Ruby versions we want to support can use # these.) # # Thus, the gem can still be installed, but can be in a broken state. To avoid breaking customer applications, we # use this helper to load it and gracefully handle failures. private_class_method def self.protobuf_loaded_successfully? return @protobuf_loaded if defined?(@protobuf_loaded) begin require 'google/protobuf' @protobuf_loaded = true rescue LoadError => e # NOTE: We use Kernel#warn here because this code gets run BEFORE Datadog.logger is actually set up. # In the future it'd be nice to shuffle the logger startup to happen first to avoid this special case. Kernel.warn( '[DDTRACE] Error while loading google-protobuf gem. ' \ "Cause: '#{e.message}' Location: '#{Array(e.backtrace).first}'. " \ 'This can happen when google-protobuf is missing its native components. ' \ 'To fix this, try removing and reinstalling the gem, forcing it to recompile the components: ' \ '`gem uninstall google-protobuf -a; BUNDLE_FORCE_RUBY_PLATFORM=true bundle install`. ' \ 'If the error persists, please contact support via or ' \ 'file a bug at .' ) @protobuf_loaded = false end end private_class_method def self.native_library_failed_to_load? success, exception = try_loading_native_library unless success if exception 'There was an error loading the profiling native extension due to ' \ "'#{exception.message}' at '#{exception.backtrace.first}'" else 'The profiling native extension did not load correctly. ' \ 'If the error persists, please contact support via or ' \ 'file a bug at .' end end end private_class_method def self.try_loading_native_library if Datadog::Core::Environment::VariableHelpers.env_to_bool('DD_PROFILING_NO_EXTENSION', false) SKIPPED_NATIVE_EXTENSION_ONLY_ONCE.run do Kernel.warn( '[DDTRACE] Skipped loading of profiling native extension due to DD_PROFILING_NO_EXTENSION environment ' \ 'variable being set. ' \ 'This option is experimental and will lead to the profiler not working in future releases. ' \ 'If you needed to use this, please tell us why on .' ) end return [true, nil] end begin require "ddtrace_profiling_native_extension.#{RUBY_VERSION}_#{RUBY_PLATFORM}" success = defined?(Datadog::Profiling::NativeExtension) && Datadog::Profiling::NativeExtension.send(:native_working?) [success, nil] rescue StandardError, LoadError => e [false, e] end end private_class_method def self.load_profiling return false unless supported? require 'ddtrace/profiling/ext/forking' require 'ddtrace/profiling/collectors/stack' require 'ddtrace/profiling/exporter' require 'ddtrace/profiling/recorder' require 'ddtrace/profiling/scheduler' require 'ddtrace/profiling/tasks/setup' require 'ddtrace/profiling/transport/io' require 'ddtrace/profiling/transport/http' require 'ddtrace/profiling/profiler' require 'ddtrace/profiling/native_extension' require 'ddtrace/profiling/trace_identifiers/helper' require 'ddtrace/profiling/pprof/pprof_pb' true end load_profiling if supported? end end