datadog 2.36.0 → 2.37.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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -1
  3. data/ext/datadog_profiling_native_extension/clock_id_from_mach.c +5 -12
  4. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +6 -3
  5. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +17 -3
  6. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +219 -98
  7. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +2 -0
  8. data/ext/datadog_profiling_native_extension/extconf.rb +11 -0
  9. data/ext/datadog_profiling_native_extension/ruby_helpers.c +7 -0
  10. data/ext/libdatadog_api/di.c +76 -0
  11. data/ext/libdatadog_api/extconf.rb +6 -0
  12. data/ext/libdatadog_extconf_helpers.rb +1 -1
  13. data/lib/datadog/ai_guard/api_client.rb +6 -4
  14. data/lib/datadog/appsec/api_security/route_extractor.rb +7 -4
  15. data/lib/datadog/appsec/configuration.rb +6 -0
  16. data/lib/datadog/appsec/context.rb +4 -2
  17. data/lib/datadog/appsec/contrib/excon/ssrf_detection_middleware.rb +59 -4
  18. data/lib/datadog/appsec/contrib/faraday/ssrf_detection_middleware.rb +59 -5
  19. data/lib/datadog/appsec/contrib/graphql/gateway/multiplex.rb +1 -1
  20. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +4 -0
  21. data/lib/datadog/appsec/contrib/rack/input_peeker.rb +4 -17
  22. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +21 -0
  23. data/lib/datadog/appsec/contrib/rails/gateway/request.rb +2 -1
  24. data/lib/datadog/appsec/contrib/rest_client/request_ssrf_detection_patch.rb +60 -7
  25. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +4 -0
  26. data/lib/datadog/appsec/ext.rb +1 -0
  27. data/lib/datadog/appsec/metrics/collector.rb +18 -1
  28. data/lib/datadog/appsec/metrics/exporter.rb +9 -3
  29. data/lib/datadog/appsec/remote.rb +9 -3
  30. data/lib/datadog/appsec/route_normalizer/rails_route_pattern.rb +176 -0
  31. data/lib/datadog/appsec/route_normalizer/route_pattern.rb +378 -0
  32. data/lib/datadog/appsec/route_normalizer/route_text.rb +57 -0
  33. data/lib/datadog/appsec/route_normalizer.rb +80 -0
  34. data/lib/datadog/appsec/utils/http/body_reader.rb +61 -0
  35. data/lib/datadog/core/configuration/components.rb +82 -2
  36. data/lib/datadog/core/configuration/components_state.rb +6 -1
  37. data/lib/datadog/core/configuration/settings.rb +0 -3
  38. data/lib/datadog/core/configuration/supported_configurations.rb +2 -0
  39. data/lib/datadog/core/evp.rb +11 -0
  40. data/lib/datadog/core/remote/client/capabilities.rb +34 -7
  41. data/lib/datadog/core/telemetry/event/app_started.rb +8 -1
  42. data/lib/datadog/di/base.rb +3 -1
  43. data/lib/datadog/di/code_tracker.rb +5 -2
  44. data/lib/datadog/di/component.rb +110 -36
  45. data/lib/datadog/di/error.rb +10 -0
  46. data/lib/datadog/di/fatal_exceptions.rb +26 -0
  47. data/lib/datadog/di/instrumenter.rb +381 -165
  48. data/lib/datadog/di/probe.rb +14 -0
  49. data/lib/datadog/di/probe_file_loader.rb +8 -3
  50. data/lib/datadog/di/probe_manager.rb +67 -9
  51. data/lib/datadog/di/probe_notification_builder.rb +4 -1
  52. data/lib/datadog/di/probe_notifier_worker.rb +52 -32
  53. data/lib/datadog/di/redactor.rb +16 -1
  54. data/lib/datadog/di/remote.rb +175 -14
  55. data/lib/datadog/di/serializer.rb +12 -5
  56. data/lib/datadog/di/transport/input.rb +8 -4
  57. data/lib/datadog/di.rb +63 -0
  58. data/lib/datadog/kit/appsec/events/v2.rb +60 -2
  59. data/lib/datadog/open_feature/component.rb +29 -6
  60. data/lib/datadog/open_feature/configuration.rb +8 -0
  61. data/lib/datadog/open_feature/ext.rb +2 -0
  62. data/lib/datadog/open_feature/flag_evaluation/aggregator.rb +286 -0
  63. data/lib/datadog/open_feature/flag_evaluation/writer.rb +433 -0
  64. data/lib/datadog/open_feature/hooks/flag_eval_evp_hook.rb +101 -0
  65. data/lib/datadog/open_feature/hooks/{flag_eval_hook.rb → flag_eval_metrics_hook.rb} +1 -1
  66. data/lib/datadog/open_feature/native_evaluator.rb +22 -0
  67. data/lib/datadog/open_feature/provider.rb +59 -27
  68. data/lib/datadog/open_feature/transport.rb +55 -1
  69. data/lib/datadog/opentelemetry/sdk/logs_exporter.rb +5 -12
  70. data/lib/datadog/opentelemetry/sdk/metrics_exporter.rb +4 -11
  71. data/lib/datadog/opentelemetry/sdk.rb +19 -0
  72. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +1 -1
  73. data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +5 -3
  74. data/lib/datadog/symbol_database/component.rb +164 -47
  75. data/lib/datadog/symbol_database/configuration.rb +12 -4
  76. data/lib/datadog/symbol_database/extensions.rb +19 -0
  77. data/lib/datadog/symbol_database/extractor.rb +124 -87
  78. data/lib/datadog/symbol_database/file_hash.rb +3 -1
  79. data/lib/datadog/symbol_database/remote.rb +8 -3
  80. data/lib/datadog/symbol_database/scope_batcher.rb +7 -3
  81. data/lib/datadog/symbol_database/uploader.rb +9 -2
  82. data/lib/datadog/symbol_database.rb +22 -0
  83. data/lib/datadog/tracing/distributed/baggage.rb +2 -1
  84. data/lib/datadog/tracing/ext.rb +9 -0
  85. data/lib/datadog/tracing/remote.rb +34 -3
  86. data/lib/datadog/version.rb +1 -1
  87. data/lib/datadog.rb +1 -0
  88. metadata +19 -6
