diff --git a/lib-injection/host_inject.rb b/lib-injection/host_inject.rb index b4d12f7a999..10a0aba5556 100644 --- a/lib-injection/host_inject.rb +++ b/lib-injection/host_inject.rb @@ -1,36 +1,39 @@ if ENV['DD_TRACE_SKIP_LIB_INJECTION'] == 'true' + # Skip else - begin - require 'rubygems' - require 'open3' - require 'bundler' - require 'bundler/cli' - require 'shellwords' - require 'fileutils' - require 'json' - - def dd_debug_log(msg) - pid = Process.respond_to?(:pid) ? Process.pid : 0 # Not available on all platforms + utils = Module.new do + module_function + + def debug(msg) $stdout.puts "[datadog][#{pid}][#{$0}] #{msg}" if ENV['DD_TRACE_DEBUG'] == 'true' end - def dd_error_log(msg) - pid = Process.respond_to?(:pid) ? Process.pid : 0 # Not available on all platforms + def error(msg) warn "[datadog][#{pid}][#{$0}] #{msg}" end - def dd_skip_injection! - ENV['DD_TRACE_SKIP_LIB_INJECTION'] = 'true' + def pid + Process.respond_to?(:pid) ? Process.pid : 0 end - def dd_send_telemetry(events) - pid = Process.respond_to?(:pid) ? Process.pid : 0 # Not available on all platforms + def path + major, minor, = RUBY_VERSION.split('.') + ruby_api_version = "#{major}.#{minor}.0" - tracer_version = if File.exist?('/opt/datadog/apm/library/ruby/version.txt') - File.read('/opt/datadog/apm/library/ruby/version.txt').chomp - else - 'unknown' - end + "/opt/datadog/apm/library/ruby/#{ruby_api_version}" + end + end + + telemetry = Module.new do + module_function + + def emit(pid, events) + tracer_version = + if File.exist?('/opt/datadog/apm/library/ruby/version.txt') + File.read('/opt/datadog/apm/library/ruby/version.txt').chomp + else + 'unknown' + end payload = { metadata: { @@ -42,181 +45,202 @@ def dd_send_telemetry(events) pid: pid }, points: events - }.to_json + } fowarder = ENV['DD_TELEMETRY_FORWARDER_PATH'] - return if fowarder.nil? || fowarder.empty? + if fowarder && !fowarder.empty? + require 'open3' + require 'json' - Open3.capture2e([fowarder, 'library_entrypoint'], stdin_data: payload) + Open3.capture2e([fowarder, 'library_entrypoint'], stdin_data: payload.to_json) + end end + end - precheck = Module.new do - module_function + pid = utils.pid - def in_bundle? - Bundler::SharedHelpers.in_bundle? - end + if Process.respond_to?(:fork) + utils.debug 'Starts injection' - def runtime_supported? - major, minor, = RUBY_VERSION.split('.') - ruby_api_version = "#{major}.#{minor}.0" + require 'rubygems' - supported_ruby_api_versions = ['2.7.0', '3.0.0', '3.1.0', '3.2.0'].freeze + read, write = IO.pipe - RUBY_ENGINE == 'ruby' && supported_ruby_api_versions.any? { |v| ruby_api_version == v } - end + fork do + read.close - def platform_supported? - platform_support_matrix = { - cpu: ['x86_64', 'aarch64'].freeze, - os: ['linux'].freeze, - version: ['gnu', nil].freeze # nil is equivalent to `gnu` for local platform - } - local_platform = Gem::Platform.local - - platform_support_matrix.fetch(:cpu).any? { |v| local_platform.cpu == v } && - platform_support_matrix.fetch(:os).any? { |v| local_platform.os == v } && - platform_support_matrix.fetch(:version).any? { |v| local_platform.version == v } - end + require 'open3' + require 'bundler' + require 'bundler/cli' + require 'fileutils' - def already_installed? - ['ddtrace', 'datadog'].any? do |gem| - fork do - $stdout = File.new('/dev/null', 'w') - $stderr = File.new('/dev/null', 'w') - Bundler::CLI::Common.select_spec(gem) - end - _, status = Process.wait2 - status.success? + precheck = Module.new do + module_function + + def in_bundle? + Bundler::SharedHelpers.in_bundle? end - end - def frozen_bundle? - Bundler.frozen_bundle? - end + def runtime_supported? + major, minor, = RUBY_VERSION.split('.') + ruby_api_version = "#{major}.#{minor}.0" - def bundler_supported? - Bundler::CLI.commands['add'] && Bundler::CLI.commands['add'].options.key?('require') - end - end - - case - when !precheck.in_bundle? - dd_debug_log 'Not in bundle... skipping injection' - when !precheck.runtime_supported? - dd_debug_log "Runtime not supported: #{RUBY_DESCRIPTION}" - dd_send_telemetry( - [ - { name: 'library_entrypoint.abort', tags: ['reason:incompatible_runtime'] }, - { name: 'library_entrypoint.abort.runtime' } - ] - ) - when !precheck.platform_supported? - dd_debug_log "Platform not supported: #{local_platform}" - dd_send_telemetry([{ name: 'library_entrypoint.abort', tags: ['reason:incompatible_platform'] }]) - when !Process.respond_to?(:fork) - dd_debug_log 'Fork not supported... skipping injection' - when precheck.already_installed? - dd_debug_log 'Skip injection: already installed' - when precheck.frozen_bundle? - dd_error_log "Skip injection: bundler is configured with 'deployment' or 'frozen'" - dd_send_telemetry([{ name: 'library_entrypoint.abort', tags: ['reason:bundler'] }]) - when !precheck.bundler_supported? - dd_error_log "Skip injection: bundler version #{Bundler::VERSION} is not supported, please upgrade to >= 2.3." - dd_send_telemetry([{ name: 'library_entrypoint.abort', tags: ['reason:bundler_version'] }]) - else - # Injection - major, minor, = RUBY_VERSION.split('.') - ruby_api_version = "#{major}.#{minor}.0" - dd_lib_injection_path = "/opt/datadog/apm/library/ruby/#{ruby_api_version}" - dd_debug_log "Loading from #{dd_lib_injection_path}..." - lock_file_parser = Bundler::LockfileParser.new(Bundler.read_file("#{dd_lib_injection_path}/Gemfile.lock")) - gem_version_mapping = lock_file_parser.specs.each_with_object({}) do |spec, hash| - hash[spec.name] = spec.version.to_s - hash - end + supported_ruby_api_versions = ['2.7.0', '3.0.0', '3.1.0', '3.2.0'].freeze - gemfile = Bundler::SharedHelpers.default_gemfile - lockfile = Bundler::SharedHelpers.default_lockfile - - datadog_gemfile = gemfile.dirname + '.datadog-Gemfile' - datadog_lockfile = lockfile.dirname + '.datadog-Gemfile.lock' - - # Copies for trial - ::FileUtils.cp gemfile, datadog_gemfile - ::FileUtils.cp lockfile, datadog_lockfile - - injection_failure = false - - # This is order dependent - [ - 'msgpack', - 'ffi', - 'debase-ruby_core_source', - 'libdatadog', - 'libddwaf', - 'datadog' - ].each do |gem| - fork do - $stdout = File.new('/dev/null', 'w') - $stderr = File.new('/dev/null', 'w') - Bundler::CLI::Common.select_spec(gem) + RUBY_ENGINE == 'ruby' && supported_ruby_api_versions.any? { |v| ruby_api_version == v } end - _, status = Process.wait2 - if status.success? - dd_debug_log "#{gem} already installed... skipping..." - next + def platform_supported? + platform_support_matrix = { + cpu: ['x86_64', 'aarch64'].freeze, + os: ['linux'].freeze, + version: ['gnu', nil].freeze # nil is equivalent to `gnu` for local platform + } + local_platform = Gem::Platform.local + + platform_support_matrix.fetch(:cpu).any? { |v| local_platform.cpu == v } && + platform_support_matrix.fetch(:os).any? { |v| local_platform.os == v } && + platform_support_matrix.fetch(:version).any? { |v| local_platform.version == v } end - bundle_add_cmd = "bundle add #{gem} --skip-install --version #{gem_version_mapping[gem]} " - bundle_add_cmd << '--require datadog/auto_instrument' if gem == 'datadog' - - dd_debug_log "Injection with `#{bundle_add_cmd}`" + def already_installed? + ['ddtrace', 'datadog'].any? do |gem| + fork do + $stdout = File.new('/dev/null', 'w') + $stderr = File.new('/dev/null', 'w') + Bundler::CLI::Common.select_spec(gem) + end + _, status = Process.wait2 + status.success? + end + end - env = { 'BUNDLE_GEMFILE' => datadog_gemfile.to_s, - 'DD_TRACE_SKIP_LIB_INJECTION' => 'true', - 'GEM_PATH' => dd_lib_injection_path } - add_output, add_status = Open3.capture2e(env, bundle_add_cmd) + def frozen_bundle? + Bundler.frozen_bundle? + end - if add_status.success? - dd_debug_log "Successfully injected #{gem} into the application." - else - injection_failure = true - dd_error_log "Injection failed: Unable to add datadog. Error output: #{add_output}" + def bundler_supported? + Bundler::CLI.commands['add'] && Bundler::CLI.commands['add'].options.key?('require') end end - if injection_failure - ::FileUtils.rm datadog_gemfile - ::FileUtils.rm datadog_lockfile - dd_send_telemetry([{ name: 'library_entrypoint.error', tags: ['error_type:injection_failure'] }]) + if !precheck.in_bundle? + utils.debug 'Not in bundle... skipping injection' + exit!(1) + elsif !precheck.runtime_supported? + utils.debug "Runtime not supported: #{RUBY_DESCRIPTION}" + telemetry.emit( + pid, + [{ name: 'library_entrypoint.abort', tags: ['reason:incompatible_runtime'] }, + { name: 'library_entrypoint.abort.runtime' }] + ) + exit!(1) + elsif !precheck.platform_supported? + utils.debug "Platform not supported: #{local_platform}" + telemetry.emit(pid, [{ name: 'library_entrypoint.abort', tags: ['reason:incompatible_platform'] }]) + exit!(1) + elsif precheck.already_installed? + utils.debug 'Skip injection: already installed' + elsif precheck.frozen_bundle? + utils.error "Skip injection: bundler is configured with 'deployment' or 'frozen'" + telemetry.emit(pid, [{ name: 'library_entrypoint.abort', tags: ['reason:bundler'] }]) + exit!(1) + elsif !precheck.bundler_supported? + utils.error "Skip injection: bundler version #{Bundler::VERSION} is not supported, please upgrade to >= 2.3." + telemetry.emit(pid, [{ name: 'library_entrypoint.abort', tags: ['reason:bundler_version'] }]) + exit!(1) else - # Look for pre-installed tracers - Gem.paths = { 'GEM_PATH' => "#{dd_lib_injection_path}:#{ENV['GEM_PATH']}" } + # Injection + path = utils.path + utils.debug "Loading from #{path}" + lock_file_parser = Bundler::LockfileParser.new(Bundler.read_file("#{path}/Gemfile.lock")) + gem_version_mapping = lock_file_parser.specs.each_with_object({}) do |spec, hash| + hash[spec.name] = spec.version.to_s + hash + end - # Also apply to the environment variable, to guarantee any spawned processes will respected the modified `GEM_PATH`. - ENV['GEM_PATH'] = Gem.path.join(':') - ENV['BUNDLE_GEMFILE'] = datadog_gemfile.to_s + gemfile = Bundler::SharedHelpers.default_gemfile + lockfile = Bundler::SharedHelpers.default_lockfile - dd_send_telemetry([{ name: 'library_entrypoint.complete', tags: ['injection_forced:false'] }]) - end - end - dd_skip_injection! - rescue Exception => e - if respond_to?(:dd_send_telemetry) - dd_send_telemetry( + datadog_gemfile = gemfile.dirname + '.datadog-Gemfile' + datadog_lockfile = lockfile.dirname + '.datadog-Gemfile.lock' + + # Copies for trial + ::FileUtils.cp gemfile, datadog_gemfile + ::FileUtils.cp lockfile, datadog_lockfile + + injection_failure = false + + # This is order dependent [ - { name: 'library_entrypoint.error', - tags: ["error_type:#{e.class.name}"] } - ] - ) + 'msgpack', + 'ffi', + 'debase-ruby_core_source', + 'libdatadog', + 'libddwaf', + 'datadog' + ].each do |gem| + fork do + $stdout = File.new('/dev/null', 'w') + $stderr = File.new('/dev/null', 'w') + Bundler::CLI::Common.select_spec(gem) + end + + _, status = Process.wait2 + if status.success? + utils.debug "#{gem} already installed... skipping..." + next + end + + bundle_add_cmd = "bundle add #{gem} --skip-install --version #{gem_version_mapping[gem]} " + bundle_add_cmd << '--require datadog/auto_instrument' if gem == 'datadog' + + utils.debug "Injection with `#{bundle_add_cmd}`" + + env = { 'BUNDLE_GEMFILE' => datadog_gemfile.to_s, + 'DD_TRACE_SKIP_LIB_INJECTION' => 'true', + 'GEM_PATH' => utils.path } + add_output, add_status = Open3.capture2e(env, bundle_add_cmd) + + if add_status.success? + utils.debug "Successfully injected #{gem} into the application." + else + injection_failure = true + utils.error "Injection failed: Unable to add datadog. Error output: #{add_output}" + end + end + + if injection_failure + ::FileUtils.rm datadog_gemfile + ::FileUtils.rm datadog_lockfile + telemetry.emit(pid, [{ name: 'library_entrypoint.error', tags: ['error_type:injection_failure'] }]) + exit!(1) + else + write.puts datadog_gemfile.to_s + telemetry.emit(pid, [{ name: 'library_entrypoint.complete', tags: ['injection_forced:false'] }]) + end + end end - pid = Process.respond_to?(:pid) ? Process.pid : 0 # Not available on all platforms - warn "[datadog][#{pid}][#{$0}] Injection failed: #{e.class.name} #{e.message}\nBacktrace: #{e.backtrace.join("\n")}" - # Skip injection if the environment variable is set + write.close + gemfile = read.read.to_s.chomp + + _, status = Process.wait2 ENV['DD_TRACE_SKIP_LIB_INJECTION'] = 'true' + + if status.success? + dd_lib_injection_path = utils.path + + Gem.paths = { 'GEM_PATH' => "#{dd_lib_injection_path}:#{ENV['GEM_PATH']}" } + ENV['GEM_PATH'] = Gem.path.join(':') + ENV['BUNDLE_GEMFILE'] = gemfile + utils.debug "Fork success: Using Gemfile `#{gemfile}`" + else + utils.debug 'Fork abort' + end + else + utils.debug 'Fork not supported... skipping injection' + telemetry.emit(pid, [{ name: 'library_entrypoint.abort', tags: ['reason:fork_not_supported'] }]) end end