datadog 2.46.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -2
  3. data/ext/datadog_profiling_native_extension/collectors_stack.c +31 -14
  4. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +1 -1
  5. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +3 -0
  6. data/ext/libdatadog_extconf_helpers.rb +1 -1
  7. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +29 -2
  8. data/lib/datadog/appsec/contrib/rails/patches/render_to_body_patch.rb +8 -7
  9. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +14 -2
  10. data/lib/datadog/appsec/contrib/sinatra/patches/json_patch.rb +8 -7
  11. data/lib/datadog/appsec/security_engine/engine.rb +2 -1
  12. data/lib/datadog/core/configuration/components.rb +11 -10
  13. data/lib/datadog/core/configuration/components_state.rb +8 -1
  14. data/lib/datadog/core/configuration/supported_configurations.rb +2 -0
  15. data/lib/datadog/core/remote/client.rb +44 -3
  16. data/lib/datadog/core/remote/dispatcher.rb +34 -3
  17. data/lib/datadog/core/workers/runtime_metrics.rb +1 -1
  18. data/lib/datadog/di/capture_expression_evaluator.rb +1 -1
  19. data/lib/datadog/di/component.rb +70 -0
  20. data/lib/datadog/di/remote.rb +46 -2
  21. data/lib/datadog/di/serializer.rb +27 -11
  22. data/lib/datadog/open_feature/configuration.rb +6 -0
  23. data/lib/datadog/open_feature/hooks/evaluation_telemetry_opt_out_hook.rb +25 -0
  24. data/lib/datadog/open_feature/provider.rb +22 -3
  25. data/lib/datadog/profiling/collectors/info.rb +37 -6
  26. data/lib/datadog/profiling/component.rb +22 -20
  27. data/lib/datadog/profiling/exporter.rb +3 -5
  28. data/lib/datadog/tracing/component.rb +7 -2
  29. data/lib/datadog/tracing/configuration/ext.rb +2 -0
  30. data/lib/datadog/tracing/configuration/settings.rb +11 -0
  31. data/lib/datadog/tracing/context_provider.rb +8 -0
  32. data/lib/datadog/tracing/contrib/ethon/easy_patch.rb +10 -5
  33. data/lib/datadog/tracing/contrib/excon/middleware.rb +10 -3
  34. data/lib/datadog/tracing/contrib/faraday/middleware.rb +10 -4
  35. data/lib/datadog/tracing/contrib/http/instrumentation.rb +16 -1
  36. data/lib/datadog/tracing/contrib/httpclient/instrumentation.rb +8 -2
  37. data/lib/datadog/tracing/contrib/httprb/instrumentation.rb +10 -4
  38. data/lib/datadog/tracing/contrib/rest_client/request_patch.rb +10 -3
  39. data/lib/datadog/tracing/contrib/utils/quantization/http.rb +102 -0
  40. data/lib/datadog/tracing/remote.rb +50 -19
  41. data/lib/datadog/version.rb +1 -1
  42. metadata +8 -8
  43. data/lib/datadog/appsec/utils/hash_coercion.rb +0 -23
@@ -122,6 +122,7 @@ module Datadog
122
122
  # cannot race a foreground start! with a background stop!.
123
123
  @lifecycle_mutex = Mutex.new
124
124
  @started = false
125
+ @rc_enablement_signal_paths = []
125
126
  end
126
127
 
127
128
  attr_reader :settings
@@ -201,6 +202,75 @@ module Datadog
201
202
  @started
202
203
  end
203
204
 
