From 99036d964e777cbd91222bf9a936f7d59a2fceac Mon Sep 17 00:00:00 2001 From: Tony Hsu Date: Mon, 22 Jul 2024 14:58:27 +0200 Subject: [PATCH 1/6] Wrap with forked process --- lib-injection/host_inject.rb | 76 ++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 39 deletions(-) diff --git a/lib-injection/host_inject.rb b/lib-injection/host_inject.rb index b4d12f7a999..12ead4fe840 100644 --- a/lib-injection/host_inject.rb +++ b/lib-injection/host_inject.rb @@ -1,7 +1,18 @@ if ENV['DD_TRACE_SKIP_LIB_INJECTION'] == 'true' + # Skip +elsif !Process.respond_to?(:fork) + pid = Process.respond_to?(:pid) ? Process.pid : 0 # Not available on all platforms + $stdout.puts "[datadog][#{pid}][#{$0}] Fork not supported... skipping injection" if ENV['DD_TRACE_DEBUG'] == 'true' else - begin - require 'rubygems' + pid = Process.respond_to?(:pid) ? Process.pid : 0 # Not available on all platforms + $stdout.puts "[datadog][#{pid}][#{$0}] Starts injection" if ENV['DD_TRACE_DEBUG'] == 'true' + require 'rubygems' + + read, write = IO.pipe + + Process.fork do + read.close + require 'open3' require 'bundler' require 'bundler/cli' @@ -19,18 +30,14 @@ def dd_error_log(msg) warn "[datadog][#{pid}][#{$0}] #{msg}" end - def dd_skip_injection! - ENV['DD_TRACE_SKIP_LIB_INJECTION'] = 'true' - end - def dd_send_telemetry(events) pid = Process.respond_to?(:pid) ? Process.pid : 0 # Not available on all platforms 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 + File.read('/opt/datadog/apm/library/ruby/version.txt').chomp + else + 'unknown' + end payload = { metadata: { @@ -106,17 +113,13 @@ def bundler_supported? 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' } - ] - ) + 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? @@ -193,30 +196,25 @@ def bundler_supported? ::FileUtils.rm datadog_lockfile dd_send_telemetry([{ name: 'library_entrypoint.error', tags: ['error_type:injection_failure'] }]) else - # Look for pre-installed tracers - Gem.paths = { 'GEM_PATH' => "#{dd_lib_injection_path}:#{ENV['GEM_PATH']}" } - - # 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 - + write.puts datadog_gemfile 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( - [ - { name: 'library_entrypoint.error', - tags: ["error_type:#{e.class.name}"] } - ] - ) - 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")}" + end + + write.close + result = read.read + + _, status = Process.wait2 + ENV['DD_TRACE_SKIP_LIB_INJECTION'] = 'true' + + if status.success? + major, minor, = RUBY_VERSION.split('.') + ruby_api_version = "#{major}.#{minor}.0" + dd_lib_injection_path = "/opt/datadog/apm/library/ruby/#{ruby_api_version}" - # Skip injection if the environment variable is set - ENV['DD_TRACE_SKIP_LIB_INJECTION'] = 'true' + Gem.paths = { 'GEM_PATH' => "#{dd_lib_injection_path}:#{ENV['GEM_PATH']}" } + ENV['GEM_PATH'] = Gem.path.join(':') + ENV['BUNDLE_GEMFILE'] = result.to_s.chomp end end From 0cdf0f9d1edb0d0c5a5169426571ae23690b87b1 Mon Sep 17 00:00:00 2001 From: Tony Hsu Date: Tue, 23 Jul 2024 13:11:00 +0200 Subject: [PATCH 2/6] Refactor --- lib-injection/host_inject.rb | 165 ++++++++++++++++++++--------------- 1 file changed, 93 insertions(+), 72 deletions(-) diff --git a/lib-injection/host_inject.rb b/lib-injection/host_inject.rb index 12ead4fe840..b6771547548 100644 --- a/lib-injection/host_inject.rb +++ b/lib-injection/host_inject.rb @@ -1,16 +1,38 @@ +module DatadogInjectUtils + module_function + + def debug(msg) + $stdout.puts "[datadog][#{pid}][#{$0}] #{msg}" if ENV['DD_TRACE_DEBUG'] == 'true' + end + + def error(msg) + warn "[datadog][#{pid}][#{$0}] #{msg}" + end + + def pid + Process.respond_to?(:pid) ? Process.pid : 0 + end + + def path + major, minor, = RUBY_VERSION.split('.') + ruby_api_version = "#{major}.#{minor}.0" + + "/opt/datadog/apm/library/ruby/#{ruby_api_version}" + end +end + if ENV['DD_TRACE_SKIP_LIB_INJECTION'] == 'true' # Skip elsif !Process.respond_to?(:fork) - pid = Process.respond_to?(:pid) ? Process.pid : 0 # Not available on all platforms - $stdout.puts "[datadog][#{pid}][#{$0}] Fork not supported... skipping injection" if ENV['DD_TRACE_DEBUG'] == 'true' + DatadogInjectUtils.debug 'Fork not supported... skipping injection' else - pid = Process.respond_to?(:pid) ? Process.pid : 0 # Not available on all platforms - $stdout.puts "[datadog][#{pid}][#{$0}] Starts injection" if ENV['DD_TRACE_DEBUG'] == 'true' + DatadogInjectUtils.debug 'Starts injection' + require 'rubygems' read, write = IO.pipe - Process.fork do + fork do read.close require 'open3' @@ -20,42 +42,37 @@ require 'fileutils' require 'json' - def dd_debug_log(msg) - pid = Process.respond_to?(:pid) ? Process.pid : 0 # Not available on all platforms - $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 - warn "[datadog][#{pid}][#{$0}] #{msg}" - end + telemetry = Module.new do + module_function - def dd_send_telemetry(events) - pid = Process.respond_to?(:pid) ? Process.pid : 0 # Not available on all platforms + def emit(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 - 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: { + language_name: 'ruby', + language_version: RUBY_VERSION, + runtime_name: RUBY_ENGINE, + runtime_version: RUBY_VERSION, + tracer_version: tracer_version, + pid: DatadogInjectUtils.pid + }, + points: events + }.to_json - payload = { - metadata: { - language_name: 'ruby', - language_version: RUBY_VERSION, - runtime_name: RUBY_ENGINE, - runtime_version: RUBY_VERSION, - tracer_version: tracer_version, - pid: pid - }, - points: events - }.to_json + DatadogInjectUtils.debug "Telemetry: #{payload}" - fowarder = ENV['DD_TELEMETRY_FORWARDER_PATH'] + fowarder = ENV['DD_TELEMETRY_FORWARDER_PATH'] - return if fowarder.nil? || fowarder.empty? + return if fowarder.nil? || fowarder.empty? - Open3.capture2e([fowarder, 'library_entrypoint'], stdin_data: payload) + Open3.capture2e([fowarder, 'library_entrypoint'], stdin_data: payload) + end end precheck = Module.new do @@ -108,33 +125,35 @@ def bundler_supported? 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 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'] }]) + if !precheck.in_bundle? + DatadogInjectUtils.debug 'Not in bundle... skipping injection' + abort + elsif !precheck.runtime_supported? + DatadogInjectUtils.debug "Runtime not supported: #{RUBY_DESCRIPTION}" + telemetry.emit( + [{ name: 'library_entrypoint.abort', tags: ['reason:incompatible_runtime'] }, + { name: 'library_entrypoint.abort.runtime' }] + ) + abort + elsif !precheck.platform_supported? + DatadogInjectUtils.debug "Platform not supported: #{local_platform}" + telemetry.emit([{ name: 'library_entrypoint.abort', tags: ['reason:incompatible_platform'] }]) + abort + elsif precheck.already_installed? + DatadogInjectUtils.debug 'Skip injection: already installed' + elsif precheck.frozen_bundle? + DatadogInjectUtils.error "Skip injection: bundler is configured with 'deployment' or 'frozen'" + telemetry.emit([{ name: 'library_entrypoint.abort', tags: ['reason:bundler'] }]) + abort + elsif !precheck.bundler_supported? + DatadogInjectUtils.error "Skip injection: bundler version #{Bundler::VERSION} is not supported, please upgrade to >= 2.3." + telemetry.emit([{ name: 'library_entrypoint.abort', tags: ['reason:bundler_version'] }]) + abort 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")) + path = DatadogInjectUtils.path + DatadogInjectUtils.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 @@ -169,52 +188,54 @@ def bundler_supported? _, status = Process.wait2 if status.success? - dd_debug_log "#{gem} already installed... skipping..." + DatadogInjectUtils.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' - dd_debug_log "Injection with `#{bundle_add_cmd}`" + DatadogInjectUtils.debug "Injection with `#{bundle_add_cmd}`" env = { 'BUNDLE_GEMFILE' => datadog_gemfile.to_s, 'DD_TRACE_SKIP_LIB_INJECTION' => 'true', - 'GEM_PATH' => dd_lib_injection_path } + 'GEM_PATH' => DatadogInjectUtils.path } add_output, add_status = Open3.capture2e(env, bundle_add_cmd) if add_status.success? - dd_debug_log "Successfully injected #{gem} into the application." + DatadogInjectUtils.debug "Successfully injected #{gem} into the application." else injection_failure = true - dd_error_log "Injection failed: Unable to add datadog. Error output: #{add_output}" + DatadogInjectUtils.error "Injection failed: Unable to add datadog. Error output: #{add_output}" 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'] }]) + telemetry.emit([{ name: 'library_entrypoint.error', tags: ['error_type:injection_failure'] }]) + abort else write.puts datadog_gemfile - dd_send_telemetry([{ name: 'library_entrypoint.complete', tags: ['injection_forced:false'] }]) + telemetry.emit([{ name: 'library_entrypoint.complete', tags: ['injection_forced:false'] }]) end end end write.close - result = read.read + gemfile = read.read.to_s.chomp _, status = Process.wait2 ENV['DD_TRACE_SKIP_LIB_INJECTION'] = 'true' if status.success? - major, minor, = RUBY_VERSION.split('.') - ruby_api_version = "#{major}.#{minor}.0" - dd_lib_injection_path = "/opt/datadog/apm/library/ruby/#{ruby_api_version}" + dd_lib_injection_path = DatadogInjectUtils.path Gem.paths = { 'GEM_PATH' => "#{dd_lib_injection_path}:#{ENV['GEM_PATH']}" } ENV['GEM_PATH'] = Gem.path.join(':') - ENV['BUNDLE_GEMFILE'] = result.to_s.chomp + ENV['BUNDLE_GEMFILE'] = gemfile + DatadogInjectUtils.debug "Fork success: Using Gemfile `#{gemfile}`" + else + DatadogInjectUtils.debug 'Fork abort' end end From 11398e3d30bb93b3c8d0fe858b1a1b7959a74fbc Mon Sep 17 00:00:00 2001 From: Tony Hsu Date: Wed, 24 Jul 2024 16:09:17 +0200 Subject: [PATCH 3/6] Use exit! --- lib-injection/host_inject.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib-injection/host_inject.rb b/lib-injection/host_inject.rb index b6771547548..bf5e99cb77d 100644 --- a/lib-injection/host_inject.rb +++ b/lib-injection/host_inject.rb @@ -127,28 +127,28 @@ def bundler_supported? if !precheck.in_bundle? DatadogInjectUtils.debug 'Not in bundle... skipping injection' - abort + exit!(1) elsif !precheck.runtime_supported? DatadogInjectUtils.debug "Runtime not supported: #{RUBY_DESCRIPTION}" telemetry.emit( [{ name: 'library_entrypoint.abort', tags: ['reason:incompatible_runtime'] }, { name: 'library_entrypoint.abort.runtime' }] ) - abort + exit!(1) elsif !precheck.platform_supported? DatadogInjectUtils.debug "Platform not supported: #{local_platform}" telemetry.emit([{ name: 'library_entrypoint.abort', tags: ['reason:incompatible_platform'] }]) - abort + exit!(1) elsif precheck.already_installed? DatadogInjectUtils.debug 'Skip injection: already installed' elsif precheck.frozen_bundle? DatadogInjectUtils.error "Skip injection: bundler is configured with 'deployment' or 'frozen'" telemetry.emit([{ name: 'library_entrypoint.abort', tags: ['reason:bundler'] }]) - abort + exit!(1) elsif !precheck.bundler_supported? DatadogInjectUtils.error "Skip injection: bundler version #{Bundler::VERSION} is not supported, please upgrade to >= 2.3." telemetry.emit([{ name: 'library_entrypoint.abort', tags: ['reason:bundler_version'] }]) - abort + exit!(1) else # Injection path = DatadogInjectUtils.path @@ -214,7 +214,7 @@ def bundler_supported? ::FileUtils.rm datadog_gemfile ::FileUtils.rm datadog_lockfile telemetry.emit([{ name: 'library_entrypoint.error', tags: ['error_type:injection_failure'] }]) - abort + exit!(1) else write.puts datadog_gemfile telemetry.emit([{ name: 'library_entrypoint.complete', tags: ['injection_forced:false'] }]) From b77cab03e7adfb462282ab445b82ca09e057d85f Mon Sep 17 00:00:00 2001 From: Tony Hsu Date: Wed, 24 Jul 2024 16:34:18 +0200 Subject: [PATCH 4/6] Refactor with local scope --- lib-injection/host_inject.rb | 370 ++++++++++++++++++----------------- 1 file changed, 192 insertions(+), 178 deletions(-) diff --git a/lib-injection/host_inject.rb b/lib-injection/host_inject.rb index bf5e99cb77d..3bded998ae3 100644 --- a/lib-injection/host_inject.rb +++ b/lib-injection/host_inject.rb @@ -1,51 +1,34 @@ -module DatadogInjectUtils - module_function - - def debug(msg) - $stdout.puts "[datadog][#{pid}][#{$0}] #{msg}" if ENV['DD_TRACE_DEBUG'] == 'true' - end - - def error(msg) - warn "[datadog][#{pid}][#{$0}] #{msg}" - end - - def pid - Process.respond_to?(:pid) ? Process.pid : 0 - end - - def path - major, minor, = RUBY_VERSION.split('.') - ruby_api_version = "#{major}.#{minor}.0" - - "/opt/datadog/apm/library/ruby/#{ruby_api_version}" - end -end - if ENV['DD_TRACE_SKIP_LIB_INJECTION'] == 'true' # Skip -elsif !Process.respond_to?(:fork) - DatadogInjectUtils.debug 'Fork not supported... skipping injection' else - DatadogInjectUtils.debug 'Starts injection' + utils = Module.new do + module_function - require 'rubygems' + def debug(msg) + $stdout.puts "[datadog][#{pid}][#{$0}] #{msg}" if ENV['DD_TRACE_DEBUG'] == 'true' + end - read, write = IO.pipe + def error(msg) + warn "[datadog][#{pid}][#{$0}] #{msg}" + end + + def pid + Process.respond_to?(:pid) ? Process.pid : 0 + end - fork do - read.close + def path + major, minor, = RUBY_VERSION.split('.') + ruby_api_version = "#{major}.#{minor}.0" - require 'open3' - require 'bundler' - require 'bundler/cli' - require 'shellwords' - require 'fileutils' - require 'json' + "/opt/datadog/apm/library/ruby/#{ruby_api_version}" + end + end - telemetry = Module.new do - module_function + telemetry = Module.new do + module_function - def emit(events) + def emit(pid, events) + callable = lambda do tracer_version = if File.exist?('/opt/datadog/apm/library/ruby/version.txt') File.read('/opt/datadog/apm/library/ruby/version.txt').chomp @@ -60,182 +43,213 @@ def emit(events) runtime_name: RUBY_ENGINE, runtime_version: RUBY_VERSION, tracer_version: tracer_version, - pid: DatadogInjectUtils.pid + pid: pid }, points: events - }.to_json - - DatadogInjectUtils.debug "Telemetry: #{payload}" + } 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 + + if Process.respond_to?(:fork) + fork(&callable) + Process.wait2 + else + callable.call 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 + require 'open3' + require 'bundler' + require 'bundler/cli' + require 'fileutils' - 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 + precheck = Module.new do + module_function - 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? + 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 + supported_ruby_api_versions = ['2.7.0', '3.0.0', '3.1.0', '3.2.0'].freeze - if !precheck.in_bundle? - DatadogInjectUtils.debug 'Not in bundle... skipping injection' - exit!(1) - elsif !precheck.runtime_supported? - DatadogInjectUtils.debug "Runtime not supported: #{RUBY_DESCRIPTION}" - telemetry.emit( - [{ name: 'library_entrypoint.abort', tags: ['reason:incompatible_runtime'] }, - { name: 'library_entrypoint.abort.runtime' }] - ) - exit!(1) - elsif !precheck.platform_supported? - DatadogInjectUtils.debug "Platform not supported: #{local_platform}" - telemetry.emit([{ name: 'library_entrypoint.abort', tags: ['reason:incompatible_platform'] }]) - exit!(1) - elsif precheck.already_installed? - DatadogInjectUtils.debug 'Skip injection: already installed' - elsif precheck.frozen_bundle? - DatadogInjectUtils.error "Skip injection: bundler is configured with 'deployment' or 'frozen'" - telemetry.emit([{ name: 'library_entrypoint.abort', tags: ['reason:bundler'] }]) - exit!(1) - elsif !precheck.bundler_supported? - DatadogInjectUtils.error "Skip injection: bundler version #{Bundler::VERSION} is not supported, please upgrade to >= 2.3." - telemetry.emit([{ name: 'library_entrypoint.abort', tags: ['reason:bundler_version'] }]) - exit!(1) - else - # Injection - path = DatadogInjectUtils.path - DatadogInjectUtils.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 - - 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? - DatadogInjectUtils.debug "#{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' - - DatadogInjectUtils.debug "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' => DatadogInjectUtils.path } - add_output, add_status = Open3.capture2e(env, bundle_add_cmd) + def frozen_bundle? + Bundler.frozen_bundle? + end - if add_status.success? - DatadogInjectUtils.debug "Successfully injected #{gem} into the application." - else - injection_failure = true - DatadogInjectUtils.error "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 - telemetry.emit([{ 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 - write.puts datadog_gemfile - telemetry.emit([{ name: 'library_entrypoint.complete', tags: ['injection_forced:false'] }]) + # 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 + + 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) + 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 + telemetry.emit(pid, [{ name: 'library_entrypoint.complete', tags: ['injection_forced:false'] }]) + end end end - end - write.close - gemfile = read.read.to_s.chomp + write.close + gemfile = read.read.to_s.chomp - _, status = Process.wait2 - ENV['DD_TRACE_SKIP_LIB_INJECTION'] = 'true' + _, status = Process.wait2 + ENV['DD_TRACE_SKIP_LIB_INJECTION'] = 'true' - if status.success? - dd_lib_injection_path = DatadogInjectUtils.path + 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 - DatadogInjectUtils.debug "Fork success: Using Gemfile `#{gemfile}`" + 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 - DatadogInjectUtils.debug 'Fork abort' + utils.debug 'Fork not supported... skipping injection' + telemetry.emit(pid, [{ name: 'library_entrypoint.abort', tags: ['reason:fork_not_supported'] }]) end end From 53dad7983f019a4bec44eb9d6db56e1c22e7d738 Mon Sep 17 00:00:00 2001 From: Tony Hsu Date: Wed, 24 Jul 2024 23:09:15 +0200 Subject: [PATCH 5/6] Write string --- lib-injection/host_inject.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib-injection/host_inject.rb b/lib-injection/host_inject.rb index 3bded998ae3..c8256b80761 100644 --- a/lib-injection/host_inject.rb +++ b/lib-injection/host_inject.rb @@ -226,7 +226,7 @@ def bundler_supported? telemetry.emit(pid, [{ name: 'library_entrypoint.error', tags: ['error_type:injection_failure'] }]) exit!(1) else - write.puts datadog_gemfile + write.puts datadog_gemfile.to_s telemetry.emit(pid, [{ name: 'library_entrypoint.complete', tags: ['injection_forced:false'] }]) end end From 1badd782b77ea49b0311aa91e792d693ef18ec4b Mon Sep 17 00:00:00 2001 From: Tony Hsu Date: Thu, 25 Jul 2024 12:33:11 +0200 Subject: [PATCH 6/6] Simplify telemetry --- lib-injection/host_inject.rb | 57 +++++++++++++++--------------------- 1 file changed, 24 insertions(+), 33 deletions(-) diff --git a/lib-injection/host_inject.rb b/lib-injection/host_inject.rb index c8256b80761..10a0aba5556 100644 --- a/lib-injection/host_inject.rb +++ b/lib-injection/host_inject.rb @@ -28,41 +28,32 @@ def path module_function def emit(pid, events) - callable = lambda do - 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: { - language_name: 'ruby', - language_version: RUBY_VERSION, - runtime_name: RUBY_ENGINE, - runtime_version: RUBY_VERSION, - tracer_version: tracer_version, - pid: pid - }, - points: events - } - - fowarder = ENV['DD_TELEMETRY_FORWARDER_PATH'] - - if fowarder && !fowarder.empty? - require 'open3' - require 'json' - - Open3.capture2e([fowarder, 'library_entrypoint'], stdin_data: payload.to_json) + 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 - end - if Process.respond_to?(:fork) - fork(&callable) - Process.wait2 - else - callable.call + payload = { + metadata: { + language_name: 'ruby', + language_version: RUBY_VERSION, + runtime_name: RUBY_ENGINE, + runtime_version: RUBY_VERSION, + tracer_version: tracer_version, + pid: pid + }, + points: events + } + + fowarder = ENV['DD_TELEMETRY_FORWARDER_PATH'] + + if fowarder && !fowarder.empty? + require 'open3' + require 'json' + + Open3.capture2e([fowarder, 'library_entrypoint'], stdin_data: payload.to_json) end end end