Module: Datadog::Tracing::Remote

Defined in:
lib/datadog/tracing/remote.rb

Overview

Remote configuration declaration

Constant Summary collapse

PRODUCT =
"APM_TRACING"
CAPABILITIES =
[
  1 << 12, # APM_TRACING_SAMPLE_RATE: Dynamic trace sampling rate configuration
  1 << 13, # APM_TRACING_LOGS_INJECTION: Dynamic trace logs injection configuration
  1 << 14, # APM_TRACING_HTTP_HEADER_TAGS: Dynamic trace HTTP header tags configuration
  1 << 29, # APM_TRACING_SAMPLE_RULES: Dynamic trace sampling rules configuration
  1 << 45, # APM_TRACING_MULTICONFIG: merge multiple org/env-level APM_TRACING configs
  # APM_TRACING_ENABLE_DYNAMIC_INSTRUMENTATION (bit 38) is declared in
  # DI::Remote.capabilities, not here, so it is registered only when DI
  # is not explicitly disabled and the runtime supports DI.
].freeze
SCOPE_LABELS =

Diagnostic scope label per specificity priority.

{
  5 => "service+env",
  4 => "service",
  3 => "env",
  2 => "cluster",
  1 => "org",
}.freeze

Class Method Summary collapse

Class Method Details

.apply_lib_config(lib_config, repository) ⇒ nil

Applies one lib_config: maps the dynamic OPTIONS to telemetry, drives DI enablement from "dynamic_instrumentation_enabled", and reports the configuration change to telemetry.

Parameters:

Returns:

  • (nil)


209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/datadog/tracing/remote.rb', line 209

def apply_lib_config(lib_config, repository)
  env_vars = Datadog::Tracing::Configuration::Dynamic::OPTIONS.map do |name, env_var, option|
    value = lib_config[name]

    # Guard for RBS/Steep
    raise "option is a #{option.class}, expected Option" unless option.is_a?(Configuration::Dynamic::Option)

    telemetry_value = option.call(value)

    [env_var, telemetry_value]
  end

  if (di_enabled = lib_config["dynamic_instrumentation_enabled"]) != nil # rubocop:disable Style/NonNilCheck
    # repository is forwarded so that an enable signal can reconcile DI
    # against probes delivered in an earlier poll while DI was stopped
    # (see Datadog::DI::Remote.handle_rc_enablement).
    Datadog::DI::Remote.handle_rc_enablement(di_enabled, repository)

    components = Datadog.send(:components, allow_initialization: false)
    di_products = Datadog::DI::Remote.products +
      Datadog::SymbolDatabase::Remote.deferred_products(Datadog.configuration)

    if di_enabled
      components&.symbol_database&.resume_pending_upload
      # Advertise the DI products only if the component actually started.
      # handle_rc_enablement above no-ops when DI cannot run: the component
      # is nil on an unsupported runtime, or the enable signal is blocked
      # by DD_DYNAMIC_INSTRUMENTATION_ENABLED=false. Advertising then would
      # report DI as in use when it is not and invite probe configs the
      # tracer must refuse; withdraw the products otherwise.
      if components&.dynamic_instrumentation&.started?
        components&.remote&.add_products(*di_products)
      else
        components&.remote&.remove_products(*di_products)
      end
    else
      components&.symbol_database&.stop_for_di_disable
      components&.remote&.remove_products(*di_products)
    end

    Datadog.logger.debug { "APM_TRACING RC: merged dynamic_instrumentation_enabled=#{di_enabled}" }
  end

  # allow_initialization: false because this runs on the remote-config
  # worker thread. If components haven't been built yet (e.g. during a
  # teardown/reset window), the default value would synchronously build
  # the entire component tree from this thread. The &. chain matches the
  # pattern used by DI::Remote.handle_rc_enablement in the same dispatch
  # path.
  Datadog.send(:components, allow_initialization: false)&.telemetry&.client_configuration_change!(env_vars)
  nil
end