205
+ # Whether the component is started while the customer left
206
+ # DD_DYNAMIC_INSTRUMENTATION_ENABLED at its default, the state produced
207
+ # by remote-configuration enablement.
208
+ #
209
+ # An explicit `enabled = false` takes precedence over a prior RC
210
+ # enablement: once the customer sets the option, this returns false
211
+ # even while the component is still started.
212
+ #
213
+ # @return [Boolean]
214
+ def implicitly_enabled?
215
+ started? && settings.dynamic_instrumentation.using_default?(:enabled)
216
+ end
217
+
218
+ # Whether an APM_TRACING merge that omitted dynamic_instrumentation_enabled
219
+ # withdraws this component's RC enable signal, so the component must stop.
220
+ #
221
+ # Returns true when the component is implicitly enabled and no unparseable
222
+ # content can be the signal carrier. A content at a recorded signal path can be
223
+ # the carrier. When no paths are recorded, every unparseable content can be the
224
+ # carrier.
225
+ #
226
+ # @param unparseable_contents [Array<Core::Remote::Configuration::Content>] the APM_TRACING contents that failed to parse at this merge
227
+ # @return [Boolean]
228
+ def rc_enablement_signal_withdrawn?(unparseable_contents)
229
+ return false unless implicitly_enabled?
230
+
231
+ recorded_signal_paths = rc_enablement_signal_paths
232
+ signal_source_unparseable =
233
+ if recorded_signal_paths.empty?
234
+ unparseable_contents.any?
235
+ else
236
+ unparseable_contents.any? { |content| recorded_signal_paths.include?(content.path) }
237
+ end
238
+ if signal_source_unparseable
239
+ if recorded_signal_paths.empty?
240
+ logger.trace do
241
+ "APM_TRACING RC: dynamic_instrumentation_enabled absent while an APM_TRACING config " \
242
+ "failed to parse; the enable signal carrier is unknown (no signal paths recorded), " \
243
+ "keeping the DI state"
244
+ end
245
+ else
246
+ logger.trace do
247
+ "APM_TRACING RC: dynamic_instrumentation_enabled absent while the config that carried " \
248
+ "the enable signal failed to parse; keeping the DI state"
249
+ end
250
+ end
251
+ false
252
+ else
253
+ logger.trace { "APM_TRACING RC: dynamic_instrumentation_enabled withdrawn; treating as false" }
254
+ true
255
+ end
256
+ end
257
+
258
+ # The repository paths of the APM_TRACING contents that supplied the
259
+ # dynamic_instrumentation_enabled value at the last merge where the field was
260
+ # present. The component holds the paths so that they outlive the RC client.
261
+ #
262
+ # @return [Array<Core::Remote::Configuration::Path>]
263
+ attr_reader :rc_enablement_signal_paths
264
+
265
+ # Replaces the recorded enable-signal carrier paths.
266
+ #
267
+ # @param paths [Array<Core::Remote::Configuration::Path>] the carrier paths, empty when the merged field was absent
268
+ # @return [void]
269
+ def record_rc_enablement_signal_paths(paths)
270
+ @rc_enablement_signal_paths = paths.dup
271
+ nil
272
+ end
273
+
204
274
  # Shuts down dynamic instrumentation permanently.
205
275
  #
206
276
  # Removes all code hooks and stops background threads.
@@ -98,6 +98,9 @@ module Datadog
98
98
  # and DI.activate_tracking is defined.
99
99
  DI.activate_tracking
100
100
  was_started = component.started?
101
+ if repository && !was_started
102
+ remove_withdrawn_probes(repository, component)
103
+ end
101
104
  component.start!
102
105
  # A probe delivered in an earlier poll while the component was stopped
103
106
  # was dropped by #receivers (which ignores changes while !started?) and
@@ -145,7 +148,9 @@ module Datadog
145
148
  # DEV: Apply this refactor to AppSec as well if implemented.
146
149
 
147
150
  component = DI.component
148
- if component&.started?
151
+ if changes.empty?
152
+ remove_withdrawn_probes(repository, component) if component
153
+ elsif component&.started?
149
154
  changes.each do |change|
150
155
  case change.type
151
156
  when :insert
@@ -176,7 +181,7 @@ module Datadog
176
181
 
177
182
  def receiver(products = [PRODUCT], &block)
178
183
  matcher = Core::Remote::Dispatcher::Matcher::Product.new(products)
179
- [Core::Remote::Dispatcher::Receiver.new(matcher, &block)]
184
+ [Core::Remote::Dispatcher::Receiver.new(matcher, initial_reconcile: true, &block)]
180
185
  end
181
186
 
182
187
  private
@@ -266,6 +271,45 @@ module Datadog
266
271
  end
267
272
  end
268
273
 