@@ -1,13 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'ext'
4
+ require_relative '../core/utils/time'
4
5
  require 'open_feature/sdk'
5
6
 
6
7
  module Datadog
7
8
  module OpenFeature
8
9
  # OpenFeature feature flagging provider backed by Datadog Remote Configuration.
9
10
  #
10
- # Requires openfeature-sdk >= 0.5.1 for flag evaluation metrics support.
11
+ # Requires openfeature-sdk >= 0.5.1 for flag evaluation metrics and EVP hook support.
12
+ #
13
+ # Hook lifecycle note: FlagEvalEVPHook is returned from #hooks so EVP uses the SDK-final
14
+ # EvaluationDetails. This matches FlagEvalMetricsHook and records defaults/errors produced
15
+ # by OpenFeature hook failures or post-provider type validation.
11
16
  #
12
17
  # Implementation follows the OpenFeature contract of Provider SDK.
13
18
  # For details see:
@@ -70,8 +75,10 @@ module Datadog
70
75
  end
71
76
 
72
77
  def hooks
73
- hook = Datadog.send(:components).open_feature&.flag_eval_hook
74
- [hook].compact
78
+ component = Datadog.send(:components).open_feature
79
+ otel_hook = component&.flag_eval_metrics_hook
80
+ evp_hook = component&.flag_eval_evp_hook
81
+ [otel_hook, evp_hook].compact
75
82
  end
76
83
 
77
84
  def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
@@ -101,57 +108,82 @@ module Datadog
101
108
  private
102
109
 