.capabilities ⇒ Array[Integer]

Returns the remote config capability bits advertised.

Returns:

  • (Array[Integer]) —

    the remote config capability bits advertised



39
40
41
# File 'lib/datadog/tracing/remote.rb', line 39

def capabilities
  CAPABILITIES
end

.config_matches?(config, service, env) ⇒ bool

Whether a config targets this tracer. A concrete (non-"") service or env that differs from ours excludes the config; "" and an absent service_target match anything.

Parameters:

  • config (Hash[String, untyped]) —

    a parsed config

  • service (String, nil) —

    this tracer's service

  • env (String, nil) —

    this tracer's env

Returns:

  • (bool) —

    true when the config applies to this tracer



147
148
149
150
151
152
153
154
155
156
157
# File 'lib/datadog/tracing/remote.rb', line 147

def config_matches?(config, service, env)
  target = config["service_target"]
  return true unless target.is_a?(Hash)

  target_service = target["service"]
  target_env = target["env"]
  return false if target_service && target_service != "*" && target_service != service
  return false if target_env && target_env != "*" && target_env != env

  true
end

.config_priority(config) ⇒ Integer

Specificity of a config, higher meaning more specific: service+env (5), service (4), env (3), cluster (2), org (1). A target counts as concrete only when present and not "*".

Parameters:

  • config (Hash[String, untyped]) —

    a parsed config

Returns:

  • (Integer) —

    the specificity rank, 1 through 5



165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/datadog/tracing/remote.rb', line 165

def config_priority(config)
  target = config["service_target"]
  service = target.is_a?(Hash) ? target["service"] : nil
  env = target.is_a?(Hash) ? target["env"] : nil
  single_service = !service.nil? && service != "*"
  single_env = !env.nil? && env != "*"

  return 5 if single_service && single_env
  return 4 if single_service
  return 3 if single_env
  return 2 unless config["k8s_target_v2"].nil?

  1
end

.merge_and_apply_configs(repository) ⇒ nil

Merges every active APM_TRACING config in the repository and applies the result once. Org/env-level (multi-config) remote enablement delivers several APM_TRACING configs in parallel (a (service, env)-specific one, an env-wide one, and/or an org-wide "*" one); for each lib_config field the value from the most-specific matching config wins. The RC repository prunes deleted configs, so this recomputes the merge from the repository's current contents on each dispatch.

Parameters:

Returns:

  • (nil)


53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/datadog/tracing/remote.rb', line 53