274
+ # Removes every probe the component tracks whose id is absent from all
275
+ # LIVE_DEBUGGING contents in the repository. Reconciles backend deletions that
276
+ # produced no delete change for {.receivers}. Skips removal while a
277
+ # LIVE_DEBUGGING content fails to parse, because that content can hold any
278
+ # tracked probe.
279
+ def remove_withdrawn_probes(repository, component)
280
+ # @type var present_probe_ids: Array[String]
281
+ present_probe_ids = []
282
+ content_failed_to_parse = false
283
+ repository.contents.each do |content|
284
+ next unless content.path.product == PRODUCT
285
+
286
+ begin
287
+ present_probe_ids << parse_content(content)["id"]
288
+ rescue => exc
289
+ component.logger.debug do
290
+ "di: skipping withdrawn-probe reconcile, a LIVE_DEBUGGING content failed to parse: " \
291
+ "#{exc.class}: #{exc.message}"
292
+ end
293
+ content_failed_to_parse = true
294
+ break
295
+ end
296
+ end
297
+ return if content_failed_to_parse
298
+
299
+ probe_repository = component.probe_manager.probe_repository
300
+ tracked_probe_ids = probe_repository.installed_probes.keys | probe_repository.pending_probes.keys
301
+ (tracked_probe_ids - present_probe_ids).each do |probe_id|
302
+ component.logger.trace { "di: removing probe #{probe_id} withdrawn from remote configuration" }
303
+ component.probe_manager.remove_probe(probe_id)
304
+ rescue Exception => exc # standard:disable Lint/RescueException
305
+ Datadog::DI.reraise_if_fatal(exc)
306
+ raise if component.settings.dynamic_instrumentation.internal.propagate_all_exceptions
307
+
308
+ component.logger.debug { "di: error removing withdrawn probe #{probe_id}: #{exc.class}: #{exc.message}" }
309
+ component.telemetry&.report(exc, description: "Error removing withdrawn probe")
310
+ end
311
+ end
312
+
269
313
  def probe_known?(probe_id, component)
270
314
  repo = component.probe_manager.probe_repository
