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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 90e55c02331cafb4d96cd0dedf6edb09fc5a6c7f993ffd611d8ca13f12d42cdb
4
- data.tar.gz: a99ea41f5975708838075815674c78e47a46ef8368188186d2e20653a8372b19
3
+ metadata.gz: f4a11a75d5b532014ee82ed0628621a2586ca9b05ab6b00c7136357b7b0a5445
4
+ data.tar.gz: d51c04201a7f931546374be3adc2c6445e523e98713b60c3e7167398da2dede2
5
5
  SHA512:
6
- metadata.gz: 4061492c8bba7bf1238b6442d9275bbd7172978ecdbe4d4a92b53f0c67c2ca2d7b20947aaa42247fdc37ccbe81502710b4d69733e420e18fcd1e13d6e27b9864
7
- data.tar.gz: 6fe366c5fcb305780a575c830bc77967be48605478071c1d16975242a95cb5c178b81dac776306f5b082b27770140174d29ad0c4dd7df3c9522b4a048883303f
6
+ metadata.gz: 344ce6f6622f0c6879f598917bab789aef9345a0498d3164b7481df27fdd916b8a4b65b69893fda1527db16ad01da921340441e35a880df4207ae6806966c31a
7
+ data.tar.gz: 2a77c07e5af10203211755104d3b4e117a70ef64d673d04330769b01a350d2bc8de024b5f7fad2954d95c50a99fa3526c1a473f169653437ad73cb51cea7e542
data/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [2.47.0] - 2026-10-26
6
+
7
+ ### Added
8
+
9
+ * Tracing: Add quantized request paths to outbound HTTP client resource names, such as `GET /users/*`; enable with `DD_TRACE_HTTP_CLIENT_RESOURCE_NAME_QUANTIZE=true`. ([#6298][])
10
+ * Open Feature: Let evaluations opt out of flag evaluation events, metrics, and span tags with `dd_evaluation_telemetry_opt_out: true` in the evaluation context; enable it with `DD_FEATURE_FLAGS_EVALUATION_TELEMETRY_PER_SUBJECT_OPT_OUT_ENABLED=true`. ([#6486][])
11
+
12
+ ### Changed
13
+
14
+ * AppSec: Update `libddwaf` to 1.30.0.1.0. ([#6548][])
15
+
16
+ ### Fixed
17
+
18
+ * Core: Fix forked processes, such as Puma and Unicorn workers, reporting no runtime metrics when the parent process traced before forking. ([#6500][]) ([@dduugg][])
19
+ * Profiling: Fix the "no signals" workaround being enabled when `mysql2` uses MariaDB Connector/C 3.x. ([#6543][])
20
+ * AppSec: Fix ruleset updates with Remote Configuration. ([#6547][])
21
+ * AppSec: Fix duplicate JSON serialization of Rails and Sinatra response objects when application security is enabled. ([#6548][])
22
+
5
23
  ## [2.46.0] - 2026-10-19
6
24
 
7
25
  ### Added
@@ -3851,7 +3869,8 @@ Release notes: https://github.com/DataDog/dd-trace-rb/releases/tag/v0.3.1
3851
3869
  Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
3852
3870
 
3853
3871
 
3854
- [Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v2.46.0...master
3872
+ [Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v2.47.0...master
3873
+ [2.47.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.46.0...v2.47.0
3855
3874
  [2.46.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.45.0...v2.46.0
3856
3875
  [2.45.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.44.0...v2.45.0
3857
3876
  [2.44.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.43.0...v2.44.0
@@ -5694,6 +5713,7 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
5694
5713
  [#6261]: https://github.com/DataDog/dd-trace-rb/issues/6261
5695
5714
  [#6286]: https://github.com/DataDog/dd-trace-rb/issues/6286
5696
5715
  [#6291]: https://github.com/DataDog/dd-trace-rb/issues/6291
5716
+ [#6298]: https://github.com/DataDog/dd-trace-rb/issues/6298
5697
5717
  [#6316]: https://github.com/DataDog/dd-trace-rb/issues/6316
5698
5718
  [#6320]: https://github.com/DataDog/dd-trace-rb/issues/6320
5699
5719
  [#6339]: https://github.com/DataDog/dd-trace-rb/issues/6339
@@ -5721,7 +5741,12 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
5721
5741
  [#6474]: https://github.com/DataDog/dd-trace-rb/issues/6474
5722
5742
  [#6481]: https://github.com/DataDog/dd-trace-rb/issues/6481
5723
5743
  [#6485]: https://github.com/DataDog/dd-trace-rb/issues/6485
5744
+ [#6486]: https://github.com/DataDog/dd-trace-rb/issues/6486
5724
5745
  [#6488]: https://github.com/DataDog/dd-trace-rb/issues/6488
5746
+ [#6500]: https://github.com/DataDog/dd-trace-rb/issues/6500
5747
+ [#6543]: https://github.com/DataDog/dd-trace-rb/issues/6543
5748
+ [#6547]: https://github.com/DataDog/dd-trace-rb/issues/6547
5749
+ [#6548]: https://github.com/DataDog/dd-trace-rb/issues/6548
5725
5750
  [@AdrianLC]: https://github.com/AdrianLC
5726
5751
  [@Azure7111]: https://github.com/Azure7111
5727
5752
  [@BabyGroot]: https://github.com/BabyGroot
@@ -5878,4 +5903,4 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
5878
5903
  [@y-yagi]: https://github.com/y-yagi
5879
5904
  [@yujideveloper]: https://github.com/yujideveloper
5880
5905
  [@yukimurasawa]: https://github.com/yukimurasawa
5881
- [@zachmccormick]: https://github.com/zachmccormick
5906
+ [@zachmccormick]: https://github.com/zachmccormick
@@ -41,7 +41,7 @@ static const char *get_or_compute_native_filename(void *function, st_table *nati
41
41
  static void initialize_static_ruby_actual_filename(VALUE profiling_module);
42
42
  static void add_truncated_frames_placeholder(ddog_prof_Location *locations);
43
43
  static void record_placeholder_stack_in_native_code(VALUE recorder_instance, sample_values values, sample_labels labels);
44
- static void maybe_trim_template_random_ids(ddog_CharSlice *name_slice, ddog_CharSlice *filename_slice);
44
+ static void maybe_trim_template_random_ids(ddog_CharSlice *frame_name, ddog_CharSlice method_name, ddog_CharSlice filename);
45
45
 
46
46
  // NULL if we weren't able to get the native filename for the Ruby VM.
47
47
  // On a static Ruby without libruby.so, `get_or_compute_native_filename` returns `ruby_native_filename` but
@@ -438,7 +438,7 @@ void sample_thread(
438
438
  frame_name = name_slice;
439
439
  }
440
440
 
441
- maybe_trim_template_random_ids(&frame_name, &filename_slice);
441
+ maybe_trim_template_random_ids(&frame_name, name_slice, filename_slice);
442
442
 
443
443
  int libdatadog_stores_stacks_flipped_from_rb_profile_frames_index = top_of_stack_position - i;
444
444
 
@@ -548,33 +548,50 @@ static const char *get_or_compute_native_filename(void *function, st_table *nati
548
548
  // the extra "_".)
549
549
  //
550
550
  // This method trims these suffixes, so that we keep less data + the names correctly aggregate together.
551
- static void maybe_trim_template_random_ids(ddog_CharSlice *name_slice, ddog_CharSlice *filename_slice) {
551
+ //
552
+ // We match on `method_name` because it always ends with the ids, while `frame_name` can have DDTRACE_BLOCK_SUFFIX
553
+ // after them ("Foo#_app_views_articles_index_html_erb__123_456 (block)"); in that case we move the suffix left over
554
+ // the ids. A `frame_name` that is a Ruby String ends with the ids we just matched, so that write only ever lands on
555
+ // a name we composed in the sample's own buffer.
556
+ static void maybe_trim_template_random_ids(ddog_CharSlice *frame_name, ddog_CharSlice method_name, ddog_CharSlice filename) {
552
557
  // Check filename doesn't end with ".rb"; templates are usually along the lines of .html.erb/.html.haml/...
553
- if (filename_slice->len < 3 || memcmp(filename_slice->ptr + filename_slice->len - 3, ".rb", 3) == 0) return;
558
+ if (filename.len < 3 || memcmp(filename.ptr + filename.len - 3, ".rb", 3) == 0) return;
554
559
 
555
- if (name_slice->len > 1024) return;
556
- if (name_slice->len == 0) return;
560
+ if (method_name.len > 1024) return;
561
+ if (method_name.len == 0) return;
557
562
 
558
- int pos = ((int) name_slice->len) - 1;
563
+ int pos = ((int) method_name.len) - 1;
559
564
 
560
565
  // Let's match on something__number_number:
561
566
  // Find start of id suffix from the end...
562
- if (name_slice->ptr[pos] < '0' || name_slice->ptr[pos] > '9') return;
567
+ if (method_name.ptr[pos] < '0' || method_name.ptr[pos] > '9') return;
563
568
 
564
569
  // ...now match a bunch of numbers and interspersed underscores
565
570
  for (int underscores = 0; pos >= 0 && underscores < 2; pos--) {
566
- if (name_slice->ptr[pos] == '_') underscores++;
567
- else if (name_slice->ptr[pos] < '0' || name_slice->ptr[pos] > '9') return;
571
+ if (method_name.ptr[pos] == '_') underscores++;
572
+ else if (method_name.ptr[pos] < '0' || method_name.ptr[pos] > '9') return;
568
573
  }
569
574
 
570
575
  // Make sure there's something left before the underscores (hence the <= instead of <) + match the last underscore
571
- if (pos <= 0 || name_slice->ptr[pos] != '_') return;
576
+ if (pos <= 0 || method_name.ptr[pos] != '_') return;
572
577
 
573
578
  // Does it have the optional third underscore? If so, remove it as well
574
- if (pos > 1 && name_slice->ptr[pos-1] == '_') pos--;
579
+ if (pos > 1 && method_name.ptr[pos-1] == '_') pos--;
580
+
581
+ // If we got here, we matched on our pattern. Let's remove the ids from the end of the frame name.
582
+ size_t random_ids_len = method_name.len - pos;
583
+
584
+ static const char block_suffix[] = DDTRACE_BLOCK_SUFFIX;
585
+ size_t block_suffix_len = sizeof(block_suffix) - 1;
586
+
587
+ if (
588
+ frame_name->len >= random_ids_len + block_suffix_len &&
589
+ memcmp(frame_name->ptr + frame_name->len - block_suffix_len, block_suffix, block_suffix_len) == 0
590
+ ) {
591
+ memcpy((char *) frame_name->ptr + frame_name->len - block_suffix_len - random_ids_len, block_suffix, block_suffix_len);
592
+ }
575
593
 
576
- // If we got here, we matched on our pattern. Let's slice the length of the string to exclude it.
577
- name_slice->len = pos;
594
+ frame_name->len -= random_ids_len;
578
595
  }
579
596
 
580
597
  static void add_truncated_frames_placeholder(ddog_prof_Location *locations) {
@@ -1136,7 +1136,7 @@ static ssize_t calculate_iseq_label(VALUE owner, const rb_iseq_t *iseq, char *bu
1136
1136
  // We use "Foo#bar (block)" because it's much faster and looks better in the UI.
1137
1137
  const rb_iseq_t* method_iseq = ISEQ_BODY(iseq)->local_iseq;
1138
1138
 
1139
- static const char suffix[] = " (block)";
1139
+ static const char suffix[] = DDTRACE_BLOCK_SUFFIX;
1140
1140
  size_t suffix_len = sizeof(suffix) - 1;
1141
1141
 
1142
1142
  // Only use the result if there is enough space for both parts
@@ -102,6 +102,9 @@ VALUE ddtrace_alloc_free_rb_mod_name(VALUE mod);
102
102
  // if the module does not have a permanent name (e.g. `#<Module:0x0123>::Foo`).
103
103
  VALUE ddtrace_permanent_mod_name(VALUE mod);
104
104
 
105
+ // Suffix ddtrace_location_label() adds to blocks, e.g. "Foo#bar (block)"
106
+ #define DDTRACE_BLOCK_SUFFIX " (block)"
107
+
105
108
  ssize_t ddtrace_location_label(const rb_callable_method_entry_t *cme, const rb_iseq_t *iseq, char *buf, size_t buf_size);
106
109
  VALUE ddtrace_location_base_label(const rb_callable_method_entry_t *cme, const rb_iseq_t *iseq);
107
110
  void* ddtrace_cme_cfunc_func(const rb_callable_method_entry_t *cme);
@@ -11,7 +11,7 @@ module Datadog
11
11
  module LibdatadogExtconfHelpers
12
12
  # Used to make sure the correct gem version gets loaded, as extconf.rb does not get run with "bundle exec" and thus
13
13
  # may see multiple libdatadog versions. See https://github.com/DataDog/dd-trace-rb/pull/2531 for the horror story.
14
- LIBDATADOG_VERSION = "~> 46.0.0.1.1"
14
+ LIBDATADOG_VERSION = "~> 46.1.0.1.1"
15
15
 
16
16
  # Used as an workaround for a limitation with how dynamic linking works in environments where the datadog gem and
17
17
  # libdatadog are moved after the extension gets compiled.
@@ -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)
@@ -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.
@@ -288,12 +288,20 @@ module Datadog
288
288
  end
289
289
  end
290
290
 
291
+ if dynamic_instrumentation && old_state
292
+ dynamic_instrumentation.record_rc_enablement_signal_paths(old_state.rc_enablement_signal_paths)
293
+ end
294
+
291
295
  if remote && old_state&.remote_started?
292
296
  # The library was reconfigured and previously it already started
293
297
  # the remote component (i.e., it received at least one request
294
298
  # through the installed Rack middleware which started the remote).
295
299
  # If the new configuration also has remote enabled, start the
296
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.
297
305
  remote.start
298
306
  end
299
307
 
@@ -405,19 +413,12 @@ module Datadog
405
413
  # di_implicitly_enabled distinguishes RC-driven start from explicit
406
414
  # start (env var or programmatic) so that an explicit
407
415
  # `enabled = false` on reconfiguration can take effect.
408
- #
409
- # using_default? — not `!enabled` — because Datadog.configure has
410
- # already mutated the singleton settings by the time #state runs,
411
- # so `!enabled` would treat a customer's explicit
412
- # `enabled = false` as "implicitly enabled" and restart DI.
413
- # using_default? asks "did the customer ever touch this setting",
414
- # which is the right question for RC carry-over.
415
- di_implicit = dynamic_instrumentation&.started? &&
416
- @settings.dynamic_instrumentation.using_default?(:enabled)
416
+ di_implicit = dynamic_instrumentation&.implicitly_enabled?
417
417
  ComponentsState.new(
418
418
  telemetry_enabled: telemetry.enabled,
419
419
  remote_started: remote&.started?,
420
420
  di_implicitly_enabled: di_implicit || false,
421
+ rc_enablement_signal_paths: dynamic_instrumentation&.rc_enablement_signal_paths || [],
421
422
  )
422
423
  end
423
424
 
@@ -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
@@ -72,6 +72,7 @@ module Datadog
72
72
  "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS",
73
73
  "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS",
74
74
  "DD_FEATURE_FLAGS_ENABLED",
75
+ "DD_FEATURE_FLAGS_EVALUATION_TELEMETRY_PER_SUBJECT_OPT_OUT_ENABLED",
75
76
  "DD_FLAGGING_EVALUATION_COUNTS_ENABLED",
76
77
  "DD_GIT_COMMIT_SHA",
77
78
  "DD_GIT_REPOSITORY_URL",
@@ -242,6 +243,7 @@ module Datadog
242
243
  "DD_TRACE_HTTP_ANALYTICS_ENABLED",
243
244
  "DD_TRACE_HTTP_ANALYTICS_SAMPLE_RATE",
244
245
  "DD_TRACE_HTTP_CLIENT_ERROR_STATUSES",
246
+ "DD_TRACE_HTTP_CLIENT_RESOURCE_NAME_QUANTIZE",
245
247
  "DD_TRACE_HTTP_DISTRIBUTED_TRACING",
246
248
  "DD_TRACE_HTTP_ENABLED",
247
249
  "DD_TRACE_HTTP_ERROR_STATUS_CODES",
@@ -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
@@ -23,16 +23,39 @@ module Datadog
23
23
 
24
24
  def dispatch(changes, repository)
25
25
  receivers.each do |receiver|
26
- matching_changes = changes.select { |c| receiver.match?(c.path) }
26
+ matching = matching_changes(changes, receiver)
27
27
 
28
- receiver.call(repository, matching_changes) if matching_changes.any?
28
+ receiver.call(repository, matching) if matching.any?
29
29
  end
30
+ nil
31
+ end
32
+
33
+ # Calls each receiver that declares initial-reconcile participation and has no
34
+ # matching change in `changes`, passing an empty change list.
35
+ def reconcile(changes, repository)
36
+ receivers.each do |receiver|
37
+ next unless receiver.initial_reconcile?
38
+
39
+ receiver.call(repository, []) unless any_matching?(changes, receiver)
40
+ end
41
+ nil
42
+ end
43
+
44
+ private
45
+
46
+ def matching_changes(changes, receiver)
47
+ changes.select { |change| receiver.match?(change.path) }
48
+ end
49
+
50
+ def any_matching?(changes, receiver)
51
+ changes.any? { |change| receiver.match?(change.path) }
30
52
  end
31
53
 
32
54
  # Store Matcher and block to be executed on a match
33
55
  class Receiver
34
- def initialize(matcher, &block)
56
+ def initialize(matcher, initial_reconcile: false, &block)
35
57
  @block = block
58
+ @initial_reconcile = initial_reconcile
36
59
  @matcher = matcher
37
60
  end
38
61
 
@@ -40,6 +63,14 @@ module Datadog
40
63
  @matcher.match?(path)
41
64
  end
42
65
 
66
+ # Whether #reconcile may call this receiver with an empty change
67
+ # list on the client's first sync. A participating receiver must
68
+ # act on the repository's current contents when the change list is
69
+ # empty.
70
+ def initial_reconcile?
71
+ @initial_reconcile
72
+ end
73
+
43
74
  def call(repository, changes)
44
75
  @block.call(repository, changes)
45
76
  end
@@ -32,7 +32,7 @@ module Datadog
32
32
  end
33
33
 
34
34
  # Workers::Async::Thread settings
35
- self.fork_policy = options.fetch(:fork_policy, Workers::Async::Thread::FORK_POLICY_STOP)
35
+ self.fork_policy = options.fetch(:fork_policy, Workers::Async::Thread::FORK_POLICY_RESTART)
36
36
 
37
37
  # Workers::IntervalLoop settings
38
38
  self.loop_base_interval = options.fetch(:interval, DEFAULT_FLUSH_INTERVAL)
@@ -59,7 +59,7 @@ module Datadog
59
59
  probe.capture_expressions.each do |capture_expression|
60
60
  name = capture_expression.name
61
61
 
62
- if Core::Utils::Time.get_time >= deadline
62
+ if Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID, :float_second) >= deadline
63
63
  output[name] = {notCapturedReason: "timeout"}
64
64
  telemetry&.inc(DI::TELEMETRY_NAMESPACE, "capture_expressions_skipped_by_timeout", 1)
65
65
  guardrails_telemetry.skipped(