datadog 2.45.0 → 2.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -1
  3. data/ext/datadog_profiling_native_extension/collectors_stack.c +31 -14
  4. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +5 -1
  5. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +5 -0
  6. data/ext/datadog_profiling_native_extension/profiling.c +6 -0
  7. data/ext/libdatadog_api/di.c +30 -5
  8. data/ext/libdatadog_api/extconf.rb +2 -0
  9. data/ext/libdatadog_api/otel_thread_context.c +20 -14
  10. data/ext/libdatadog_api/process_discovery.c +15 -1
  11. data/ext/libdatadog_api/trace_exporter.c +105 -25
  12. data/ext/libdatadog_extconf_helpers.rb +1 -1
  13. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +29 -2
  14. data/lib/datadog/appsec/contrib/rails/patches/render_to_body_patch.rb +8 -7
  15. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +14 -2
  16. data/lib/datadog/appsec/contrib/sinatra/patches/json_patch.rb +8 -7
  17. data/lib/datadog/appsec/security_engine/engine.rb +2 -1
  18. data/lib/datadog/core/configuration/components.rb +40 -18
  19. data/lib/datadog/core/configuration/components_state.rb +8 -1
  20. data/lib/datadog/core/configuration/supported_configurations.rb +7 -2
  21. data/lib/datadog/core/process_discovery.rb +5 -1
  22. data/lib/datadog/core/remote/client/capabilities.rb +34 -29
  23. data/lib/datadog/core/remote/client.rb +44 -3
  24. data/lib/datadog/core/remote/component.rb +28 -12
  25. data/lib/datadog/core/remote/dispatcher.rb +47 -6
  26. data/lib/datadog/core/workers/runtime_metrics.rb +1 -1
  27. data/lib/datadog/di/boot.rb +1 -0
  28. data/lib/datadog/di/capture_expression_evaluator/result.rb +30 -0
  29. data/lib/datadog/di/capture_expression_evaluator.rb +39 -9
  30. data/lib/datadog/di/component.rb +108 -3
  31. data/lib/datadog/di/configuration.rb +53 -2
  32. data/lib/datadog/di/context.rb +7 -2
  33. data/lib/datadog/di/contrib/active_record.rb +8 -2
  34. data/lib/datadog/di/evaluation_failure.rb +28 -0
  35. data/lib/datadog/di/guardrails_telemetry/reason.rb +87 -0
  36. data/lib/datadog/di/guardrails_telemetry.rb +203 -0
  37. data/lib/datadog/di/instrumenter.rb +65 -15
  38. data/lib/datadog/di/logger.rb +30 -8
  39. data/lib/datadog/di/probe_manager.rb +48 -5
  40. data/lib/datadog/di/probe_notification_builder.rb +28 -26
  41. data/lib/datadog/di/probe_notifier_worker.rb +56 -6
  42. data/lib/datadog/di/remote.rb +46 -2
  43. data/lib/datadog/di/serializer.rb +147 -55
  44. data/lib/datadog/di/snapshot_event.rb +38 -0
  45. data/lib/datadog/di/transport/http.rb +13 -3
  46. data/lib/datadog/di/transport/input.rb +32 -7
  47. data/lib/datadog/open_feature/activation.rb +268 -0
  48. data/lib/datadog/open_feature/component.rb +31 -15
  49. data/lib/datadog/open_feature/configuration.rb +6 -0
  50. data/lib/datadog/open_feature/hooks/evaluation_telemetry_opt_out_hook.rb +25 -0
  51. data/lib/datadog/open_feature/provider.rb +304 -8
  52. data/lib/datadog/open_feature.rb +31 -1
  53. data/lib/datadog/profiling/collectors/info.rb +37 -6
  54. data/lib/datadog/profiling/component.rb +22 -20
  55. data/lib/datadog/profiling/exporter.rb +3 -5
  56. data/lib/datadog/tracing/component.rb +21 -2
  57. data/lib/datadog/tracing/configuration/ext.rb +2 -0
  58. data/lib/datadog/tracing/configuration/settings.rb +14 -1
  59. data/lib/datadog/tracing/context_provider.rb +8 -0
  60. data/lib/datadog/tracing/contrib/ethon/easy_patch.rb +10 -5
  61. data/lib/datadog/tracing/contrib/excon/middleware.rb +10 -3
  62. data/lib/datadog/tracing/contrib/faraday/middleware.rb +10 -4
  63. data/lib/datadog/tracing/contrib/http/instrumentation.rb +16 -1
  64. data/lib/datadog/tracing/contrib/httpclient/instrumentation.rb +8 -2
  65. data/lib/datadog/tracing/contrib/httprb/instrumentation.rb +10 -4
  66. data/lib/datadog/tracing/contrib/rest_client/request_patch.rb +10 -3
  67. data/lib/datadog/tracing/contrib/utils/quantization/http.rb +102 -0
  68. data/lib/datadog/tracing/otel_thread_context.rb +3 -5
  69. data/lib/datadog/tracing/remote.rb +50 -19
  70. data/lib/datadog/tracing/transport/http/telemetry.rb +94 -0
  71. data/lib/datadog/tracing/transport/http/traces.rb +2 -0
  72. data/lib/datadog/tracing/transport/native/telemetry.rb +17 -0
  73. data/lib/datadog/tracing/transport/native.rb +16 -1
  74. data/lib/datadog/tracing/transport/telemetry.rb +62 -0
  75. data/lib/datadog/tracing/transport/traces.rb +82 -15
  76. data/lib/datadog/version.rb +1 -1
  77. metadata +19 -10
  78. data/lib/datadog/appsec/utils/hash_coercion.rb +0 -23
