Skip to content
Merged
9 changes: 7 additions & 2 deletions lib/datadog/core/configuration/components.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'

Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions lib/datadog/core/configuration/config_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
28 changes: 25 additions & 3 deletions lib/datadog/core/environment/identity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
mabdinur marked this conversation as resolved.
@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
Expand Down
5 changes: 5 additions & 0 deletions lib/datadog/core/telemetry/transport/http/telemetry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions lib/datadog/core/utils/spawn_monkey_patch.rb
Original file line number Diff line number Diff line change
@@ -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))
Comment thread
Strech marked this conversation as resolved.
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
2 changes: 2 additions & 0 deletions sig/datadog/core/configuration/config_helper.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
6 changes: 6 additions & 0 deletions sig/datadog/core/environment/identity.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions sig/datadog/core/utils/spawn_monkey_patch.rbs
Original file line number Diff line number Diff line change
@@ -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
22 changes: 22 additions & 0 deletions spec/datadog/core/configuration/components_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 13 additions & 0 deletions spec/datadog/core/configuration/config_helper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 44 additions & 0 deletions spec/datadog/core/environment/identity_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down
48 changes: 48 additions & 0 deletions spec/datadog/core/utils/spawn_monkey_patch_spec.rb
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion spec/support/core_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading