diff --git a/lib/datadog/core/configuration/components.rb b/lib/datadog/core/configuration/components.rb index 98ff497661..9a18221328 100644 --- a/lib/datadog/core/configuration/components.rb +++ b/lib/datadog/core/configuration/components.rb @@ -12,6 +12,7 @@ require_relative '../workers/runtime_metrics' require_relative '../remote/component' require_relative '../utils/at_fork_monkey_patch' +require_relative '../utils/spawn_monkey_patch' require_relative '../utils/only_once' require_relative '../../tracing/component' require_relative '../../profiling/component' @@ -22,6 +23,7 @@ require_relative '../../error_tracking/component' require_relative '../crashtracking/component' require_relative '../environment/agent_info' +require_relative '../environment/identity' require_relative '../process_discovery' require_relative '../../data_streams/processor' @@ -31,7 +33,7 @@ module Configuration # Global components for the trace library. class Components # Class-level constant to ensure fork patch is applied only once - AT_FORK_ONLY_ONCE = Utils::OnlyOnce.new + PATCH_ONLY_ONCE = Utils::OnlyOnce.new class << self def build_health_metrics(settings, logger, telemetry) @@ -128,8 +130,11 @@ def initialize(settings) Deprecations.log_deprecations_from_all_sources(@logger) # Register fork handling once globally - self.class::AT_FORK_ONLY_ONCE.run do + self.class::PATCH_ONLY_ONCE.run do Utils::AtForkMonkeyPatch.apply! + Utils::SpawnMonkeyPatch.apply!( + lineage_envs_provider: Core::Environment::Identity.method(:runtime_propagation_envs), + ) # Register callback that calls Components.after_fork Utils::AtForkMonkeyPatch.at_fork(:child) do diff --git a/lib/datadog/core/configuration/config_helper.rb b/lib/datadog/core/configuration/config_helper.rb index f7d6d116f2..fcef1b46cb 100644 --- a/lib/datadog/core/configuration/config_helper.rb +++ b/lib/datadog/core/configuration/config_helper.rb @@ -40,6 +40,12 @@ def key?(name) !get_environment_variable(name).nil? end + # Returns the source environment as a Hash. Used when the full environment + # must be passed through (e.g. Process.spawn child env). + def to_h + @source_env.to_h + end + alias_method :has_key?, :key? alias_method :include?, :key? alias_method :member?, :key? diff --git a/lib/datadog/core/environment/identity.rb b/lib/datadog/core/environment/identity.rb index b39bd39a7c..5b34c8f732 100644 --- a/lib/datadog/core/environment/identity.rb +++ b/lib/datadog/core/environment/identity.rb @@ -13,18 +13,40 @@ module Environment module Identity extend Core::Utils::Forking + ENV_ROOT_SESSION_ID = '_DD_ROOT_RB_SESSION_ID' + ENV_PARENT_SESSION_ID = '_DD_PARENT_RB_SESSION_ID' + module_function - # Retrieves number of classes from runtime + env = defined?(::Datadog::DATADOG_ENV) ? ::Datadog::DATADOG_ENV : ENV # rubocop:disable CustomCops/EnvUsageCop + @root_runtime_id = env[ENV_ROOT_SESSION_ID]&.freeze + @parent_runtime_id = env[ENV_PARENT_SESSION_ID]&.freeze + def id @id ||= ::SecureRandom.uuid.freeze - # Check if runtime has changed, e.g. forked. - after_fork! { @id = ::SecureRandom.uuid.freeze } + after_fork! do + # Order matters: capture @id before overwriting + @parent_runtime_id = @id + @root_runtime_id ||= @id + @id = ::SecureRandom.uuid.freeze + end @id end + def root_runtime_id + @root_runtime_id + end + + def parent_runtime_id + @parent_runtime_id + end + + def runtime_propagation_envs + {ENV_ROOT_SESSION_ID => root_runtime_id || id, ENV_PARENT_SESSION_ID => id}.freeze + end + def pid ::Process.pid end diff --git a/lib/datadog/core/telemetry/transport/http/telemetry.rb b/lib/datadog/core/telemetry/transport/http/telemetry.rb index e2b896afc8..b37f7b4be4 100644 --- a/lib/datadog/core/telemetry/transport/http/telemetry.rb +++ b/lib/datadog/core/telemetry/transport/http/telemetry.rb @@ -44,11 +44,16 @@ def headers(request_type:, api_key:) 'DD-Telemetry-Request-Type' => request_type, 'DD-Client-Library-Language' => Core::Environment::Ext::LANG, 'DD-Client-Library-Version' => Core::Environment::Identity.gem_datadog_version_semver2, + 'DD-Session-ID' => Core::Environment::Identity.id, # Enable debug mode for telemetry # 'DD-Telemetry-Debug-Enabled' => 'true', }.tap do |result| result['DD-API-KEY'] = api_key unless api_key.nil? + root = Core::Environment::Identity.root_runtime_id + result['DD-Root-Session-ID'] = root if root + parent = Core::Environment::Identity.parent_runtime_id + result['DD-Parent-Session-ID'] = parent if parent end end end diff --git a/lib/datadog/core/utils/spawn_monkey_patch.rb b/lib/datadog/core/utils/spawn_monkey_patch.rb new file mode 100644 index 0000000000..53af685130 --- /dev/null +++ b/lib/datadog/core/utils/spawn_monkey_patch.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Datadog + module Core + module Utils + module SpawnMonkeyPatch + # @param lineage_envs_provider [#call] returns a Hash of env vars to merge into the child process + def self.apply!(lineage_envs_provider:) + @lineage_envs_provider = lineage_envs_provider + ::Process.singleton_class.prepend(ProcessSpawnPatch) + true + end + + module ProcessSpawnPatch + def spawn(*args, **opts) + args.replace(SpawnMonkeyPatch.inject_lineage_envs(args)) + super + end + end + + # Process.spawn(env?, cmd, ...): env is optional first arg (Hash). When present, merge + # runtime_ids into it; when absent, prepend full ENV + runtime_ids so the child inherits both. + def self.inject_lineage_envs(args) + runtime_ids = @lineage_envs_provider.call + env_provided = Hash === args.first + + base_env = env_provided ? args.first : DATADOG_ENV.to_h + env = base_env.merge(runtime_ids) + rest = env_provided ? args.drop(1) : args + + [env, *rest] + end + end + end + end +end diff --git a/sig/datadog/core/configuration/config_helper.rbs b/sig/datadog/core/configuration/config_helper.rbs index 77fb203915..db20b4f874 100644 --- a/sig/datadog/core/configuration/config_helper.rbs +++ b/sig/datadog/core/configuration/config_helper.rbs @@ -24,6 +24,8 @@ module Datadog def key?: (String name) -> bool + def to_h: () -> Hash[::String, ::String] + alias has_key? key? alias include? key? alias member? key? diff --git a/sig/datadog/core/environment/identity.rbs b/sig/datadog/core/environment/identity.rbs index 5644841f0a..dcf4036375 100644 --- a/sig/datadog/core/environment/identity.rbs +++ b/sig/datadog/core/environment/identity.rbs @@ -6,6 +6,12 @@ module Datadog def self?.id: () -> ::String + def self?.root_runtime_id: () -> ::String? + + def self?.parent_runtime_id: () -> ::String? + + def self?.runtime_propagation_envs: () -> ::Hash[::String, ::String] + def self?.lang: () -> ::String def self?.lang_engine: () -> ::String diff --git a/sig/datadog/core/utils/spawn_monkey_patch.rbs b/sig/datadog/core/utils/spawn_monkey_patch.rbs new file mode 100644 index 0000000000..4dbcceb490 --- /dev/null +++ b/sig/datadog/core/utils/spawn_monkey_patch.rbs @@ -0,0 +1,18 @@ +module Datadog + module Core + module Utils + module SpawnMonkeyPatch + # Set in apply! before Process.spawn is intercepted; internal wiring only. + self.@lineage_envs_provider: ^() -> ::Hash[::String, ::String] + + def self.apply!: (lineage_envs_provider: ^() -> ::Hash[::String, ::String]) -> true + + module ProcessSpawnPatch + def spawn: (*untyped args, **untyped opts) -> untyped + end + + def self.inject_lineage_envs: (untyped args) -> ::Array[untyped] + end + end + end +end diff --git a/spec/datadog/core/configuration/components_spec.rb b/spec/datadog/core/configuration/components_spec.rb index 2171b28f8a..613bfe6016 100644 --- a/spec/datadog/core/configuration/components_spec.rb +++ b/spec/datadog/core/configuration/components_spec.rb @@ -736,4 +736,26 @@ end end end + + describe 'PATCH_ONLY_ONCE monkey patches' do + reset_at_fork_monkey_patch_for_components! + + before do + skip 'Fork not supported' unless Process.respond_to?(:fork) + skip 'Process.spawn not supported' unless Process.respond_to?(:spawn) + end + + it 'applies AtForkMonkeyPatch and SpawnMonkeyPatch when Components is initialized' do + expect_in_fork do + described_class.new(Datadog::Core::Configuration::Settings.new) + + expect(Process.singleton_class.ancestors).to include( + Datadog::Core::Utils::AtForkMonkeyPatch::ProcessMonkeyPatch, + ) + expect(Process.singleton_class.ancestors).to include( + Datadog::Core::Utils::SpawnMonkeyPatch::ProcessSpawnPatch, + ) + end + end + end end diff --git a/spec/datadog/core/configuration/config_helper_spec.rb b/spec/datadog/core/configuration/config_helper_spec.rb index 25d5f51453..5218133b73 100644 --- a/spec/datadog/core/configuration/config_helper_spec.rb +++ b/spec/datadog/core/configuration/config_helper_spec.rb @@ -68,6 +68,19 @@ end end + describe '#to_h' do + subject do + described_class.new( + source_env: {'DD_SERVICE' => 'my-app', 'PATH' => '/usr/bin'}, + supported_configurations: ['DD_SERVICE'] + ) + end + + it 'returns the source environment as a Hash' do + expect(subject.to_h).to eq('DD_SERVICE' => 'my-app', 'PATH' => '/usr/bin') + end + end + describe '#get_environment_variable' do context 'when using default source_env' do subject do diff --git a/spec/datadog/core/environment/identity_spec.rb b/spec/datadog/core/environment/identity_spec.rb index 349481977f..4f21bfd985 100644 --- a/spec/datadog/core/environment/identity_spec.rb +++ b/spec/datadog/core/environment/identity_spec.rb @@ -34,6 +34,50 @@ end end + describe '.root_runtime_id' do + subject(:root_runtime_id) { described_class.root_runtime_id } + + context 'in root process' do + it { is_expected.to be_nil } + end + + context 'when invoked in a fork' do + before { skip 'Fork not supported on current platform' unless Process.respond_to?(:fork) } + + it 'equals the parent id' do + parent_id = described_class.id + + expect_in_fork do + expect(described_class.id).to_not be_nil + expect(described_class.root_runtime_id).to_not be_nil + expect(described_class.id).to_not eq(root_runtime_id) + expect(described_class.root_runtime_id).to eq(parent_id) + end + end + end + end + + describe '.parent_runtime_id' do + subject(:parent_runtime_id) { described_class.parent_runtime_id } + + context 'in root process' do + it { is_expected.to be_nil } + end + + context 'when invoked in a fork' do + before { skip 'Fork not supported on current platform' unless Process.respond_to?(:fork) } + + it 'equals the parent id' do + parent_id = described_class.id + + expect_in_fork do + expect(described_class.id).to_not eq(parent_id) + expect(described_class.parent_runtime_id).to eq(parent_id) + end + end + end + end + describe '::lang' do subject(:lang) { described_class.lang } diff --git a/spec/datadog/core/utils/spawn_monkey_patch_spec.rb b/spec/datadog/core/utils/spawn_monkey_patch_spec.rb new file mode 100644 index 0000000000..9946cded10 --- /dev/null +++ b/spec/datadog/core/utils/spawn_monkey_patch_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'datadog/core/utils/spawn_monkey_patch' +require 'datadog/core/configuration/components' +require 'datadog/core/configuration/settings' + +RSpec.describe Datadog::Core::Utils::SpawnMonkeyPatch do + describe '::apply!' do + subject(:apply!) { described_class.apply!(lineage_envs_provider: -> { {} }) } + + context 'when Process.spawn is supported' do + before do + skip 'Fork not supported' unless Process.respond_to?(:fork) + skip 'Process.spawn not supported' unless Process.respond_to?(:spawn) + end + + it 'prepends the spawn monkey patch' do + expect_in_fork do + apply! + expect(Process.singleton_class.ancestors).to include(described_class::ProcessSpawnPatch) + expect(Process.method(:spawn).source_location.first).to match(/spawn_monkey_patch\.rb/) + end + end + end + end + + describe 'Components initialization' do + reset_at_fork_monkey_patch_for_components! + + before do + skip 'Fork not supported' unless Process.respond_to?(:fork) + skip 'Process.spawn not supported' unless Process.respond_to?(:spawn) + end + + it 'applies both fork and spawn patches when Components is initialized' do + expect_in_fork do + Datadog::Core::Configuration::Components.new(Datadog::Core::Configuration::Settings.new) + + expect(Process.singleton_class.ancestors).to include( + Datadog::Core::Utils::AtForkMonkeyPatch::ProcessMonkeyPatch, + ) + expect(Process.singleton_class.ancestors).to include( + Datadog::Core::Utils::SpawnMonkeyPatch::ProcessSpawnPatch, + ) + end + end + end +end diff --git a/spec/support/core_helpers.rb b/spec/support/core_helpers.rb index b24345c9ee..f383f15a17 100644 --- a/spec/support/core_helpers.rb +++ b/spec/support/core_helpers.rb @@ -145,7 +145,7 @@ def reset_at_fork_monkey_patch_for_components! # because normally it would be added during library initialization # and after the fork monkey patch test runs, the handler would get # cleared out. - Datadog::Core::Configuration::Components.const_get(:AT_FORK_ONLY_ONCE).send(:reset_ran_once_state_for_tests) + Datadog::Core::Configuration::Components.const_get(:PATCH_ONLY_ONCE).send(:reset_ran_once_state_for_tests) # We also need to clear out the handlers because we could have # the handlers registered from the library initialization time,