103
110
  def evaluate(flag_key, default_value:, expected_type:, evaluation_context:)
111
+ # Stamp evaluation entry time once, here on the eval thread. The EVP path uses this for
112
+ # accurate first/last_evaluation bounds instead of a later hook-fire clock read.
113
+ eval_time_ms = (Core::Utils::Time.now.to_f * 1000).to_i
114
+
104
115
  engine = OpenFeature.engine
105
- return component_not_configured_default(default_value) if engine.nil?
116
+ return component_not_configured_default(default_value, eval_time_ms) if engine.nil?
117
+
118
+ result = fetch_engine_value(engine, flag_key, default_value, expected_type, evaluation_context)
119
+
120
+ # Build metadata before branching so provider-returned details carry eval-entry time.
121
+ flag_meta = build_flag_metadata(result, eval_time_ms)
122
+
123
+ if result.error?
124
+ return sdk_error_details(default_value, result.error_code, result.error_message, result.reason, flag_meta)
125
+ end
126
+
127
+ sdk_success_details(result, flag_meta)
128
+ rescue => e
129
+ error_message = "#{e.class}: #{e.message}"
130
+ error_result = Datadog::OpenFeature::ResolutionDetails.build_error(
131
+ value: default_value,
132
+ error_code: Ext::GENERAL,
133
+ error_message: error_message
134
+ )
135
+ error_flag_meta = build_flag_metadata(error_result, eval_time_ms || (Core::Utils::Time.now.to_f * 1000).to_i)
106
136
 
