From 329b0cd04cee76516c737d50c9f90d329d6d5ce9 Mon Sep 17 00:00:00 2001 From: Munir Date: Mon, 16 Mar 2026 17:08:10 -0400 Subject: [PATCH 01/10] chore(telemetry): track process creation via it headers --- gemfiles/ruby_3.4_opentelemetry.gemfile | 2 + gemfiles/ruby_3.4_opentelemetry.gemfile.lock | 17 +++++++ lib/datadog/core/environment/identity.rb | 33 +++++++++++++- .../telemetry/transport/http/telemetry.rb | 5 +++ lib/datadog/core/utils/spawn_monkey_patch.rb | 45 +++++++++++++++++++ 5 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 lib/datadog/core/utils/spawn_monkey_patch.rb diff --git a/gemfiles/ruby_3.4_opentelemetry.gemfile b/gemfiles/ruby_3.4_opentelemetry.gemfile index 7ef9fcc939..9c3a680ec2 100644 --- a/gemfiles/ruby_3.4_opentelemetry.gemfile +++ b/gemfiles/ruby_3.4_opentelemetry.gemfile @@ -31,6 +31,8 @@ gem "webrick", ">= 1.8.2" gem "opentelemetry-sdk", "~> 1.1" gem "opentelemetry-metrics-sdk", ">= 0.8" gem "opentelemetry-exporter-otlp-metrics", ">= 0.4" +gem "opentelemetry-logs-sdk", ">= 0.1" +gem "opentelemetry-exporter-otlp-logs", ">= 0.1" group :check do diff --git a/gemfiles/ruby_3.4_opentelemetry.gemfile.lock b/gemfiles/ruby_3.4_opentelemetry.gemfile.lock index f06bf3b1d1..9a87f43fca 100644 --- a/gemfiles/ruby_3.4_opentelemetry.gemfile.lock +++ b/gemfiles/ruby_3.4_opentelemetry.gemfile.lock @@ -62,6 +62,15 @@ GEM opentelemetry-api (1.7.0) opentelemetry-common (0.23.0) opentelemetry-api (~> 1.0) + opentelemetry-exporter-otlp-logs (0.3.0) + google-protobuf (>= 3.18) + googleapis-common-protos-types (~> 1.3) + opentelemetry-api (~> 1.1) + opentelemetry-common (~> 0.20) + opentelemetry-logs-api (~> 0.1) + opentelemetry-logs-sdk (~> 0.1) + opentelemetry-sdk + opentelemetry-semantic_conventions opentelemetry-exporter-otlp-metrics (0.6.1) google-protobuf (>= 3.18, < 5.0) googleapis-common-protos-types (~> 1.3) @@ -71,6 +80,12 @@ GEM opentelemetry-metrics-sdk (~> 0.5) opentelemetry-sdk (~> 1.2) opentelemetry-semantic_conventions + opentelemetry-logs-api (0.2.0) + opentelemetry-api (~> 1.0) + opentelemetry-logs-sdk (0.4.0) + opentelemetry-api (~> 1.2) + opentelemetry-logs-api (~> 0.1) + opentelemetry-sdk (~> 1.3) opentelemetry-metrics-api (0.4.0) opentelemetry-api (~> 1.0) opentelemetry-metrics-sdk (0.11.1) @@ -161,7 +176,9 @@ DEPENDENCIES json-schema (< 3) memory_profiler (~> 0.9) mutex_m + opentelemetry-exporter-otlp-logs (>= 0.1) opentelemetry-exporter-otlp-metrics (>= 0.4) + opentelemetry-logs-sdk (>= 0.1) opentelemetry-metrics-sdk (>= 0.8) opentelemetry-sdk (~> 1.1) os (~> 1.1) diff --git a/lib/datadog/core/environment/identity.rb b/lib/datadog/core/environment/identity.rb index b39bd39a7c..3b15cc3ebd 100644 --- a/lib/datadog/core/environment/identity.rb +++ b/lib/datadog/core/environment/identity.rb @@ -13,18 +13,49 @@ 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 + # Seed from env when this process was spawned (Process.spawn, exec). + # For fork-based processes these are set by after_fork!. + @ancestor_runtime_id = ENV[ENV_ROOT_SESSION_ID]&.freeze + @parent_runtime_id = ENV[ENV_PARENT_SESSION_ID]&.freeze + # Retrieves number of classes from runtime def id @id ||= ::SecureRandom.uuid.freeze # Check if runtime has changed, e.g. forked. - after_fork! { @id = ::SecureRandom.uuid.freeze } + after_fork! do + @parent_runtime_id = @id + @ancestor_runtime_id ||= @id + @id = ::SecureRandom.uuid.freeze + end @id end + # Root of the fork tree (Stable Service Instance Identifier). Nil in root process. + def ancestor_runtime_id + @ancestor_runtime_id + end + + # Direct parent's runtime_id. Nil in root process. + def parent_runtime_id + @parent_runtime_id + end + + # Returns session lineage env vars to inject into child process environments. + # Allows exec-based child processes (Process.spawn) to reconstruct process lineage. + def runtime_propagation_envs + ancestor = ancestor_runtime_id + current = id + root = ancestor || current + { ENV_ROOT_SESSION_ID => root, ENV_PARENT_SESSION_ID => current }.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..c77b71fa63 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? + ancestor = Core::Environment::Identity.ancestor_runtime_id + result['DD-Root-Session-ID'] = ancestor if ancestor + 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..ac32578c83 --- /dev/null +++ b/lib/datadog/core/utils/spawn_monkey_patch.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require_relative '../environment/identity' + +module Datadog + module Core + module Utils + # Monkey patches Process.spawn to inject session lineage env vars + # (_DD_ROOT_RB_SESSION_ID, _DD_PARENT_RB_SESSION_ID) into the child's + # environment so exec-based child processes can reconstruct process lineage. + module SpawnMonkeyPatch + def self.apply! + return false unless ::Process.respond_to?(:spawn) + + ::Process.singleton_class.prepend(ProcessSpawnPatch) + + true + end + + module ProcessSpawnPatch + def spawn(*args, **opts) + args = SpawnMonkeyPatch.inject_lineage_envs(args) + super(*args, **opts) + end + end + + class << self + def inject_lineage_envs(args) + lineage = Core::Environment::Identity.runtime_propagation_envs + + if args.first.is_a?(Hash) + # env hash provided: merge lineage into it + env = args.first.merge(lineage) + [env, *args.drop(1)] + else + # no env hash: prepend ENV merged with lineage + env = ENV.to_h.merge(lineage) + [env, *args] + end + end + end + end + end + end +end From 1ac18f3691c4b50df44acd7d7d9737381a314652 Mon Sep 17 00:00:00 2001 From: "dd-apm-ecosystems-autobot[bot]" <214617597+dd-apm-ecosystems-autobot[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 21:18:39 +0000 Subject: [PATCH 02/10] =?UTF-8?q?[=F0=9F=A4=96]=20Lock=20Dependency:=20htt?= =?UTF-8?q?ps://github.com/DataDog/dd-trace-rb/actions/runs/23166118107?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gemfiles/ruby_3.4_opentelemetry.gemfile | 2 -- gemfiles/ruby_3.4_opentelemetry.gemfile.lock | 17 ----------------- 2 files changed, 19 deletions(-) diff --git a/gemfiles/ruby_3.4_opentelemetry.gemfile b/gemfiles/ruby_3.4_opentelemetry.gemfile index 9c3a680ec2..7ef9fcc939 100644 --- a/gemfiles/ruby_3.4_opentelemetry.gemfile +++ b/gemfiles/ruby_3.4_opentelemetry.gemfile @@ -31,8 +31,6 @@ gem "webrick", ">= 1.8.2" gem "opentelemetry-sdk", "~> 1.1" gem "opentelemetry-metrics-sdk", ">= 0.8" gem "opentelemetry-exporter-otlp-metrics", ">= 0.4" -gem "opentelemetry-logs-sdk", ">= 0.1" -gem "opentelemetry-exporter-otlp-logs", ">= 0.1" group :check do diff --git a/gemfiles/ruby_3.4_opentelemetry.gemfile.lock b/gemfiles/ruby_3.4_opentelemetry.gemfile.lock index 7175b1c7d3..5321186532 100644 --- a/gemfiles/ruby_3.4_opentelemetry.gemfile.lock +++ b/gemfiles/ruby_3.4_opentelemetry.gemfile.lock @@ -62,15 +62,6 @@ GEM opentelemetry-api (1.7.0) opentelemetry-common (0.23.0) opentelemetry-api (~> 1.0) - opentelemetry-exporter-otlp-logs (0.3.0) - google-protobuf (>= 3.18) - googleapis-common-protos-types (~> 1.3) - opentelemetry-api (~> 1.1) - opentelemetry-common (~> 0.20) - opentelemetry-logs-api (~> 0.1) - opentelemetry-logs-sdk (~> 0.1) - opentelemetry-sdk - opentelemetry-semantic_conventions opentelemetry-exporter-otlp-metrics (0.6.1) google-protobuf (>= 3.18, < 5.0) googleapis-common-protos-types (~> 1.3) @@ -80,12 +71,6 @@ GEM opentelemetry-metrics-sdk (~> 0.5) opentelemetry-sdk (~> 1.2) opentelemetry-semantic_conventions - opentelemetry-logs-api (0.2.0) - opentelemetry-api (~> 1.0) - opentelemetry-logs-sdk (0.4.0) - opentelemetry-api (~> 1.2) - opentelemetry-logs-api (~> 0.1) - opentelemetry-sdk (~> 1.3) opentelemetry-metrics-api (0.4.0) opentelemetry-api (~> 1.0) opentelemetry-metrics-sdk (0.11.1) @@ -176,9 +161,7 @@ DEPENDENCIES json-schema (< 3) memory_profiler (~> 0.9) mutex_m - opentelemetry-exporter-otlp-logs (>= 0.1) opentelemetry-exporter-otlp-metrics (>= 0.4) - opentelemetry-logs-sdk (>= 0.1) opentelemetry-metrics-sdk (>= 0.8) opentelemetry-sdk (~> 1.1) os (~> 1.1) From 309042dae4ea3b9ea2428781de3b46c56425520d Mon Sep 17 00:00:00 2001 From: Munir Date: Mon, 23 Mar 2026 15:47:35 -0400 Subject: [PATCH 03/10] add spawnmonkeypatch to only once --- lib/datadog/core/configuration/components.rb | 6 ++++-- spec/support/core_helpers.rb | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/datadog/core/configuration/components.rb b/lib/datadog/core/configuration/components.rb index 98ff497661..099ca015e6 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' @@ -31,7 +32,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 +129,9 @@ 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! # Register callback that calls Components.after_fork Utils::AtForkMonkeyPatch.at_fork(:child) do 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, From 5e656d1b3c0f9a6ab3745a514cfd955e290b47ec Mon Sep 17 00:00:00 2001 From: Munir Date: Mon, 23 Mar 2026 15:53:21 -0400 Subject: [PATCH 04/10] use root instead of ancestor id (term used in dd-trace-py) --- lib/datadog/core/environment/identity.rb | 11 +++++------ .../core/telemetry/transport/http/telemetry.rb | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/datadog/core/environment/identity.rb b/lib/datadog/core/environment/identity.rb index 3b15cc3ebd..2110e67cfd 100644 --- a/lib/datadog/core/environment/identity.rb +++ b/lib/datadog/core/environment/identity.rb @@ -20,7 +20,7 @@ module Identity # Seed from env when this process was spawned (Process.spawn, exec). # For fork-based processes these are set by after_fork!. - @ancestor_runtime_id = ENV[ENV_ROOT_SESSION_ID]&.freeze + @root_runtime_id = ENV[ENV_ROOT_SESSION_ID]&.freeze @parent_runtime_id = ENV[ENV_PARENT_SESSION_ID]&.freeze # Retrieves number of classes from runtime @@ -30,7 +30,7 @@ def id # Check if runtime has changed, e.g. forked. after_fork! do @parent_runtime_id = @id - @ancestor_runtime_id ||= @id + @root_runtime_id ||= @id @id = ::SecureRandom.uuid.freeze end @@ -38,8 +38,8 @@ def id end # Root of the fork tree (Stable Service Instance Identifier). Nil in root process. - def ancestor_runtime_id - @ancestor_runtime_id + def root_runtime_id + @root_runtime_id end # Direct parent's runtime_id. Nil in root process. @@ -50,9 +50,8 @@ def parent_runtime_id # Returns session lineage env vars to inject into child process environments. # Allows exec-based child processes (Process.spawn) to reconstruct process lineage. def runtime_propagation_envs - ancestor = ancestor_runtime_id + root = root_runtime_id || id current = id - root = ancestor || current { ENV_ROOT_SESSION_ID => root, ENV_PARENT_SESSION_ID => current }.freeze end diff --git a/lib/datadog/core/telemetry/transport/http/telemetry.rb b/lib/datadog/core/telemetry/transport/http/telemetry.rb index c77b71fa63..b37f7b4be4 100644 --- a/lib/datadog/core/telemetry/transport/http/telemetry.rb +++ b/lib/datadog/core/telemetry/transport/http/telemetry.rb @@ -50,8 +50,8 @@ def headers(request_type:, api_key:) # 'DD-Telemetry-Debug-Enabled' => 'true', }.tap do |result| result['DD-API-KEY'] = api_key unless api_key.nil? - ancestor = Core::Environment::Identity.ancestor_runtime_id - result['DD-Root-Session-ID'] = ancestor if ancestor + 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 From f72882b97a1dbf6534f011d943502505cb50d3af Mon Sep 17 00:00:00 2001 From: Munir Date: Mon, 23 Mar 2026 16:12:09 -0400 Subject: [PATCH 05/10] lint and clean up implementation --- .../core/configuration/config_helper.rb | 6 ++++ lib/datadog/core/environment/identity.rb | 6 ++-- lib/datadog/core/utils/spawn_monkey_patch.rb | 28 ++++++++----------- sig/datadog/core/utils/spawn_monkey_patch.rbs | 16 +++++++++++ 4 files changed, 37 insertions(+), 19 deletions(-) create mode 100644 sig/datadog/core/utils/spawn_monkey_patch.rbs 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 2110e67cfd..48619ee257 100644 --- a/lib/datadog/core/environment/identity.rb +++ b/lib/datadog/core/environment/identity.rb @@ -20,8 +20,8 @@ module Identity # Seed from env when this process was spawned (Process.spawn, exec). # For fork-based processes these are set by after_fork!. - @root_runtime_id = ENV[ENV_ROOT_SESSION_ID]&.freeze - @parent_runtime_id = ENV[ENV_PARENT_SESSION_ID]&.freeze + @root_runtime_id = DATADOG_ENV[ENV_ROOT_SESSION_ID]&.freeze + @parent_runtime_id = DATADOG_ENV[ENV_PARENT_SESSION_ID]&.freeze # Retrieves number of classes from runtime def id @@ -52,7 +52,7 @@ def parent_runtime_id def runtime_propagation_envs root = root_runtime_id || id current = id - { ENV_ROOT_SESSION_ID => root, ENV_PARENT_SESSION_ID => current }.freeze + {ENV_ROOT_SESSION_ID => root, ENV_PARENT_SESSION_ID => current}.freeze end def pid diff --git a/lib/datadog/core/utils/spawn_monkey_patch.rb b/lib/datadog/core/utils/spawn_monkey_patch.rb index ac32578c83..c2061136dd 100644 --- a/lib/datadog/core/utils/spawn_monkey_patch.rb +++ b/lib/datadog/core/utils/spawn_monkey_patch.rb @@ -5,9 +5,6 @@ module Datadog module Core module Utils - # Monkey patches Process.spawn to inject session lineage env vars - # (_DD_ROOT_RB_SESSION_ID, _DD_PARENT_RB_SESSION_ID) into the child's - # environment so exec-based child processes can reconstruct process lineage. module SpawnMonkeyPatch def self.apply! return false unless ::Process.respond_to?(:spawn) @@ -19,24 +16,23 @@ def self.apply! module ProcessSpawnPatch def spawn(*args, **opts) - args = SpawnMonkeyPatch.inject_lineage_envs(args) - super(*args, **opts) + args.replace(SpawnMonkeyPatch.inject_lineage_envs(args)) + super end end class << self + # 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 inject_lineage_envs(args) - lineage = Core::Environment::Identity.runtime_propagation_envs - - if args.first.is_a?(Hash) - # env hash provided: merge lineage into it - env = args.first.merge(lineage) - [env, *args.drop(1)] - else - # no env hash: prepend ENV merged with lineage - env = ENV.to_h.merge(lineage) - [env, *args] - end + runtime_ids = Core::Environment::Identity.runtime_propagation_envs + 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 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..e640e63869 --- /dev/null +++ b/sig/datadog/core/utils/spawn_monkey_patch.rbs @@ -0,0 +1,16 @@ +module Datadog + module Core + module Utils + module SpawnMonkeyPatch + def self.apply!: () -> (false | true) + + module ProcessSpawnPatch + def spawn: (*untyped args, **untyped opts) -> untyped + end + + def self.inject_lineage_envs: (untyped args) -> untyped + end + end + end + end +end \ No newline at end of file From 4fbe9f89ee9c6ffdc6ed7494be31709a5224b4e6 Mon Sep 17 00:00:00 2001 From: Munir Date: Mon, 23 Mar 2026 16:26:45 -0400 Subject: [PATCH 06/10] add tests and fix circular import with Datadogenv usage --- lib/datadog/core.rb | 9 +++- lib/datadog/core/environment/identity.rb | 13 +---- lib/datadog/core/utils/spawn_monkey_patch.rb | 3 -- lib/datadog/tracing/tracer.rb | 1 + .../core/configuration/components_spec.rb | 22 +++++++++ .../core/configuration/config_helper_spec.rb | 13 +++++ .../datadog/core/environment/identity_spec.rb | 42 ++++++++++++++++ .../core/utils/spawn_monkey_patch_spec.rb | 48 +++++++++++++++++++ 8 files changed, 136 insertions(+), 15 deletions(-) create mode 100644 spec/datadog/core/utils/spawn_monkey_patch_spec.rb diff --git a/lib/datadog/core.rb b/lib/datadog/core.rb index d178cbd2f8..e438bfe68f 100644 --- a/lib/datadog/core.rb +++ b/lib/datadog/core.rb @@ -2,6 +2,14 @@ require_relative 'core/deprecations' require_relative 'core/configuration/config_helper' + +# DATADOG_ENV must be defined before requiring extensions, because extensions loads +# components (via configuration), which loads spawn_monkey_patch, which loads identity; +# identity uses DATADOG_ENV at load time. +module Datadog + DATADOG_ENV = Core::Configuration::ConfigHelper.new +end + require_relative 'core/extensions' # We must load core extensions to make certain global APIs @@ -22,7 +30,6 @@ module Core end end - DATADOG_ENV = Core::Configuration::ConfigHelper.new extend Core::Extensions # Add shutdown hook: diff --git a/lib/datadog/core/environment/identity.rb b/lib/datadog/core/environment/identity.rb index 48619ee257..1230f02620 100644 --- a/lib/datadog/core/environment/identity.rb +++ b/lib/datadog/core/environment/identity.rb @@ -18,17 +18,14 @@ module Identity module_function - # Seed from env when this process was spawned (Process.spawn, exec). - # For fork-based processes these are set by after_fork!. @root_runtime_id = DATADOG_ENV[ENV_ROOT_SESSION_ID]&.freeze @parent_runtime_id = DATADOG_ENV[ENV_PARENT_SESSION_ID]&.freeze - # Retrieves number of classes from runtime def id @id ||= ::SecureRandom.uuid.freeze - # Check if runtime has changed, e.g. forked. after_fork! do + # Order matters: capture @id before overwriting @parent_runtime_id = @id @root_runtime_id ||= @id @id = ::SecureRandom.uuid.freeze @@ -37,22 +34,16 @@ def id @id end - # Root of the fork tree (Stable Service Instance Identifier). Nil in root process. def root_runtime_id @root_runtime_id end - # Direct parent's runtime_id. Nil in root process. def parent_runtime_id @parent_runtime_id end - # Returns session lineage env vars to inject into child process environments. - # Allows exec-based child processes (Process.spawn) to reconstruct process lineage. def runtime_propagation_envs - root = root_runtime_id || id - current = id - {ENV_ROOT_SESSION_ID => root, ENV_PARENT_SESSION_ID => current}.freeze + {ENV_ROOT_SESSION_ID => root_runtime_id || id, ENV_PARENT_SESSION_ID => id}.freeze end def pid diff --git a/lib/datadog/core/utils/spawn_monkey_patch.rb b/lib/datadog/core/utils/spawn_monkey_patch.rb index c2061136dd..9bbf1d5f33 100644 --- a/lib/datadog/core/utils/spawn_monkey_patch.rb +++ b/lib/datadog/core/utils/spawn_monkey_patch.rb @@ -7,10 +7,7 @@ module Core module Utils module SpawnMonkeyPatch def self.apply! - return false unless ::Process.respond_to?(:spawn) - ::Process.singleton_class.prepend(ProcessSpawnPatch) - true end diff --git a/lib/datadog/tracing/tracer.rb b/lib/datadog/tracing/tracer.rb index 637f30575f..dd9ab6404a 100644 --- a/lib/datadog/tracing/tracer.rb +++ b/lib/datadog/tracing/tracer.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require_relative '../core' require_relative '../core/environment/ext' require_relative '../core/environment/socket' 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..749f78ee9a 100644 --- a/spec/datadog/core/environment/identity_spec.rb +++ b/spec/datadog/core/environment/identity_spec.rb @@ -34,6 +34,48 @@ 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 + described_class.id # Triggers after_fork! update + 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 + described_class.id # Triggers after_fork! update + 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..860af2e0fc --- /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! } + + 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 From 8e2e7dab94519de881c3013adaf83a6e2fdb53bc Mon Sep 17 00:00:00 2001 From: Munir Date: Mon, 23 Mar 2026 16:40:21 -0400 Subject: [PATCH 07/10] fix steep --- .../core/configuration/config_helper.rbs | 2 ++ sig/datadog/core/environment/identity.rbs | 6 ++++++ sig/datadog/core/utils/spawn_monkey_patch.rbs | 19 +++++++++---------- 3 files changed, 17 insertions(+), 10 deletions(-) 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..31ba8d04c3 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 index e640e63869..52fa113139 100644 --- a/sig/datadog/core/utils/spawn_monkey_patch.rbs +++ b/sig/datadog/core/utils/spawn_monkey_patch.rbs @@ -1,15 +1,14 @@ module Datadog - module Core - module Utils - module SpawnMonkeyPatch - def self.apply!: () -> (false | true) - - module ProcessSpawnPatch - def spawn: (*untyped args, **untyped opts) -> untyped - end - - def self.inject_lineage_envs: (untyped args) -> untyped + module Core + module Utils + module SpawnMonkeyPatch + def self.apply!: () -> (false | true) + + module ProcessSpawnPatch + def spawn: (*untyped args, **untyped opts) -> untyped end + + def self.inject_lineage_envs: (untyped args) -> untyped end end end From e2b2a892b551937a770e19c80be4919410e5b12b Mon Sep 17 00:00:00 2001 From: Munir Date: Mon, 23 Mar 2026 16:51:07 -0400 Subject: [PATCH 08/10] avoid fixing circular import in this PR --- lib/datadog/core.rb | 10 +--------- lib/datadog/core/configuration.rb | 4 ++++ lib/datadog/core/environment/identity.rb | 5 +++-- lib/datadog/tracing/tracer.rb | 1 - 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/lib/datadog/core.rb b/lib/datadog/core.rb index e438bfe68f..84bfc7ae30 100644 --- a/lib/datadog/core.rb +++ b/lib/datadog/core.rb @@ -2,16 +2,7 @@ require_relative 'core/deprecations' require_relative 'core/configuration/config_helper' - -# DATADOG_ENV must be defined before requiring extensions, because extensions loads -# components (via configuration), which loads spawn_monkey_patch, which loads identity; -# identity uses DATADOG_ENV at load time. -module Datadog - DATADOG_ENV = Core::Configuration::ConfigHelper.new -end - require_relative 'core/extensions' - # We must load core extensions to make certain global APIs # accessible: both for Datadog features and the core itself. module Datadog @@ -30,6 +21,7 @@ module Core end end + DATADOG_ENV = Core::Configuration::ConfigHelper.new extend Core::Extensions # Add shutdown hook: diff --git a/lib/datadog/core/configuration.rb b/lib/datadog/core/configuration.rb index 427633c5f7..4ced087d7a 100644 --- a/lib/datadog/core/configuration.rb +++ b/lib/datadog/core/configuration.rb @@ -1,5 +1,9 @@ # frozen_string_literal: true +# Products that require core/configuration directly (e.g. datadog/open_feature) bypass core.rb. +# Ensure DATADOG_ENV exists so Identity and other modules that use it at load time work. +require_relative '../core' unless defined?(::Datadog::DATADOG_ENV) + require_relative 'configuration/components' require_relative 'configuration/settings' require_relative 'telemetry/emitter' diff --git a/lib/datadog/core/environment/identity.rb b/lib/datadog/core/environment/identity.rb index 1230f02620..5b34c8f732 100644 --- a/lib/datadog/core/environment/identity.rb +++ b/lib/datadog/core/environment/identity.rb @@ -18,8 +18,9 @@ module Identity module_function - @root_runtime_id = DATADOG_ENV[ENV_ROOT_SESSION_ID]&.freeze - @parent_runtime_id = DATADOG_ENV[ENV_PARENT_SESSION_ID]&.freeze + 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 diff --git a/lib/datadog/tracing/tracer.rb b/lib/datadog/tracing/tracer.rb index dd9ab6404a..637f30575f 100644 --- a/lib/datadog/tracing/tracer.rb +++ b/lib/datadog/tracing/tracer.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require_relative '../core' require_relative '../core/environment/ext' require_relative '../core/environment/socket' From 319995f577b5fe9452273d8874e1db5ec869ea7e Mon Sep 17 00:00:00 2001 From: Munir Date: Mon, 23 Mar 2026 16:53:10 -0400 Subject: [PATCH 09/10] avoid fixing circular import in this PR --- lib/datadog/core.rb | 1 + lib/datadog/core/configuration.rb | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/datadog/core.rb b/lib/datadog/core.rb index 84bfc7ae30..d178cbd2f8 100644 --- a/lib/datadog/core.rb +++ b/lib/datadog/core.rb @@ -3,6 +3,7 @@ require_relative 'core/deprecations' require_relative 'core/configuration/config_helper' require_relative 'core/extensions' + # We must load core extensions to make certain global APIs # accessible: both for Datadog features and the core itself. module Datadog diff --git a/lib/datadog/core/configuration.rb b/lib/datadog/core/configuration.rb index 4ced087d7a..427633c5f7 100644 --- a/lib/datadog/core/configuration.rb +++ b/lib/datadog/core/configuration.rb @@ -1,9 +1,5 @@ # frozen_string_literal: true -# Products that require core/configuration directly (e.g. datadog/open_feature) bypass core.rb. -# Ensure DATADOG_ENV exists so Identity and other modules that use it at load time work. -require_relative '../core' unless defined?(::Datadog::DATADOG_ENV) - require_relative 'configuration/components' require_relative 'configuration/settings' require_relative 'telemetry/emitter' From 2751bebb5593035bae80230a37a3bc54916ef505 Mon Sep 17 00:00:00 2001 From: Munir Date: Tue, 24 Mar 2026 13:26:47 -0400 Subject: [PATCH 10/10] clean ups from PR review --- lib/datadog/core/configuration/components.rb | 5 +++- lib/datadog/core/utils/spawn_monkey_patch.rb | 26 +++++++++---------- sig/datadog/core/environment/identity.rbs | 2 +- sig/datadog/core/utils/spawn_monkey_patch.rbs | 9 ++++--- .../datadog/core/environment/identity_spec.rb | 10 ++++--- .../core/utils/spawn_monkey_patch_spec.rb | 2 +- 6 files changed, 30 insertions(+), 24 deletions(-) diff --git a/lib/datadog/core/configuration/components.rb b/lib/datadog/core/configuration/components.rb index 099ca015e6..9a18221328 100644 --- a/lib/datadog/core/configuration/components.rb +++ b/lib/datadog/core/configuration/components.rb @@ -23,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' @@ -131,7 +132,9 @@ def initialize(settings) # Register fork handling once globally self.class::PATCH_ONLY_ONCE.run do Utils::AtForkMonkeyPatch.apply! - Utils::SpawnMonkeyPatch.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/utils/spawn_monkey_patch.rb b/lib/datadog/core/utils/spawn_monkey_patch.rb index 9bbf1d5f33..53af685130 100644 --- a/lib/datadog/core/utils/spawn_monkey_patch.rb +++ b/lib/datadog/core/utils/spawn_monkey_patch.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -require_relative '../environment/identity' - module Datadog module Core module Utils module SpawnMonkeyPatch - def self.apply! + # @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 @@ -18,19 +18,17 @@ def spawn(*args, **opts) end end - class << self - # 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 inject_lineage_envs(args) - runtime_ids = Core::Environment::Identity.runtime_propagation_envs - env_provided = Hash === args.first + # 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 + 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 + [env, *rest] end end end diff --git a/sig/datadog/core/environment/identity.rbs b/sig/datadog/core/environment/identity.rbs index 31ba8d04c3..dcf4036375 100644 --- a/sig/datadog/core/environment/identity.rbs +++ b/sig/datadog/core/environment/identity.rbs @@ -10,7 +10,7 @@ module Datadog def self?.parent_runtime_id: () -> ::String? - def self?.runtime_propagation_envs: () -> Hash[::String, ::String] + def self?.runtime_propagation_envs: () -> ::Hash[::String, ::String] def self?.lang: () -> ::String diff --git a/sig/datadog/core/utils/spawn_monkey_patch.rbs b/sig/datadog/core/utils/spawn_monkey_patch.rbs index 52fa113139..4dbcceb490 100644 --- a/sig/datadog/core/utils/spawn_monkey_patch.rbs +++ b/sig/datadog/core/utils/spawn_monkey_patch.rbs @@ -2,14 +2,17 @@ module Datadog module Core module Utils module SpawnMonkeyPatch - def self.apply!: () -> (false | true) + # 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) -> untyped + def self.inject_lineage_envs: (untyped args) -> ::Array[untyped] end end end -end \ No newline at end of file +end diff --git a/spec/datadog/core/environment/identity_spec.rb b/spec/datadog/core/environment/identity_spec.rb index 749f78ee9a..4f21bfd985 100644 --- a/spec/datadog/core/environment/identity_spec.rb +++ b/spec/datadog/core/environment/identity_spec.rb @@ -34,7 +34,7 @@ end end - describe '::root_runtime_id' do + describe '.root_runtime_id' do subject(:root_runtime_id) { described_class.root_runtime_id } context 'in root process' do @@ -48,14 +48,16 @@ parent_id = described_class.id expect_in_fork do - described_class.id # Triggers after_fork! update + 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 + describe '.parent_runtime_id' do subject(:parent_runtime_id) { described_class.parent_runtime_id } context 'in root process' do @@ -69,7 +71,7 @@ parent_id = described_class.id expect_in_fork do - described_class.id # Triggers after_fork! update + expect(described_class.id).to_not eq(parent_id) expect(described_class.parent_runtime_id).to eq(parent_id) end end diff --git a/spec/datadog/core/utils/spawn_monkey_patch_spec.rb b/spec/datadog/core/utils/spawn_monkey_patch_spec.rb index 860af2e0fc..9946cded10 100644 --- a/spec/datadog/core/utils/spawn_monkey_patch_spec.rb +++ b/spec/datadog/core/utils/spawn_monkey_patch_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Datadog::Core::Utils::SpawnMonkeyPatch do describe '::apply!' do - subject(:apply!) { described_class.apply! } + subject(:apply!) { described_class.apply!(lineage_envs_provider: -> { {} }) } context 'when Process.spawn is supported' do before do