@@ -66,10 +66,23 @@ module Datadog
66
66
  gateway.watch("rails.response.body.json") do |stack, container|
67
67
  context = container.context
68
68
 
69
+ body_bytesize = container.data.bytesize
70
+ limit = Datadog.configuration.appsec.body_parsing_size_limit
71
+
69
72
  persistent_data = {
70
- "server.response.body" => container.data,
73
+ "server.response.body.byte_length" => body_bytesize,
71
74
  }
72
- result = context.run_waf(persistent_data, {}, Datadog.configuration.appsec.waf_timeout)
75
+
76
+ if limit > 0 && body_bytesize <= limit
77
+ json = extract_json(container.data)
78
+ persistent_data["server.response.body"] = WAF::JSONString.new(json) if json
79
+ end
80
+
81
+ result = begin
82
+ context.run_waf(persistent_data, {}, Datadog.configuration.appsec.waf_timeout)
83
+ rescue WAF::ConversionError
84
+ next stack.call(container)
85
+ end
73
86
 
74
87
  if result.match?
75
88
  context.events.push(
@@ -85,6 +98,20 @@ module Datadog
85
98
  stack.call(container)
86
99
  end
87
100
  end
101
+
102
+ private
103
+
104
+ def extract_json(body)
105
+ return body unless body.start_with?("/**/")
106
+ return unless body.end_with?(")")
107
+
108
+ # NOTE: Best-effort support for standard Rails JSONP callbacks.
109
+ # Callback expressions with parentheses may prevent extraction
110
+ opening_parenthesis_index = body.b.index("(")
111
+ return unless opening_parenthesis_index
112
+
113
+ body.byteslice(opening_parenthesis_index + 1, body.bytesize - opening_parenthesis_index - 2) #: String
114
+ end
88
115
  end
89
116
  end
90
117
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../../../utils/hash_coercion"
4
3
  require_relative "../../../instrumentation/gateway/argument"
5
4
 
6
5
  module Datadog
@@ -8,8 +7,8 @@ module Datadog
8
7
  module Contrib
9
8
  module Rails
10
9
  module Patches
11
- # A patch targeting `AbstractController::Rendering#render_to_body`
12
- # method to capture JSON response body right before it is serialized.
10
+ # A patch for `AbstractController::Rendering#render_to_body` that captures
11
+ # the serialized JSON response body for AppSec inspection
13
12
  module RenderToBodyPatch
14
13
  def render_to_body(options = {})
15
14
  return super unless options.key?(:json)
@@ -17,13 +16,15 @@ module Datadog
17
16
  context = request.env[Datadog::AppSec::Ext::CONTEXT_KEY]
18
17
  return super unless context
19
18
 
20
- data = Utils::HashCoercion.coerce(options[:json])
21
- return super unless data
19
+ # NOTE: Custom renderers can return streaming bodies, which we
20
+ # currently do not inspect
21
+ body = super
22
+ return body unless body.is_a?(String)
22
23
 
23
- container = Instrumentation::Gateway::DataContainer.new(data, context: context)
24
+ container = Instrumentation::Gateway::DataContainer.new(body, context: context)
24
25
  Instrumentation.gateway.push("rails.response.body.json", container)
25
26
 
26
- super
27
+ body
27
28
  end
28
29
  end
29
30
  end
@@ -101,10 +101,22 @@ module Datadog
101
101
  gateway.watch("sinatra.response.body.json") do |stack, container|
102
102
  context = container.context # : Context
103
103
 
104
+ body_bytesize = container.data.bytesize
105
+ limit = Datadog.configuration.appsec.body_parsing_size_limit
106
+
104
107
  persistent_data = {
105
- "server.response.body" => container.data,
108
+ "server.response.body.byte_length" => body_bytesize,
106
109
  }
107
- result = context.run_waf(persistent_data, {}, Datadog.configuration.appsec.waf_timeout)
110
+
111
+ if limit > 0 && body_bytesize <= limit
112
+ persistent_data["server.response.body"] = WAF::JSONString.new(container.data)
113
+ end
114
+
115
+ result = begin
116
+ context.run_waf(persistent_data, {}, Datadog.configuration.appsec.waf_timeout)
117
+ rescue WAF::ConversionError
118
+ next stack.call(container)
119
+ end
108
120
 
109
121
  if result.match?
110
122
  context.events.push(
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../../../utils/hash_coercion"
4
3
  require_relative "../../../instrumentation/gateway/argument"
5
4
 
6
5
  module Datadog
@@ -8,20 +7,22 @@ module Datadog
8
7
  module Contrib
9
8
  module Sinatra
10
9
  module Patches
11
- # A patch targeting `Sinatra::JSON#json` method to capture JSON response
12
- # body right before it is serialized.
10
+ # A patch for `Sinatra::JSON#json` that captures the serialized
11
+ # response body for AppSec inspection
13
12
  module JsonPatch
14
13
  def json(object, options = {})
15
14
  context = @request.env[Datadog::AppSec::Ext::CONTEXT_KEY] # : Context?
16
15
  return super unless context
17
16
 
18
- data = Utils::HashCoercion.coerce(object)
19
- return super unless data
17
+ # NOTE: Custom renderers can return streaming bodies, which we
18
+ # currently do not inspect
19
+ body = super
20
+ return body unless body.is_a?(String)
20
21
 
21
- container = Instrumentation::Gateway::DataContainer.new(data, context: context)
22
+ container = Instrumentation::Gateway::DataContainer.new(body, context: context)
22
23
  Instrumentation.gateway.push("sinatra.response.body.json", container)
23
24
 
24
- super
25
+ body
25
26
  end
26
27
  end
27
28
  end
@@ -71,7 +71,8 @@ module Datadog
71
71
  if is_ruleset_update &&
72
72
  (diagnostics.key?("error") ||
73
73
  diagnostics.dig("rules", "error") ||
74
- diagnostics.dig("processors", "errors"))
74
+ diagnostics.dig("processors", "error"))
75
+ @waf_builder.remove_config_at_path(path)
75
76
  diagnostics = load_default_config(telemetry: AppSec.telemetry)
76
77
  @reconfigured_ruleset_version = diagnostics["ruleset_version"]
77
78
  report_configuration_diagnostics(diagnostics, action: "update", telemetry: AppSec.telemetry)
@@ -21,7 +21,7 @@ require_relative "../../ai_guard/component"
21
21
  require_relative "../../di/component"
22
22
  require_relative "../../symbol_database"
23
23
  require_relative "../../symbol_database/component"
24
- require_relative "../../open_feature/component"
24
+ require_relative "../../open_feature/activation"
25
25
  require_relative "../../error_tracking/component"
26
26
  require_relative "../crashtracking/component"
27
27
  require_relative "../environment/agent_info"
@@ -55,7 +55,7 @@ module Datadog
55
55
  # extension, remote config off, non-MRI). Under the always-build model
56
56
  # it is non-nil even when the DI setting defaults to off, so a built
57
57
  # SymbolDatabase component is inert until an upload is actually
58
- # permitted: the component's own gate (see Component#upload_allowed?)
58
+ # permitted: the component's own gate, SymbolDatabase::Component#upload_allowed?,
59
59
  # only extracts and uploads when DI is truly active or the customer
60
60
  # opted in explicitly. This mirrors DI advertising/building a component
61
61
  # by default while installing no probes until it is enabled.
@@ -154,8 +154,7 @@ module Datadog
154
154
  :ai_guard,
155
155
  :agent_info,
156
156
  :data_streams,
157
- :symbol_database,
158
- :open_feature
157
+ :symbol_database
159
158
 
160
159
  def initialize(settings)
161
160
  @settings = settings
@@ -188,15 +187,14 @@ module Datadog
188
187
 
189
188
  @telemetry = self.class.build_telemetry(settings, agent_settings, @logger)
190
189
 
191
- # Bind Remote Configuration dispatch to this tree, which starts before it becomes the global Components instance.
192
190
  @remote = Remote::Component.build(
193
191
  settings,
194
192
  agent_settings,
195
193
  logger: @logger,
196
194
  telemetry: telemetry,
197
- open_feature_component_provider: -> { @open_feature },
198
195
  )
199
196
  @tracer = Datadog::Tracing::Component.build_tracer(settings, agent_settings, logger: @logger)
197
+ Datadog::Tracing::Component.bind_transport_telemetry(@tracer, telemetry)
200
198
  @crashtracker = self.class.build_crashtracker(settings, agent_settings, logger: @logger)
201
199
 
202
200
  @profiler, profiler_logger_extra = Datadog::Profiling::Component.build_profiler_component(
@@ -211,7 +209,13 @@ module Datadog
211
209
  @health_metrics = self.class.build_health_metrics(settings, @logger, telemetry)
212
210
  @appsec = Datadog::AppSec::Component.build_appsec_component(settings, telemetry: telemetry)
213
211
  @ai_guard = Datadog::AIGuard::Component.build(settings, logger: @logger, telemetry: telemetry)
214
- @open_feature = OpenFeature::Component.build(settings, agent_settings, logger: @logger, telemetry: telemetry)
212
+ @open_feature_activation = OpenFeature::Activation.new(
213
+ settings,
214
+ agent_settings,
215
+ remote,
216
+ logger: @logger,
217
+ telemetry: telemetry,
218
+ )
215
219
  @dynamic_instrumentation = Datadog::DI::Component.build(settings, agent_settings, @logger, telemetry: telemetry)
216
220
  # Only build symbol database when enabled, so a disabled component is
217
221
  # never constructed.
@@ -257,6 +261,7 @@ module Datadog
257
261
  ProcessDiscovery.after_fork
258
262
  symbol_database&.after_fork!
259
263
  data_streams&.restart_flush_thread
264
+ @open_feature_activation.after_fork
260
265
  end
261
266
 
262
267
  # Hot-swaps with a new sampler.
@@ -271,6 +276,8 @@ module Datadog
271
276
  def startup!(settings, old_state: nil)
272
277
  telemetry.start(old_state&.telemetry_enabled?, components: self)
273
278
 
279
+ @open_feature_activation.startup!
280
+
274
281
  if settings.profiling.enabled
275
282
  if profiler
276
283
  profiler.start
@@ -281,12 +288,20 @@ module Datadog
281
288
  end
282
289
  end
283
290
 
291
+ if dynamic_instrumentation && old_state
292
+ dynamic_instrumentation.record_rc_enablement_signal_paths(old_state.rc_enablement_signal_paths)
293
+ end
294
+
284
295
  if remote && old_state&.remote_started?
285
296
  # The library was reconfigured and previously it already started
286
297
  # the remote component (i.e., it received at least one request
287
298
  # through the installed Rack middleware which started the remote).
288
299
  # If the new configuration also has remote enabled, start the
289
300
  # new remote right away.
301
+ #
302
+ # The DI signal-path restore above must precede this: the
303
+ # replacement client's first sync can dispatch the APM_TRACING
304
+ # merge, whose withdrawal branch reads the recorded paths.
290
305
  remote.start
291
306
  end
292
307
 
@@ -329,8 +344,14 @@ module Datadog
329
344
  # Shutdown Symbol Database
330
345
  symbol_database&.shutdown!
331
346
 
332
- # Shutdown OpenFeature component
333
- open_feature&.shutdown!
347
+ begin
348
+ @open_feature_activation.shutdown!
349
+ rescue => e
350
+ # Feature Flags is optional and must never interrupt library shutdown.
351
+ description = "Feature Flags delivery failed to shut down"
352
+ logger.error("#{description}: #{e.class}: #{e.message}")
353
+ telemetry.report(e, description: description)
354
+ end
334
355
 
335
356
  # Decommission AppSec
336
357
  appsec&.shutdown!
@@ -392,21 +413,22 @@ module Datadog
392
413
  # di_implicitly_enabled distinguishes RC-driven start from explicit
393
414
  # start (env var or programmatic) so that an explicit
394
415
  # `enabled = false` on reconfiguration can take effect.
395
- #
396
- # using_default? — not `!enabled` — because Datadog.configure has
397
- # already mutated the singleton settings by the time #state runs,
398
- # so `!enabled` would treat a customer's explicit
399
- # `enabled = false` as "implicitly enabled" and restart DI.
400
- # using_default? asks "did the customer ever touch this setting",
401
- # which is the right question for RC carry-over.
402
- di_implicit = dynamic_instrumentation&.started? &&
403
- @settings.dynamic_instrumentation.using_default?(:enabled)
416
+ di_implicit = dynamic_instrumentation&.implicitly_enabled?
404
417
  ComponentsState.new(
405
418
  telemetry_enabled: telemetry.enabled,
406
419
  remote_started: remote&.started?,
407
420
  di_implicitly_enabled: di_implicit || false,
421
+ rc_enablement_signal_paths: dynamic_instrumentation&.rc_enablement_signal_paths || [],
408
422
  )
409
423
  end
424
+
425
+ def open_feature
426
+ @open_feature_activation.component
427
+ end
428
+
429
+ private
430
+
431
+ attr_reader :open_feature_activation
410
432
  end
411
433
  end
412
434
  end
@@ -5,10 +5,12 @@ module Datadog
5
5
  module Configuration
6
6
  # Stores the state of component tree when replacing the tree.
7
7
  class ComponentsState
8
- def initialize(telemetry_enabled:, remote_started:, di_implicitly_enabled: false)
8
+ def initialize(telemetry_enabled:, remote_started:, di_implicitly_enabled: false,
9
+ rc_enablement_signal_paths: [])
9
10
  @telemetry_enabled = !!telemetry_enabled
10
11
  @remote_started = !!remote_started
11
12
  @di_implicitly_enabled = !!di_implicitly_enabled
13
+ @rc_enablement_signal_paths = rc_enablement_signal_paths || []
12
14
  end
13
15
 
14
16
  def telemetry_enabled?
@@ -22,6 +24,11 @@ module Datadog
22
24
  def di_implicitly_enabled?
23
25
  @di_implicitly_enabled
24
26
  end
27
+
28
+ # The RC enable-signal carrier paths recorded by the replaced tree's DI component.
29
+ #
30
+ # @return [Array<Core::Remote::Configuration::Path>]
31
+ attr_reader :rc_enablement_signal_paths
25
32
  end
26
33
  end
27
34
  end
@@ -51,6 +51,7 @@ module Datadog
51
51
  "DD_DBM_INJECT_SQL_BASEHASH",
52
52
  "DD_DBM_PROPAGATION_MODE",
53
53
  "DD_DISABLE_DATADOG_RAILS",
54
+ "DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT_MS",
54
55
  "DD_DYNAMIC_INSTRUMENTATION_ENABLED",
55
56
  "DD_DYNAMIC_INSTRUMENTATION_MAX_TIME_TO_SERIALIZE",
56
57
  "DD_DYNAMIC_INSTRUMENTATION_PROBE_FILE",
@@ -71,6 +72,7 @@ module Datadog
71
72
  "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS",
72
73
  "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS",
73
74
  "DD_FEATURE_FLAGS_ENABLED",
75
+ "DD_FEATURE_FLAGS_EVALUATION_TELEMETRY_PER_SUBJECT_OPT_OUT_ENABLED",
74
76
  "DD_FLAGGING_EVALUATION_COUNTS_ENABLED",
75
77
  "DD_GIT_COMMIT_SHA",
76
78
  "DD_GIT_REPOSITORY_URL",
@@ -241,6 +243,7 @@ module Datadog
241
243
  "DD_TRACE_HTTP_ANALYTICS_ENABLED",
242
244
  "DD_TRACE_HTTP_ANALYTICS_SAMPLE_RATE",
243
245
  "DD_TRACE_HTTP_CLIENT_ERROR_STATUSES",
246
+ "DD_TRACE_HTTP_CLIENT_RESOURCE_NAME_QUANTIZE",
244
247
  "DD_TRACE_HTTP_DISTRIBUTED_TRACING",
245
248
  "DD_TRACE_HTTP_ENABLED",
246
249
  "DD_TRACE_HTTP_ERROR_STATUS_CODES",
@@ -399,6 +402,7 @@ module Datadog
399
402
 
400
403
  ALIASES =
401
404
  {"DD_DISABLE_DATADOG_RAILS" => ["DISABLE_DATADOG_RAILS"],
405
+ "DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT_MS" => ["DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT"],
402
406
  "DD_PROFILING_GVL_ENABLED" => ["DD_PROFILING_PREVIEW_GVL_ENABLED"],
403
407
  "DD_RUNTIME_METRICS_RUNTIME_ID_ENABLED" => ["DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED"],
404
408
  "DD_SERVICE" => ["OTEL_SERVICE_NAME"],
@@ -409,10 +413,11 @@ module Datadog
409
413
  "DD_TRACE_SAMPLE_RATE" => ["OTEL_TRACES_SAMPLER"]}.freeze
410
414
 
411
415
  DEPRECATIONS =
412
- Set["DD_PROFILING_PREVIEW_GVL_ENABLED", "DISABLE_DATADOG_RAILS"]
416
+ Set["DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT", "DD_PROFILING_PREVIEW_GVL_ENABLED", "DISABLE_DATADOG_RAILS"]
413
417
 
414
418
  ALIAS_TO_CANONICAL =
415
- {"DD_PROFILING_PREVIEW_GVL_ENABLED" => "DD_PROFILING_GVL_ENABLED",
419
+ {"DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT" => "DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT_MS",
420
+ "DD_PROFILING_PREVIEW_GVL_ENABLED" => "DD_PROFILING_GVL_ENABLED",
416
421
  "DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED" => "DD_RUNTIME_METRICS_RUNTIME_ID_ENABLED",
417
422
  "DISABLE_DATADOG_RAILS" => "DD_DISABLE_DATADOG_RAILS",
418
423
  "OTEL_LOG_LEVEL" => "DD_TRACE_DEBUG",
@@ -23,7 +23,11 @@ module Datadog
23
23
  metadata = get_metadata(settings)
24
24
 
25
25
  shutdown!
26
- @file_descriptor = _native_store_tracer_metadata(Datadog.logger, **metadata)
26
+ @file_descriptor = _native_store_tracer_metadata(
27
+ Datadog.logger,
28
+ settings.tracing.otel_thread_context_enabled,
29
+ **metadata
30
+ )
27
31
  end
28
32
 
29
33
  def shutdown!
@@ -6,7 +6,6 @@ require_relative "../../../tracing/remote"
6
6
  require_relative "../../../di/remote"
7
7
  require_relative "../../../symbol_database"
8
8
  require_relative "../../../symbol_database/remote"
9
- require_relative "../../../open_feature/remote"
10
9
 
11
10
  module Datadog
12
11
  module Core
@@ -14,40 +13,57 @@ module Datadog
14
13
  class Client
15
14
  # Capabilities
16
15
  class Capabilities
17
- attr_reader :capabilities, :receivers, :base64_capabilities
18
-
19
- def initialize(settings, telemetry, open_feature_component_provider: nil)
20
- open_feature_component_provider ||= -> {}
16
+ def initialize(settings, telemetry:)
21
17
  @capabilities = []
22
18
  @products = []
23
- @products_mutex = Mutex.new
24
19
  @receivers = []
25
20
  @telemetry = telemetry
21
+ @mutex = Mutex.new
22
+
23
+ register(settings)
26
24
 
27
- register(settings, open_feature_component_provider)
25
+ @base64_capabilities = capabilities_to_base64(@capabilities)
26
+ end
28
27
 
29
- @base64_capabilities = capabilities_to_base64
28
+ def capabilities
29
+ @mutex.synchronize { @capabilities.dup }
30
30
  end
31
31
 
32
32
  def products
33
- @products_mutex.synchronize { @products.dup }
33
+ @mutex.synchronize { @products.dup }
34
34
  end
35
35
 
36
- def add_products(*products)
37
- @products_mutex.synchronize do
38
- products.each { |product| @products << product unless @products.include?(product) }
36
+ def receivers
37
+ @mutex.synchronize { @receivers.dup }
38
+ end
39
+
40
+ def base64_capabilities
41
+ @mutex.synchronize { @base64_capabilities }
42
+ end
43
+
44
+ def register_runtime(capabilities:, products:, receivers:)
45
+ @mutex.synchronize do
46
+ register_capabilities(capabilities)
47
+ register_products(products)
48
+ register_receivers(receivers)
49
+ @base64_capabilities = capabilities_to_base64(@capabilities)
39
50
  end
40
51
  nil
41
52
  end
42
53
 
54
+ def add_products(*products)
55
+ @mutex.synchronize { register_products(products) }
56
+ nil
57
+ end
58
+
43
59
  def remove_products(*products)
44
- @products_mutex.synchronize { products.each { |product| @products.delete(product) } }
60
+ @mutex.synchronize { products.each { |product| @products.delete(product) } }
45
61
  nil
46
62
  end
47
63
 
48
64
  private
49
65
 
50
- def register(settings, open_feature_component_provider)
66
+ def register(settings)
51
67
  if settings.respond_to?(:appsec) && settings.appsec.enabled
52
68
  register_capabilities(Datadog::AppSec::Remote.capabilities)
53
69
  register_products(Datadog::AppSec::Remote.products)
@@ -95,32 +111,21 @@ module Datadog
95
111
  end
96
112
  end
97
113
  end
98
-
99
- if settings.respond_to?(:open_feature) && settings.open_feature.enabled
100
- register_capabilities(Datadog::OpenFeature::Remote.capabilities)
101
- register_products(Datadog::OpenFeature::Remote.products)
102
- register_receivers(
103
- Datadog::OpenFeature::Remote.receivers(
104
- @telemetry,
105
- component_provider: open_feature_component_provider,
106
- ),
107
- )
108
- end
109
114
  end
110
115
 
111
116
  def register_capabilities(capabilities)
112
- @capabilities.concat(capabilities)
117
+ capabilities.each { |capability| @capabilities << capability unless @capabilities.include?(capability) }
113
118
  end
114
119
 
115
120
  def register_receivers(receivers)
116
- @receivers.concat(receivers)
121
+ receivers.each { |receiver| @receivers << receiver unless @receivers.include?(receiver) }
117
122
  end
118
123
 
119
124
  def register_products(products)
120
- @products.concat(products)
125
+ products.each { |product| @products << product unless @products.include?(product) }
121
126
  end
122
127
 
123
- def capabilities_to_base64
128
+ def capabilities_to_base64(capabilities)
124
129
  return "" if capabilities.empty?
125
130
 
126
131
  cap_to_hexs = capabilities.reduce(:|).to_s(16).tap { |s| s.size.odd? && s.prepend("0") }.scan(/\h\h/)
@@ -16,6 +16,12 @@ module Datadog
16
16
 
17
17
  attr_reader :transport, :repository, :id, :dispatcher, :settings, :logger
18
18
 
19
+ # Whether the initial reconcile is still pending, to run on the first
20
+ # sync whose response contains targets.
21
+ #
22
+ # @return [Boolean]
23
+ attr_reader :pending_initial_reconcile
24
+
19
25
  def initialize(transport, capabilities, settings:, logger:, repository: Configuration::Repository.new)
20
26
  @transport = transport
21
27
  @settings = settings
@@ -25,27 +31,52 @@ module Datadog
25
31
  @id = SecureRandom.uuid
26
32
  @capabilities = capabilities
27
33
  @dispatcher = Dispatcher.new(@capabilities.receivers)
34
+ @pending_initial_reconcile = true
28
35
  end
29
36
 
37
+ # Polls the agent for remote configuration, applies the response to
38
+ # the repository and dispatches the resulting changes to the receivers.
39
+ #
40
+ # The agent sends an empty response when it holds no targets newer
41
+ # than the targets version the client reported. Until the client
42
+ # applies a response that carries targets, it reports the initial
43
+ # targets version, so an empty response then means the agent has not
44
+ # received targets from the backend. The initial reconcile therefore
45
+ # waits for a response that carries targets.
46
+ #
47
+ # @return [void]
48
+ # @raise [TransportError] when the agent reports an internal error
49
+ # @raise [SyncError] when the response cannot be applied to the repository
30
50
  def sync
31
51
  # TODO: Skip sync if no capabilities are registered
32
52
  response = transport.send_config(payload)
33
53
 
34
54
  if response.ok?
35
- process_response(response)
55
+ changes = process_response(response)
56
+ if pending_initial_reconcile && !response.empty?
57
+ dispatcher.reconcile(changes, repository)
58
+ @pending_initial_reconcile = false
59
+ end
36
60
  elsif response.internal_error?
37
61
  raise TransportError, response.to_s
38
62
  end
63
+ nil
39
64
  end
40
65
 
41
66
  private
42
67
 
68
+ # Applies an OK response to the repository and returns the resulting
69
+ # change set, which is empty when the response is empty.
70
+ #
71
+ # @param response [Transport::Config::Response] the response returned by the agent
72
+ # @return [Configuration::Repository::ChangeSet] the changes applied by this response
73
+ # @raise [SyncError] when the response cannot be applied to the repository
43
74
  def process_response(response)
44
75
  # when response is completely empty, do nothing as in: leave as is
45
76
  if response.empty?
46
77
  logger.debug { "remote: empty response => NOOP" }
47
78
 
48
- return
79
+ return Configuration::Repository::ChangeSet.new
49
80
  end
50
81
 
51
82
  begin
@@ -61,7 +92,7 @@ module Datadog
61
92
  end
62
93
 
63
94
  # To make sure steep does not complain
64
- return unless paths && targets && contents
95
+ return Configuration::Repository::ChangeSet.new unless paths && targets && contents
65
96
 
66
97
  # TODO: sometimes it can strangely be so that paths.empty?
67
98
  # TODO: sometimes it can strangely be so that targets.empty?
@@ -69,6 +100,14 @@ module Datadog
69
100
  apply_config(paths, targets, contents)
70
101
  end
71
102
 
103
+ # Applies the response to the repository in a transaction, dispatches the
104
+ # resulting changes to the receivers and returns them.
105
+ #
106
+ # @param paths [Array<Configuration::Path>] ingress paths from the response
107
+ # @param targets [Configuration::TargetMap] targets keyed by path
108
+ # @param contents [Configuration::ContentList] contents from the response
109
+ # @return [Configuration::Repository::ChangeSet] the changes applied by the transaction
110
+ # @raise [SyncError] when a path has no matching target or content
72
111
  def apply_config(paths, targets, contents)
73
112
  changes = repository.transaction do |current, transaction|
74
113
  # paths to be removed: previously applied paths minus ingress paths
@@ -121,6 +160,8 @@ module Datadog
121
160
  else
122
161
  dispatcher.dispatch(changes, repository)
123
162
  end
163
+
164
+ changes
124
165
  end
125
166
 
126
167
  def payload # standard:disable Metrics/MethodLength