107
- result = engine.fetch_value(
137
+ sdk_error_details(default_value, Ext::GENERAL, error_message, Ext::ERROR, error_flag_meta)
138
+ end
139
+
140
+ def fetch_engine_value(engine, flag_key, default_value, expected_type, evaluation_context)
141
+ engine.fetch_value(
108
142
  flag_key,
109
143
  default_value: default_value,
110
144
  expected_type: expected_type,
111
145
  evaluation_context: evaluation_context
112
146
  )
147
+ end
113
148
 
114
- if result.error?
115
- return ::OpenFeature::SDK::Provider::ResolutionDetails.new(
116
- value: default_value,
117
- error_code: result.error_code,
118
- error_message: result.error_message,
119
- reason: result.reason
120
- )
121
- end
122
-
149
+ def sdk_success_details(result, flag_meta)
123
150
  ::OpenFeature::SDK::Provider::ResolutionDetails.new(
124
151
  value: result.value,
125
152
  variant: result.variant,
126
153
  reason: result.reason,
127
- flag_metadata: build_flag_metadata(result),
154
+ flag_metadata: flag_meta,
128
155
  )
129
- rescue => e
156
+ end
157
+
158
+ def sdk_error_details(default_value, error_code, error_message, reason, flag_meta = {})
130
159
  ::OpenFeature::SDK::Provider::ResolutionDetails.new(
131
160
  value: default_value,
132
- error_code: Ext::GENERAL,
133
- error_message: "#{e.class}: #{e.message}",
134
- reason: Ext::ERROR
161
+ error_code: error_code,
162
+ error_message: error_message,
163
+ reason: reason,
164
+ flag_metadata: flag_meta
135
165
  )
136
166
  end
137
167
 
138
- def build_flag_metadata(result)
139
- metadata = result.flag_metadata || {}
168
+ def build_flag_metadata(result, eval_time_ms)
169
+ metadata = result.flag_metadata&.dup || {}
140
170
  allocation_key = result.allocation_key
141
- if allocation_key && !allocation_key.empty?
142
- metadata = metadata.dup
143
- metadata['__dd_allocation_key'] = allocation_key
144
- end
171
+ metadata['__dd_allocation_key'] = allocation_key if allocation_key && !allocation_key.empty?
172
+
173
+ # Eval-time stamped at provider entry; the EVP hook reads 'dd.eval.timestamp_ms' for
174
+ # accurate first/last_evaluation bounds (it falls back to hook-fire time when absent).
175
+ metadata['dd.eval.timestamp_ms'] = eval_time_ms
145
176
 
146
177
  metadata
147
178
  end
148
179
 
149
- def component_not_configured_default(value)
180
+ def component_not_configured_default(value, eval_time_ms)
150
181
  ::OpenFeature::SDK::Provider::ResolutionDetails.new(
151
182
  value: value,
152
183
  error_code: Ext::PROVIDER_FATAL,
153
184
  error_message: "Datadog's OpenFeature component must be configured",
154
- reason: Ext::ERROR
185
+ reason: Ext::ERROR,
186
+ flag_metadata: {'dd.eval.timestamp_ms' => eval_time_ms}
155
187
  )
156
188
  end
157
189
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../core/encoding'
4
+ require_relative '../core/evp'
4
5
  require_relative '../core/transport/http'
5
6
  require_relative '../core/transport/http/env'
6
7
  require_relative '../core/transport/http/api/endpoint'
@@ -12,6 +13,7 @@ module Datadog
12
13
  module OpenFeature
13
14
  module Transport
14
15
  class HTTP
16
+ # API spec for the EVP exposures endpoint.
15
17
  class Spec
16
18
  def initialize
17
19
  @endpoint = Core::Transport::HTTP::API::Endpoint.new(
@@ -23,7 +25,30 @@ module Datadog
23
25
  def call(env, &block)
24
26
  @endpoint.call(env) do |request_env|
25
27
  request_env.headers['Content-Type'] = env.request.parcel.content_type
26
- request_env.headers['X-Datadog-EVP-Subdomain'] = 'event-platform-intake'
28
+ request_env.headers[Core::EVP::SUBDOMAIN_HEADER_NAME] =
29
+ Core::EVP::EVENT_PLATFORM_INTAKE_SUBDOMAIN
30
+ request_env.body = env.request.parcel.data
31
+
32
+ block.call(request_env)
33
+ end
34
+ end
35
+ end
36
+
37
+ # API spec for the EVP flagevaluation endpoint.
38
+ # Path: /evp_proxy/v2/api/v2/flagevaluation
39
+ # Header: X-Datadog-EVP-Subdomain: event-platform-intake (same as exposures)
40
+ class FlagevaluationsSpec
41
+ def initialize
42
+ @endpoint = Core::Transport::HTTP::API::Endpoint.new(
43
+ :post, '/evp_proxy/v2/api/v2/flagevaluation'
44
+ )
45
+ end
46
+
47
+ def call(env, &block)
48
+ @endpoint.call(env) do |request_env|
49
+ request_env.headers['Content-Type'] = env.request.parcel.content_type
50
+ request_env.headers[Core::EVP::SUBDOMAIN_HEADER_NAME] =
51
+ Core::EVP::EVENT_PLATFORM_INTAKE_SUBDOMAIN
27
52
  request_env.body = env.request.parcel.data
28
53
 
29
54
  block.call(request_env)
@@ -38,6 +63,14 @@ module Datadog
38
63
  ) { |t| t.api('exposures', HTTP::Spec.new) }.to_transport(self)
39
64
  end
40
65
 
66
+ # Build a transport instance for the flagevaluation EVP endpoint.
67
+ def self.build_flagevaluations(agent_settings:, logger:)
68
+ Core::Transport::HTTP.build(
69
+ agent_settings: agent_settings,
70
+ logger: logger
71
+ ) { |t| t.api('flagevaluations', HTTP::FlagevaluationsSpec.new) }.to_transport(self)
72
+ end
73
+
41
74
  def initialize(apis, default_api, logger:)
42
75
  @api = apis[default_api]
43
76
  @logger = logger
@@ -61,6 +94,27 @@ module Datadog
61
94
 
62
95
  Core::Transport::InternalErrorResponse.new(e)
63
96
  end
97
+
98
+ # POST a flag evaluations batch to /evp_proxy/v2/api/v2/flagevaluation.
99
+ # Mirrors send_exposures; uses the FlagevaluationsSpec endpoint.
100
+ def send_flag_evaluations(payload)
101
+ encoder = Core::Encoding::JSONEncoder
102
+ parcel = Core::Transport::Parcel.new(
103
+ encoder.encode(payload),
104
+ content_type: encoder.content_type
105
+ )
106
+ request = Core::Transport::Request.new(parcel)
107
+
108
+ @api.endpoint.call(Core::Transport::HTTP::Env.new(request)) do |env|
109
+ @api.call(env)
110
+ end
111
+ rescue => e
112
+ message = "Internal error during request. Cause: #{e.class}: #{e.message} " \
113
+ "Location: #{Array(e.backtrace).first}"
114
+ @logger.debug(message)
115
+
116
+ Core::Transport::InternalErrorResponse.new(e)
117
+ end
64
118
  end
65
119
  end
66
120
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'opentelemetry/exporter/otlp_logs'
4
+ require_relative '../sdk'
4
5
 
5
6
  module Datadog
6
7
  module OpenTelemetry
@@ -10,27 +11,19 @@ module Datadog
10
11
  METRIC_EXPORT_SUCCESSES = 'otel.logs_export_successes'
11
12
  METRIC_EXPORT_FAILURES = 'otel.logs_export_failures'
12
13
  METRIC_LOG_RECORDS = 'otel.log_records'
13
- TELEMETRY_NAMESPACE = 'tracers'
14
- TELEMETRY_TAGS = {'protocol' => 'http', 'encoding' => 'protobuf'}.freeze
15
14
 
16
15
  def export(log_records, timeout: nil)
17
- telemetry&.inc(TELEMETRY_NAMESPACE, METRIC_EXPORT_ATTEMPTS, 1, tags: TELEMETRY_TAGS)
18
- telemetry&.inc(TELEMETRY_NAMESPACE, METRIC_LOG_RECORDS, log_records.size, tags: TELEMETRY_TAGS)
16
+ SDK.telemetry_inc(METRIC_EXPORT_ATTEMPTS, 1)
17
+ SDK.telemetry_inc(METRIC_LOG_RECORDS, log_records.size)
19
18
  result = super
20
19
  metric_name = (result == 0) ? METRIC_EXPORT_SUCCESSES : METRIC_EXPORT_FAILURES
21
- telemetry&.inc(TELEMETRY_NAMESPACE, metric_name, 1, tags: TELEMETRY_TAGS)
20
+ SDK.telemetry_inc(metric_name, 1)
22
21
  result
23
22
  rescue => e
24
23
  Datadog.logger.warn("Failed to export OpenTelemetry Logs: #{e.class}: #{e.message}")
25
- telemetry&.inc(TELEMETRY_NAMESPACE, METRIC_EXPORT_FAILURES, 1, tags: TELEMETRY_TAGS)
24
+ SDK.telemetry_inc(METRIC_EXPORT_FAILURES, 1)
26
25
  raise
27
26
  end
28
-
29
- private
30
-
31
- def telemetry
32
- Datadog.send(:components).telemetry
33
- end
34
27
  end
35
28
  end
36
29
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'opentelemetry/exporter/otlp_metrics'
4
+ require_relative '../sdk'
4
5
 
5
6
  module Datadog
6
7
  module OpenTelemetry
@@ -9,26 +10,18 @@ module Datadog
9
10
  METRIC_EXPORT_ATTEMPTS = 'otel.metrics_export_attempts'
10
11
  METRIC_EXPORT_SUCCESSES = 'otel.metrics_export_successes'
11
12
  METRIC_EXPORT_FAILURES = 'otel.metrics_export_failures'
12
- TELEMETRY_NAMESPACE = 'tracers'
13
- TELEMETRY_TAGS = {'protocol' => "http", 'encoding' => 'protobuf'}
14
13
 
15
14
  def export(metrics, timeout: nil)
16
- telemetry&.inc(TELEMETRY_NAMESPACE, METRIC_EXPORT_ATTEMPTS, 1, tags: TELEMETRY_TAGS)
15
+ SDK.telemetry_inc(METRIC_EXPORT_ATTEMPTS, 1)
17
16
  result = super
18
17
  metric_name = (result == 0) ? METRIC_EXPORT_SUCCESSES : METRIC_EXPORT_FAILURES
19
- telemetry&.inc(TELEMETRY_NAMESPACE, metric_name, 1, tags: TELEMETRY_TAGS)
18
+ SDK.telemetry_inc(metric_name, 1)
20
19
  result
21
20
  rescue => e
22
21
  Datadog.logger.error("Failed to export OpenTelemetry Metrics: #{e.class}: #{e.message}")
23
- telemetry&.inc(TELEMETRY_NAMESPACE, METRIC_EXPORT_FAILURES, 1, tags: TELEMETRY_TAGS)
22
+ SDK.telemetry_inc(METRIC_EXPORT_FAILURES, 1)
24
23
  raise
25
24
  end
26
-
27
- private
28
-
29
- def telemetry
30
- Datadog.send(:components).telemetry
31
- end
32
25
  end
33
26
  end
34
27
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../tracing/ext'
4
+
5
+ module Datadog
6
+ module OpenTelemetry
7
+ module SDK
8
+ TELEMETRY_TAGS = {'protocol' => 'http', 'encoding' => 'protobuf'}.freeze
9
+
10
+ def self.telemetry_inc(metric_name, value)
11
+ telemetry&.inc(Datadog::Tracing::Ext::TELEMETRY_METRICS_NAMESPACE, metric_name, value, tags: TELEMETRY_TAGS)
12
+ end
13
+
14
+ def self.telemetry
15
+ Datadog.send(:components).telemetry
16
+ end
17
+ end
18
+ end
19
+ end
@@ -49,7 +49,7 @@ module Datadog
49
49
  # profiler overhead!
50
50
  dynamic_sampling_rate_enabled: true,
51
51
  skip_idle_samples_for_testing: false,
52
- idle_sampling_helper: IdleSamplingHelper.new
52
+ idle_sampling_helper: IdleSamplingHelper.new(thread_context_collector: thread_context_collector)
53
53
  )