def merge_and_apply_configs(repository)
  # Resolve the service identity the same way the RC client registers
  # it with the backend (Core::Remote::Client#service_name =
  # remote.service || service). When a customer sets remote.service,
  # the backend delivers service_target.service values targeting that
  # override; matching against the local service instead would drop
  # every service-scoped config and let a less-specific org config win.
  service = Datadog.configuration.remote.service || Datadog.configuration.service
  env = Datadog.configuration.env

  # @type var parsed: Array[[::Datadog::Core::Remote::Configuration::Content, ::Hash[::String, untyped]]]
  parsed = []
  repository.contents.each do |content|
    next unless content.path.product == PRODUCT

    begin
      config = parse_content(content)
      unless config.is_a?(Hash)
        # A syntactically valid but non-object payload (null, [], 42)
        # would later make config_matches? raise outside this per-content
        # rescue and abort the whole merge, erroring valid configs too.
        raise TypeError, "APM_TRACING remote config must be a JSON object, got #{config.class}"
      end
      # A present but non-object lib_config (e.g. {"lib_config": []}) is a
      # schema violation the customer can fix. Acknowledging it as applied
      # would tell the backend the config was successfully applied when it
      # was not, and an empty merge would revert active tracing overrides
      # to their non-RC values. Reject it per content so the malformed
      # payload is reported errored instead. A null or absent lib_config
      # carries no overrides and is left as a no-op.
      lib_config = config["lib_config"]
      if !lib_config.nil? && !lib_config.is_a?(Hash)
        raise TypeError, "APM_TRACING lib_config must be a JSON object, got #{lib_config.class}"
      end
      parsed << [content, config]
    rescue => e
      Datadog.logger.debug { "APM_TRACING RC: skipping unparseable config: #{e.class}: #{e.message}" }
      Datadog.send(:components, allow_initialization: false)&.telemetry&.report(
        e,
        description: "Failed to parse APM_TRACING remote config",
      )
      content.errored("#{e.class}: #{e.message}: #{Array(e.backtrace).join("\n")}")
    end
  end

  Datadog.logger.debug { "APM_TRACING RC: received #{parsed.length} config(s)" }

  applicable = parsed.select do |content, config|
    if config_matches?(config, service, env)
      Datadog.logger.debug do
        "APM_TRACING RC: config #{content.path.config_id} " \
          "scope=#{SCOPE_LABELS[config_priority(config)]} priority=#{config_priority(config)}"
      end
      true
    else
      Datadog.logger.debug do
        "APM_TRACING RC: dropped config #{content.path.config_id} " \
          "(service_target=#{config["service_target"].inspect}, self=#{service}/#{env})"
      end
      false
    end
  end

  # Most-specific first; ties broken by config id (ascending) for a
  # deterministic merge.
  ordered = applicable.sort_by { |content, config| [-config_priority(config), content.path.config_id] }
  merged = merge_lib_configs(ordered.map { |_content, config| config })

  # Applied even when the set is empty: an emptied repository (last config
  # removed) reverts the tracing overrides to their non-RC values.
  apply_lib_config(merged, repository)

  parsed.each { |content, _config| content.applied }
  nil
rescue => e
  Datadog.logger.debug { "APM_TRACING RC: failed to apply configs: #{e.class}: #{e.message}" }
  Datadog.send(:components, allow_initialization: false)&.telemetry&.report(
    e,
    description: "Failed to apply APM_TRACING remote configs",
  )
  parsed&.each do |content, _config|
    content.errored("#{e.class}: #{e.message}: #{Array(e.backtrace).join("\n")}")
  end
  nil
end

.merge_lib_configs(configs_most_specific_first) ⇒ Hash[String, untyped]

Merges lib_configs from configs ordered most-specific first: for each field, the first (most-specific) non-nil value wins. Fields are independent, so a lower-priority config can supply a field the higher-priority one omits.

Parameters:

  • configs_most_specific_first (Array[Hash[String, untyped]]) —

    configs, most-specific first

Returns:

  • (Hash[String, untyped]) —

    the merged lib_config



187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/datadog/tracing/remote.rb', line 187

def merge_lib_configs(configs_most_specific_first)
  merged = {}
  configs_most_specific_first.each do |config|
    lib_config = config["lib_config"]
    next unless lib_config.is_a?(Hash)

    lib_config.each do |key, value|
      next if value.nil?

      merged[key] = value unless merged.key?(key)
    end
  end
  merged
end

.products ⇒ Array[String]

Returns the remote config products this module handles.

Returns:

  • (Array[String]) —

    the remote config products this module handles



34
35
36
# File 'lib/datadog/tracing/remote.rb', line 34

def products
  [PRODUCT]
end

.receiver(products = [PRODUCT], &block) ⇒ Object



270
271
272
273
# File 'lib/datadog/tracing/remote.rb', line 270

def receiver(products = [PRODUCT], &block)
  matcher = Core::Remote::Dispatcher::Matcher::Product.new(products)
  [Core::Remote::Dispatcher::Receiver.new(matcher, &block)]
end

.receivers(_telemetry) ⇒ Array[Core::Remote::Dispatcher::Receiver]

Returns the APM_TRACING receiver.

Parameters:

Returns:



264
265
266
267
268
# File 'lib/datadog/tracing/remote.rb', line 264

def receivers(_telemetry)
  receiver do |repository, _changes|
    merge_and_apply_configs(repository)
  end
end