271
315
  !!(repo.find_installed(probe_id) ||
@@ -96,8 +96,25 @@ module Datadog
96
96
  # hex string, or UTF-8 with replacement characters) before returning
97
97
  # it from the custom serializer.
98
98
  @@flat_registry = []
99
- def self.register(condition: nil, &block)
99
+ # Registers a custom serializer. The block serializes each value
100
+ # for which +condition+ returns a truthy value.
101
+ #
102
+ # @param condition [Proc] receives the value being serialized and
103
+ # returns whether this serializer applies to it
104
+ # @param block [Proc] serializer for the values +condition+ accepts
105
+ # @raise [ArgumentError] if +condition+ is not a Proc or +block+ is absent
106
+ # @return [void]
107
+ def self.register(condition:, &block)
108
+ unless condition.is_a?(Proc)
109
+ raise ArgumentError, "Serializer.register condition must be a Proc, got #{condition.class}"
110
+ end
111
+
112
+ unless block
113
+ raise ArgumentError, "Serializer.register requires a serializer block"
114
+ end
115
+
100
116
  @@flat_registry << {condition: condition, proc: block}
117
+ nil
101
118
  end
102
119
 
103
120
  # Initializes the serializer.
@@ -159,7 +176,7 @@ module Datadog
159
176
  # @param args [Array] positional arguments passed to the probed method
160
177
  # @param kwargs [Hash{Symbol => Object}] keyword arguments passed to the probed method
161
178
  # @param target_self [any] receiver of the probed method invocation
162
- # @param deadline [Float] absolute capture deadline in monotonic clock seconds
179
+ # @param deadline [Float] absolute capture deadline in thread CPU clock seconds
163
180
  # @return [Hash{Symbol => Hash}] serialized arguments keyed by argument name
164
181
  def serialize_args(args, kwargs, target_self,
165
182
  deadline:,
@@ -178,7 +195,7 @@ module Datadog
178
195
  # of executed code.
179
196
  #
180
197
  # @param vars [Hash{Symbol => Object}] variables to serialize, keyed by variable name
181
- # @param deadline [Float] absolute capture deadline in monotonic clock seconds
198
+ # @param deadline [Float] absolute capture deadline in thread CPU clock seconds
182
199
  # @return [Hash{Symbol => Hash}] serialized variables keyed by variable name
183
200
  def serialize_vars(vars,
184
201
  deadline:,
@@ -207,7 +224,7 @@ module Datadog
207
224
  #
208
225
  # @param value [any] value to serialize
209
226
  # @param name [Symbol, String, nil] name the value is bound to, used for redaction
210
- # @param deadline [Float] absolute capture deadline in monotonic clock seconds
227
+ # @param deadline [Float] absolute capture deadline in thread CPU clock seconds
211
228
  # @return [Hash{Symbol => Object}] serialized value; the {type:, notCapturedReason: "timeout"} stub when the deadline is exceeded
212
229
  def serialize_value(value, deadline:,
213
230
  name: nil,
@@ -541,21 +558,20 @@ module Datadog
541
558
  "#<#{class_name(value.class)}: serialization error>"
542
559
  end
543
560
 
544
- # Computes the absolute monotonic deadline (in seconds) for a capture
545
- # point from max_time_to_serialize_ms. Resolve once per capture point and
546
- # share across all values serialized on the built-in paths so the
547
- # budget is not exceeded.
561
+ # Computes the absolute thread CPU time deadline (in seconds) for a
562
+ # capture point from max_time_to_serialize_ms.
548
563
  #
549
- # @return [Float] absolute monotonic clock reading, in seconds, at
564
+ # @return [Float] absolute thread CPU clock reading, in seconds, at
550
565
  # which the capture budget is exhausted
551
566
  def serialization_deadline
552
- Core::Utils::Time.get_time + settings.dynamic_instrumentation.max_time_to_serialize_ms / 1000.0
567
+ Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID, :float_second) +
568
+ settings.dynamic_instrumentation.max_time_to_serialize_ms / 1000.0
553
569
  end
554
570
 
555
571
  private
556
572
 
557
573
  def deadline_exceeded?(deadline)
558
- Core::Utils::Time.get_time >= deadline
574
+ Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID, :float_second) >= deadline
559
575
  end
560
576
 
561
577
  MAX_MESSAGE_COLLECTION_SIZE = 3
@@ -78,6 +78,12 @@ module Datadog
78
78
  o.default true
79
79
  end
80
80
 
81
+ option :evaluation_telemetry_per_subject_opt_out_enabled do |o|
82
+ o.type :bool
83
+ o.env "DD_FEATURE_FLAGS_EVALUATION_TELEMETRY_PER_SUBJECT_OPT_OUT_ENABLED"
84
+ o.default false
85
+ end
86
+
81
87
  option :configuration_source do |o|
82
88
  o.type :string
83
89
  o.env "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE"
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module OpenFeature
5
+ module Hooks
6
+ class EvaluationTelemetryOptOutHook
7
+ PARAMETER = "dd_evaluation_telemetry_opt_out"
8
+
9
+ def self.opted_out?(evaluation_context)
10
+ evaluation_context&.fields&.[](PARAMETER).equal?(true)
11
+ end
12
+
13
+ def initialize(hook)
14
+ @hook = hook
15
+ end
16
+
17
+ def finally(hook_context:, evaluation_details:, **options)
18
+ return if self.class.opted_out?(hook_context.evaluation_context)
19
+
20
+ @hook.finally(hook_context: hook_context, evaluation_details: evaluation_details, **options)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -3,6 +3,7 @@
3
3
  require_relative "ext"
4
4
  require_relative "../open_feature"
5
5
  require_relative "../core/utils/time"
6
+ require_relative "hooks/evaluation_telemetry_opt_out_hook"
6
7
  require "open_feature/sdk"
7
8
 
8
9
  module Datadog
@@ -67,7 +68,15 @@ module Datadog
67
68
 
68
69
  attr_reader :metadata
69
70
 
70
- def initialize
71
+ def initialize(enable_evaluation_telemetry_opt_out_parameter: nil)
72
+ @enable_evaluation_telemetry_opt_out_parameter = case enable_evaluation_telemetry_opt_out_parameter
73
+ when nil
74
+ Datadog.configuration.feature_flags.evaluation_telemetry_per_subject_opt_out_enabled
75
+ when true, false
76
+ enable_evaluation_telemetry_opt_out_parameter
77
+ else
78
+ raise ArgumentError, "enable_evaluation_telemetry_opt_out_parameter expects true, false, or nil"
79
+ end
71
80
  @metadata = ::OpenFeature::SDK::Provider::ProviderMetadata.new(name: NAME).freeze
72
81
  @initialization_mutex = Mutex.new
73
82
  @initializing = false
@@ -168,10 +177,13 @@ module Datadog
168
177
 
169
178
  def hooks
170
179
  component = Datadog.send(:components).open_feature
171
- [
180
+ hooks = [
172
181
  component&.flag_eval_metrics_hook,
173
182
  component&.flag_eval_evp_hook,
174
183
  ].compact
184
+ return hooks unless @enable_evaluation_telemetry_opt_out_parameter
185
+
186
+ hooks.map { |hook| Hooks::EvaluationTelemetryOptOutHook.new(hook) }
175
187
  end
176
188
 
177
189
  def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
@@ -406,7 +418,9 @@ module Datadog
406
418
  # Drive APM span enrichment directly from the evaluation path. This is the only reliable
407
419
  # way to attach `ffe_*` tags across all supported OpenFeature SDK versions, since older
408
420
  # SDKs do not dispatch provider hooks. Guarded so enrichment can never break evaluation.
409
- enrich_span(flag_key, result, evaluation_context)
421
+ unless evaluation_telemetry_opted_out?(evaluation_context)
422
+ enrich_span(flag_key, result, evaluation_context)
423
+ end
410
424
 
411
425
  sdk_success_details(result, flag_meta)
412
426
  rescue => e
@@ -495,6 +509,11 @@ module Datadog
495
509
  Datadog.send(:components).open_feature&.span_enrichment_hook
496
510
  end
497
511
 
512
+ def evaluation_telemetry_opted_out?(evaluation_context)
513
+ @enable_evaluation_telemetry_opt_out_parameter &&
514
+ Hooks::EvaluationTelemetryOptOutHook.opted_out?(evaluation_context)
515
+ end
516
+
498
517
  def component_not_configured_default(value, eval_time_ms)
499
518
  ::OpenFeature::SDK::Provider::ResolutionDetails.new(
500
519
  value: value,
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
3
4
  require "libdatadog"
4
5
  require_relative "../../core/utils/time"
5
6
 
@@ -9,11 +10,11 @@ module Datadog
9
10
  # Collects information of relevance for profiler. This will get sent alongside
10
11
  # the profile and show up in the UI or potentially influence processing in some way.
11
12
  #
12
- # Information is currently collected and frozen at construction time. A full collector
13
- # could be seen as overkill for this case but it allows us to centralize information
14
- # gathering and easily support more flexible/dynamic info collection in the future.
13
+ # Most information is static: collected and frozen at construction time. The exception is
14
+ # what `collect_dynamic_info` collects, which can change during the process lifetime.
15
15
  class Info
16
- # @rbs @info: ::Hash[::Symbol, ::Hash[::Symbol, untyped]]
16
+ # @rbs @static_info: ::Hash[::Symbol, ::Hash[::Symbol, untyped]]
17
+ # @rbs @info_json_cache: [::Hash[::Symbol, bool], ::String]?
17
18
  # @rbs @platform_info: ::Hash[::Symbol, untyped]
18
19
  # @rbs @runtime_info: ::Hash[::Symbol, untyped]
19
20
  # @rbs @application_info: ::Hash[::Symbol, untyped]
@@ -24,18 +25,48 @@ module Datadog
24
25
  def initialize(settings)
25
26
  @profiler_info = nil
26
27
 
27
- @info = {
28
+ @static_info = {
28
29
  platform: collect_platform_info,
29
30
  runtime: collect_runtime_info,
30
31
  application: collect_application_info(settings),
31
32
  profiler: collect_profiler_info(settings),
32
33
  }.freeze
34
+ @info_json_cache = nil
33
35
  end
34
36
 
35
- attr_reader :info #: ::Hash[::Symbol, ::Hash[::Symbol, untyped]]
37
+ #: () -> ::Hash[::Symbol, ::Hash[::Symbol, untyped]]
38
+ def info
39
+ build_info(collect_dynamic_info)
40
+ end
41
+
42
+ # Serializing the same info on every flush would be wasteful, so the latest result is
43
+ # cached and reused for as long as the dynamic info is unchanged.
44
+ #: () -> ::String
45
+ def info_json
46
+ dynamic_info = collect_dynamic_info
47
+ cached_dynamic_info, cached_json = @info_json_cache
48
+ return cached_json if cached_json && cached_dynamic_info == dynamic_info
49
+
50
+ json = JSON.generate(build_info(dynamic_info)).freeze
51
+ @info_json_cache = [dynamic_info, json].freeze #: [::Hash[::Symbol, bool], ::String]
52
+ json
53
+ end
36
54
 
37
55
  private
38
56
 
57
+ #: (::Hash[::Symbol, bool]) -> ::Hash[::Symbol, ::Hash[::Symbol, untyped]]
58
+ def build_info(dynamic_info)
59
+ @static_info.merge(runtime: @static_info.fetch(:runtime).merge(dynamic_info))
60
+ end
61
+
62
+ #: () -> ::Hash[::Symbol, bool]
63
+ def collect_dynamic_info
64
+ {
65
+ yjit_enabled: defined?(::RubyVM::YJIT) ? ::RubyVM::YJIT.enabled? : false,
66
+ zjit_enabled: defined?(::RubyVM::ZJIT) ? ::RubyVM::ZJIT.enabled? : false,
67
+ }
68
+ end
69
+
39
70
  # Ruby GC tuning environment variables
40
71
  RUBY_GC_TUNING_ENV_VARS = [
41
72
  "RUBY_GC_HEAP_FREE_SLOTS",
@@ -427,33 +427,35 @@ module Datadog
427
427
  end
428
428
  end
429
429
 
430
- # To add just a bit more complexity to our detection code, in https://github.com/DataDog/dd-trace-rb/issues/3334
431
- # a user reported that our code was incorrectly flagging the mariadb variant of libmysqlclient as being
432
- # incompatible. In fact we have no reports of the mariadb variant needing the "no signals" workaround,
433
- # so we flag it as compatible when it's in use.
430
+ # MariaDB's client has diverged from Oracle's and has no reported need for the "no signals" workaround, as
431
+ # reported in https://github.com/DataDog/dd-trace-rb/issues/3334, so we flag it as compatible when it's in use.
434
432
  #
435
- # A problem is that there doesn't seem to be an obvious way to query the mysql2 gem on which kind of
436
- # libmysqlclient it's using, so we detect it by looking at the version.
433
+ # There's no way to ask the mysql2 gem which client it was built against, so we infer it from the two versions
434
+ # that `Mysql2::Client.info` exposes:
437
435
  #
438
- # The info method for mysql2 with mariadb looks something like this:
439
- # `{:id=>30308, :version=>"3.3.8", :header_version=>"11.2.2"}`
436
+ # * `version` is `mysql_get_client_info()`. For MariaDB that's the version of
437
+ # https://github.com/mariadb-corporation/mariadb-connector-c, which is still 3.x.
438
+ # * `header_version` is a constant baked in when mysql2 was compiled. Up to mysql2 0.5.6 it was the MariaDB
439
+ # server version, which is what people usually call "the mariadb version"; 0.5.7 switched it to the
440
+ # Connector/C version (https://github.com/brianmario/mysql2/pull/1406) and that broke our detection, as
441
+ # reported in https://github.com/DataDog/dd-trace-rb/issues/6536.
440
442
  #
441
- # * The version seems to come from https://github.com/mariadb-corporation/mariadb-connector-c and the latest
442
- # one is 3.x.
443
- # * The header_version is what people usually see as the "mariadb version"
443
+ # So MariaDB shows up as `{version: "3.3.8", header_version: "11.2.2"}` with mysql2 < 0.5.7 and as
444
+ # `{version: "3.4.9", header_version: "3.4.9"}` from 0.5.7 on. We accept both shapes: a header that is a server
445
+ # version (10 or newer, a sane range for modern releases per https://endoflife.date/mariadb) or one that is a
446
+ # Connector/C version (3.x, matching the client version).
444
447
  #
445
- # As a comparison, for libmysql the info looks like:
446
- # * `{:id=>80035, :version=>"8.0.35", :header_version=>"8.0.35"}`
447
- #
448
- # Thus our detection is version 4 or older, because libmysqlclient 4 is almost 20 years old so it's most probably
449
- # not that one + header_version being 10 or newer, since according to https://endoflife.date/mariadb that's a
450
- # sane range for modern mariadb releases.
448
+ # Oracle's client repeats one version in both fields (`{version: "8.0.35", header_version: "8.0.35"}`), and our
449
+ # caller already considers 8.0.0 and above compatible, so only a client older than 5.0.0 gets this far -- and
450
+ # no Oracle release has been one since libmysqlclient 4.1, which is from 2004.
451
451
  private_class_method def self.looks_like_mariadb?(info, libmysqlclient_version)
452
452
  header_version = Gem::Version.new(info[:header_version]) if info[:header_version]
453
453
 
454
- !!(header_version &&
455
- libmysqlclient_version < Gem::Version.new("5.0.0") &&
456
- header_version >= Gem::Version.new("10.0.0"))
454
+ return false unless header_version && libmysqlclient_version < Gem::Version.new("5.0.0")
455
+ return true if header_version >= Gem::Version.new("10.0.0")
456
+
457
+ connector_c_versions = Gem::Requirement.new(">= 3.0.0", "< 4.0.0")
458
+ connector_c_versions.satisfied_by?(libmysqlclient_version) && connector_c_versions.satisfied_by?(header_version)
457
459
  end
458
460
 
459
461
  private_class_method def self.dir_interruption_workaround_enabled?(settings, no_signals_workaround_enabled)
@@ -27,7 +27,7 @@ module Datadog
27
27
  attr_reader :last_flush_finish_at #: ::Time?
28
28
  attr_reader :created_at #: ::Time
29
29
  attr_reader :internal_metadata #: ::Hash[::Symbol, untyped]
30
- attr_reader :info_json #: ::String
30
+ attr_reader :info_collector #: Datadog::Profiling::Collectors::Info
31
31
  attr_reader :sequence_tracker #: singleton(Datadog::Profiling::SequenceTracker)
32
32
 
33
33
  public
@@ -56,9 +56,7 @@ module Datadog
56
56
  @last_flush_finish_at = nil
57
57
  @created_at = time_provider.now.utc
58
58
  @internal_metadata = internal_metadata
59
- # NOTE: At the time of this comment collected info does not change over time so we'll hardcode
60
- # it on startup to prevent serializing the same info on every flush.
61
- @info_json = JSON.generate(info_collector.info).freeze
59
+ @info_collector = info_collector
62
60
  @sequence_tracker = sequence_tracker
63
61
  end
64
62
 
@@ -111,7 +109,7 @@ module Datadog
111
109
  gc: gc_stats,
112
110
  }
113
111
  ),
114
- info_json: info_json,
112
+ info_json: info_collector.info_json,
115
113
  )
116
114
  end
117
115
 
@@ -21,14 +21,14 @@ module Datadog
21
21
  tracer = settings.tracing.instance
22
22
  return tracer unless tracer.nil?
23
23
 
24
+ sampler = build_sampler(settings)
25
+
24
26
  # Apply test mode settings if test mode is activated
25
27
  if settings.tracing.test_mode.enabled
26
28
  trace_flush = build_test_mode_trace_flush(settings)
27
- sampler = build_test_mode_sampler
28
29
  writer = build_test_mode_writer(settings, agent_settings)
29
30
  else
30
31
  trace_flush = build_trace_flush(settings)
31
- sampler = build_sampler(settings)
32
32
  writer = build_writer(settings, agent_settings)
33
33
  end
34
34
 
@@ -76,7 +76,12 @@ module Datadog
76
76
  nil
77
77
  end
78
78
 
79
+ # Builds the sampler for the given settings: the sampler that keeps
80
+ # every trace when test mode is enabled, otherwise the sampler built
81
+ # from the custom sampler, sampling rules and rates.
79
82
  def build_sampler(settings)
83
+ return build_test_mode_sampler if settings.tracing.test_mode.enabled
84
+
80
85
  # A custom sampler is provided
81
86
  if (sampler = settings.tracing.sampler)
82
87
  return sampler
@@ -128,6 +128,8 @@ module Datadog
128
128
  ENV_SERVER_ERROR_STATUSES = "DD_TRACE_HTTP_SERVER_ERROR_STATUSES"
129
129
  ENV_CLIENT_ERROR_STATUSES = "DD_TRACE_HTTP_CLIENT_ERROR_STATUSES"
130
130
  end
131
+
132
+ ENV_HTTP_CLIENT_RESOURCE_NAME_QUANTIZE = "DD_TRACE_HTTP_CLIENT_RESOURCE_NAME_QUANTIZE"
131
133
  end
132
134
  end
133
135
  end
@@ -568,6 +568,17 @@ module Datadog
568
568
  o.default 512
569
569
  end
570
570
 
571
+ # Whether outbound HTTP client resource names include a quantized request path.
572
+ # URL tags and outgoing requests are unchanged.
573
+ #
574
+ # @default `DD_TRACE_HTTP_CLIENT_RESOURCE_NAME_QUANTIZE` environment variable, otherwise `false`.
575
+ # @return [Boolean]
576
+ option :http_client_resource_name_quantize do |o|
577
+ o.env Tracing::Configuration::Ext::ENV_HTTP_CLIENT_RESOURCE_NAME_QUANTIZE
578
+ o.default false
579
+ o.type :bool
580
+ end
581
+
571
582
  # HTTP error statuses configuration
572
583
  # @public_api
573
584
  settings :http_error_statuses do
@@ -21,6 +21,14 @@ module Datadog
21
21
  def context=(ctx)
22
22
  ctx.otel_thread_context = @otel_thread_context if ctx.respond_to?(:otel_thread_context=)
23
23
 
24
+ if (otel_thread_context = @otel_thread_context)
25
+ if (trace = ctx&.active_trace)
26
+ otel_thread_context.update_from_trace_op(trace)
27
+ else
28
+ otel_thread_context.clear
29
+ end
30
+ end
31
+
24
32
  @context.local = ctx
25
33
  end
26
34
 
@@ -7,6 +7,7 @@ require_relative "../../metadata/ext"
7
7
  require_relative "../http"
8
8
  require_relative "ext"
9
9
  require_relative "../http_annotation_helper"
10
+ require_relative "../utils/quantization/http"
10
11
 
11
12
  module Datadog
12
13
  module Tracing
@@ -111,7 +112,7 @@ module Datadog
111
112
  )
112
113
  datadog_trace = Tracing.active_trace
113
114
 
114
- datadog_tag_request
115
+ datadog_tag_request(uri)
115
116
 
116
117
  if Tracing::Distributed::PropagationPolicy.enabled?(
117
118
  global_config: datadog_configuration,
@@ -131,12 +132,17 @@ module Datadog
131
132
 
132
133
  attr_reader :datadog_configuration
133
134
 
134
- def datadog_tag_request
135
+ def datadog_tag_request(uri)
135
136
  span = @datadog_span
136
137
  span.set_tag(Tracing::Metadata::Ext::TAG_SVC_SRC, Ext::TAG_COMPONENT)
137
138
  method = Ext::NOT_APPLICABLE_METHOD
138
139
  method = @datadog_method.to_s if instance_variable_defined?(:@datadog_method) && !@datadog_method.nil?
139
- span.resource = method
140
+ path = uri&.path
141
+ span.resource = Contrib::Utils::Quantization::HTTP.client_resource(
142
+ method,
143
+ path,
144
+ enabled: Datadog.configuration.tracing.http_client_resource_name_quantize
145
+ )
140
146
 
141
147
  if datadog_configuration[:peer_service]
142
148
  span.set_tag(
@@ -153,10 +159,9 @@ module Datadog
153
159
 
154
160
  span.set_tag(Tracing::Metadata::Ext::TAG_KIND, Tracing::Metadata::Ext::SpanKind::TAG_CLIENT)
155
161
 
156
- uri = try_parse_uri
157
162
  return unless uri
158
163
 
159
- span.set_tag(Tracing::Metadata::Ext::HTTP::TAG_URL, uri.path)
164
+ span.set_tag(Tracing::Metadata::Ext::HTTP::TAG_URL, path)
160
165
  span.set_tag(Tracing::Metadata::Ext::HTTP::TAG_METHOD, method)
161
166
  span.set_tag(Tracing::Metadata::Ext::NET::TAG_TARGET_HOST, uri.host)
162
167
  span.set_tag(Tracing::Metadata::Ext::NET::TAG_TARGET_PORT, uri.port)
@@ -8,6 +8,7 @@ require_relative "../http"
8
8
  require_relative "../analytics"
9
9
  require_relative "ext"
10
10
  require_relative "../http_annotation_helper"
11
+ require_relative "../utils/quantization/http"
11
12
 
12
13
  module Datadog
13
14
  module Tracing
@@ -117,7 +118,13 @@ module Datadog
117
118
  end
118
119
 
119
120
  def annotate!(span, datum)
120
- span.resource = datum[:method].to_s.upcase
121
+ http_method = datum[:method].to_s.upcase
122
+ path = datum[:path]
123
+ span.resource = Contrib::Utils::Quantization::HTTP.client_resource(
124
+ http_method,
125
+ path,
126
+ enabled: Datadog.configuration.tracing.http_client_resource_name_quantize
127
+ )
121
128
  span.service = service_name(datum[:host], @options)
122
129
  span.set_tag(Tracing::Metadata::Ext::TAG_SVC_SRC, Ext::TAG_COMPONENT)
123
130
  span.type = Tracing::Metadata::Ext::HTTP::TYPE_OUTBOUND
@@ -139,8 +146,8 @@ module Datadog
139
146
  # Set analytics sample rate
140
147
  Contrib::Analytics.set_sample_rate(span, analytics_sample_rate) if analytics_enabled?
141
148
 
142
- span.set_tag(Tracing::Metadata::Ext::HTTP::TAG_URL, datum[:path])
143
- span.set_tag(Tracing::Metadata::Ext::HTTP::TAG_METHOD, datum[:method].to_s.upcase)
149
+ span.set_tag(Tracing::Metadata::Ext::HTTP::TAG_URL, path)
150
+ span.set_tag(Tracing::Metadata::Ext::HTTP::TAG_METHOD, http_method)
144
151
  span.set_tag(Tracing::Metadata::Ext::NET::TAG_TARGET_HOST, datum[:host])
145
152
  span.set_tag(Tracing::Metadata::Ext::NET::TAG_TARGET_PORT, datum[:port])
146
153
  span.set_tags(