54
54
  unless dynamic_sampling_rate_enabled
55
55
  Datadog.logger.warn(
@@ -10,6 +10,7 @@ module Datadog
10
10
  class IdleSamplingHelper
11
11
  # @rbs @worker_thread: untyped
12
12
  # @rbs @start_stop_mutex: ::Thread::Mutex
13
+ # @rbs @thread_context_collector: Datadog::Profiling::Collectors::ThreadContext?
13
14
 
14
15
  private
15
16
 
@@ -17,10 +18,11 @@ module Datadog
17
18
 
18
19
  public
19
20
 
20
- #: () -> void
21
- def initialize
21
+ #: (thread_context_collector: Datadog::Profiling::Collectors::ThreadContext) -> void
22
+ def initialize(thread_context_collector:)
22
23
  @worker_thread = nil
23
24
  @start_stop_mutex = Mutex.new
25
+ @thread_context_collector = thread_context_collector
24
26
  end
25
27
 
26
28
  #: () -> (nil | true)
@@ -37,7 +39,7 @@ module Datadog
37
39
  @worker_thread = Thread.new do
38
40
  Thread.current.name = self.class.name
39
41
 
40
- self.class._native_idle_sampling_loop(self)
42
+ self.class._native_idle_sampling_loop(self, @thread_context_collector)
41
43
 
42
44
  Datadog.logger.debug("IdleSamplingHelper thread stopping cleanly")
43
45
  rescue Exception => e # rubocop:disable Lint/RescueException