datadog 2.41.0 → 2.42.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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -2
  3. data/ext/datadog_profiling_native_extension/NativeExtensionDesign.md +12 -24
  4. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +110 -61
  5. data/ext/datadog_profiling_native_extension/collectors_stack.c +11 -3
  6. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +141 -103
  7. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +7 -3
  8. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +0 -10
  9. data/ext/datadog_profiling_native_extension/extconf.rb +52 -93
  10. data/ext/datadog_profiling_native_extension/heap_recorder.c +338 -255
  11. data/ext/datadog_profiling_native_extension/heap_recorder.h +46 -31
  12. data/ext/datadog_profiling_native_extension/native_extension_helpers.rb +0 -24
  13. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +509 -440
  14. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +15 -3
  15. data/ext/datadog_profiling_native_extension/profiling.c +2 -0
  16. data/ext/datadog_profiling_native_extension/ruby_helpers.c +1 -79
  17. data/ext/datadog_profiling_native_extension/ruby_helpers.h +0 -7
  18. data/ext/datadog_profiling_native_extension/stack_recorder.c +93 -61
  19. data/ext/datadog_profiling_native_extension/stack_recorder.h +12 -4
  20. data/ext/libdatadog_api/datadog_ruby_common.h +0 -10
  21. data/ext/libdatadog_api/di.c +10 -0
  22. data/ext/libdatadog_api/extconf.rb +3 -0
  23. data/ext/libdatadog_api/init.c +2 -0
  24. data/ext/libdatadog_api/otel_thread_context.c +232 -0
  25. data/ext/libdatadog_api/otel_thread_context.h +5 -0
  26. data/ext/libdatadog_extconf_helpers.rb +1 -1
  27. data/lib/datadog/appsec/assets/blocked.html +1 -108
  28. data/lib/datadog/core/configuration/components.rb +1 -0
  29. data/lib/datadog/core/crashtracking/component.rb +5 -1
  30. data/lib/datadog/data_streams/pathway_context.rb +20 -22
  31. data/lib/datadog/data_streams/processor.rb +31 -0
  32. data/lib/datadog/di/instrumenter.rb +41 -1
  33. data/lib/datadog/di/logger.rb +2 -2
  34. data/lib/datadog/di/probe.rb +9 -1
  35. data/lib/datadog/di/probe_notification_builder.rb +1 -0
  36. data/lib/datadog/di/remote.rb +3 -3
  37. data/lib/datadog/open_feature/evaluation_engine.rb +29 -3
  38. data/lib/datadog/open_feature/exposures/event.rb +10 -3
  39. data/lib/datadog/open_feature/ext.rb +19 -0
  40. data/lib/datadog/open_feature/flag_evaluation/aggregator.rb +236 -80
  41. data/lib/datadog/open_feature/flag_evaluation/writer.rb +179 -68
  42. data/lib/datadog/open_feature/hooks/flag_eval_evp_hook.rb +24 -21
  43. data/lib/datadog/open_feature/native_evaluator.rb +33 -6
  44. data/lib/datadog/open_feature/noop_evaluator.rb +5 -0
  45. data/lib/datadog/open_feature/provider.rb +11 -2
  46. data/lib/datadog/opentelemetry/sdk/propagator.rb +1 -1
  47. data/lib/datadog/opentelemetry/trace.rb +3 -0
  48. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +3 -0
  49. data/lib/datadog/profiling/collectors/thread_context.rb +0 -4
  50. data/lib/datadog/profiling/component.rb +8 -16
  51. data/lib/datadog/tracing/contrib/active_record/events/sql.rb +1 -0
  52. data/lib/datadog/tracing/distributed/baggage.rb +0 -1
  53. data/lib/datadog/tracing/distributed/datadog.rb +3 -3
  54. data/lib/datadog/tracing/distributed/propagation.rb +3 -0
  55. data/lib/datadog/tracing/distributed/trace_context.rb +14 -271
  56. data/lib/datadog/tracing/distributed/trace_state/datadog.rb +233 -0
  57. data/lib/datadog/tracing/distributed/trace_state/ext.rb +44 -0
  58. data/lib/datadog/tracing/distributed/trace_state/open_telemetry.rb +156 -0
  59. data/lib/datadog/tracing/distributed/trace_state.rb +121 -0
  60. data/lib/datadog/tracing/otel_thread_context.rb +30 -0
  61. data/lib/datadog/tracing/remote.rb +195 -27
  62. data/lib/datadog/tracing/sampling/rule_sampler.rb +2 -0
  63. data/lib/datadog/tracing/trace_digest.rb +22 -4
  64. data/lib/datadog/tracing/trace_operation.rb +22 -10
  65. data/lib/datadog/tracing/tracer.rb +5 -5
  66. data/lib/datadog/version.rb +1 -1
  67. metadata +14 -8
  68. data/lib/datadog/tracing/distributed/datadog_tags_codec.rb +0 -69
@@ -0,0 +1,232 @@
1
+ #include <ruby.h>
2
+ #include <ruby/thread.h>
3
+
4
+ #include "datadog_ruby_common.h"
5
+ #include "otel_thread_context.h"
6
+
7
+ #ifdef __linux__
8
+ #include <datadog/otel-thread-ctx.h>
9
+ extern __thread const uint8_t *otel_thread_ctx_v1;
10
+
11
+ static void otel_ctx_handle_free(void *handle) {
12
+ ddog_otel_thread_ctx_free((ddog_ThreadContextHandle *) handle);
13
+ }
14
+
15
+ static const rb_data_type_t otel_ctx_handle_t = {
16
+ .wrap_struct_name = "Datadog::Tracing::OTelThreadContext handle",
17
+ .function = {.dfree = otel_ctx_handle_free},
18
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY,
19
+ };
20
+ #endif
21
+
22
+ static ID fiber_context_slot;
23
+
24
+ DDTRACE_UNUSED static bool otel_context_enabled = false;
25
+
26
+ static VALUE native_set(VALUE _self, VALUE trace_id, VALUE span_id, VALUE local_root_span_id);
27
+ static VALUE native_supported_p(VALUE _self);
28
+ static VALUE native_enable(VALUE _self);
29
+ static VALUE native_read(VALUE _self);
30
+
31
+ void otel_thread_context_init(VALUE tracing_module) {
32
+ fiber_context_slot = rb_intern("__dd_otel_fiber_context");
33
+
34
+ VALUE otel_thread_context_module = rb_define_module_under(tracing_module, "OTelThreadContext");
35
+
36
+ rb_define_singleton_method(otel_thread_context_module, "_native_enable", native_enable, 0);
37
+ rb_define_singleton_method(otel_thread_context_module, "_native_set", native_set, 3);
38
+ rb_define_singleton_method(otel_thread_context_module, "_native_supported?", native_supported_p, 0);
39
+
40
+ VALUE testing_module = rb_define_module_under(otel_thread_context_module, "Testing");
41
+ rb_define_singleton_method(testing_module, "_native_read", native_read, 0);
42
+ }
43
+
44
+ #ifdef __linux__
45
+ static ddog_ThreadContextHandle *get_fiber_handle_for(VALUE thread) {
46
+ VALUE obj = rb_thread_local_aref(thread, fiber_context_slot);
47
+
48
+ if (!rb_typeddata_is_kind_of(obj, &otel_ctx_handle_t)) return NULL;
49
+
50
+ return RTYPEDDATA_DATA(obj);
51
+ }
52
+
53
+ static ddog_ThreadContextHandle *get_current_fiber_handle(void) {
54
+ return get_fiber_handle_for(rb_thread_current());
55
+ }
56
+
57
+ static void store_current_fiber_handle(ddog_ThreadContextHandle *handle) {
58
+ VALUE obj = TypedData_Wrap_Struct(rb_cObject, &otel_ctx_handle_t, handle);
59
+ rb_thread_local_aset(rb_thread_current(), fiber_context_slot, obj);
60
+ }
61
+
62
+ #ifdef RUBY_INTERNAL_THREAD_EVENT_EXITED
63
+ static void on_thread_exited(
64
+ DDTRACE_UNUSED rb_event_flag_t event,
65
+ DDTRACE_UNUSED const rb_internal_thread_event_data_t *event_data,
66
+ DDTRACE_UNUSED void *user_data
67
+ ) {
68
+ ddog_otel_thread_ctx_detach();
69
+ }
70
+ #else
71
+ static void on_thread_end(
72
+ DDTRACE_UNUSED rb_event_flag_t evflag,
73
+ DDTRACE_UNUSED VALUE data,
74
+ DDTRACE_UNUSED VALUE self,
75
+ DDTRACE_UNUSED ID mid,
76
+ DDTRACE_UNUSED VALUE klass
77
+ ) {
78
+ ddog_otel_thread_ctx_detach();
79
+ }
80
+ #endif
81
+
82
+ #ifdef HAVE_RUBY_THREAD_STORAGE_API
83
+ // This function is declared in internal/thread.h.
84
+ // It returns 0 when the current thread sits in a blocking region.
85
+ int ruby_thread_has_gvl_p(void);
86
+
87
+ static bool in_blocking_region(void) {
88
+ #ifdef HAVE_RUBY_THREAD_HAS_GVL_P
89
+ return ruby_thread_has_gvl_p() == 0;
90
+ #else
91
+ return false;
92
+ #endif
93
+ }
94
+
95
+ // Under M:N a Ruby thread can migrate between native threads,
96
+ // so we need to detach the context when the Ruby thread gets suspended.
97
+ static void on_thread_suspended(
98
+ DDTRACE_UNUSED rb_event_flag_t event,
99
+ DDTRACE_UNUSED const rb_internal_thread_event_data_t *event_data,
100
+ DDTRACE_UNUSED void *user_data
101
+ ) {
102
+ // Ruby pins threads that release GVL to call into a native extension,
103
+ // so we know the thread will not migrate.
104
+ if (in_blocking_region()) return;
105
+
106
+ ddog_otel_thread_ctx_detach();
107
+ }
108
+
109
+ static void on_thread_resumed(
110
+ DDTRACE_UNUSED rb_event_flag_t event,
111
+ const rb_internal_thread_event_data_t *event_data,
112
+ DDTRACE_UNUSED void *user_data
113
+ ) {
114
+ ddog_ThreadContextHandle *handle = get_fiber_handle_for(event_data->thread);
115
+
116
+ if (handle != NULL) ddog_otel_thread_ctx_attach(handle);
117
+ }
118
+ #endif
119
+
120
+ static void on_fiber_switch(
121
+ DDTRACE_UNUSED rb_event_flag_t evflag,
122
+ DDTRACE_UNUSED VALUE data,
123
+ DDTRACE_UNUSED VALUE self,
124
+ DDTRACE_UNUSED ID mid,
125
+ DDTRACE_UNUSED VALUE klass
126
+ ) {
127
+ ddog_ThreadContextHandle *handle = get_current_fiber_handle();
128
+
129
+ if (handle != NULL) {
130
+ ddog_otel_thread_ctx_attach(handle);
131
+ } else {
132
+ ddog_otel_thread_ctx_detach();
133
+ }
134
+ }
135
+ #endif
136
+
137
+ static VALUE native_enable(DDTRACE_UNUSED VALUE _self) {
138
+ #ifdef __linux__
139
+ if (otel_context_enabled) return Qtrue;
140
+ otel_context_enabled = true;
141
+
142
+ #ifdef RUBY_INTERNAL_THREAD_EVENT_EXITED
143
+ rb_internal_thread_add_event_hook(on_thread_exited, RUBY_INTERNAL_THREAD_EVENT_EXITED, NULL);
144
+ #else
145
+ rb_add_event_hook(on_thread_end, RUBY_EVENT_THREAD_END, Qnil);
146
+ #endif
147
+
148
+ // These hooks only matter under the M:N thread scheduler (Ruby 3.3+),
149
+ // where a Ruby thread can migrate between native threads.
150
+ #ifdef HAVE_RUBY_THREAD_STORAGE_API
151
+ rb_internal_thread_add_event_hook(on_thread_suspended, RUBY_INTERNAL_THREAD_EVENT_SUSPENDED, NULL);
152
+ rb_internal_thread_add_event_hook(on_thread_resumed, RUBY_INTERNAL_THREAD_EVENT_RESUMED, NULL);
153
+ #endif
154
+
155
+ rb_add_event_hook(on_fiber_switch, RUBY_EVENT_FIBER_SWITCH, Qnil);
156
+
157
+ return Qtrue;
158
+ #else
159
+ return Qfalse;
160
+ #endif
161
+ }
162
+
163
+ static VALUE native_supported_p(DDTRACE_UNUSED VALUE _self) {
164
+ #ifdef __linux__
165
+ return Qtrue;
166
+ #else
167
+ return Qfalse;
168
+ #endif
169
+ }
170
+
171
+ static VALUE native_set(
172
+ DDTRACE_UNUSED VALUE _self,
173
+ DDTRACE_UNUSED VALUE trace_id,
174
+ DDTRACE_UNUSED VALUE span_id,
175
+ DDTRACE_UNUSED VALUE local_root_span_id
176
+ ) {
177
+ #ifdef __linux__
178
+ if (!otel_context_enabled) return Qfalse;
179
+
180
+ uint8_t trace_id_bytes[16];
181
+ uint8_t span_id_bytes[8];
182
+ uint8_t local_root_span_id_bytes[8];
183
+
184
+ const int BIG_ENDIAN_PACK_FLAGS = INTEGER_PACK_MSWORD_FIRST | INTEGER_PACK_BIG_ENDIAN;
185
+ rb_integer_pack(trace_id, trace_id_bytes, sizeof(trace_id_bytes), 1, 0, BIG_ENDIAN_PACK_FLAGS);
186
+ rb_integer_pack(span_id, span_id_bytes, sizeof(span_id_bytes), 1, 0, BIG_ENDIAN_PACK_FLAGS);
187
+ rb_integer_pack(local_root_span_id, local_root_span_id_bytes, sizeof(local_root_span_id_bytes), 1, 0, BIG_ENDIAN_PACK_FLAGS);
188
+
189
+ ddog_ThreadContextHandle *handle = get_current_fiber_handle();
190
+
191
+ if (handle == NULL) {
192
+ handle = ddog_otel_thread_ctx_new(&trace_id_bytes, &span_id_bytes, 0, &local_root_span_id_bytes);
193
+ store_current_fiber_handle(handle);
194
+ ddog_otel_thread_ctx_attach(handle);
195
+ } else {
196
+ // FIXME: add a method to libdatadog to update the context record before attaching it
197
+ ddog_otel_thread_ctx_attach(handle);
198
+ ddog_otel_thread_ctx_update(&trace_id_bytes, &span_id_bytes, 0, &local_root_span_id_bytes);
199
+ }
200
+
201
+ return Qtrue;
202
+ #else
203
+ return Qfalse;
204
+ #endif
205
+ }
206
+
207
+ static VALUE native_read(DDTRACE_UNUSED VALUE _self) {
208
+ #ifdef __linux__
209
+ const uint8_t *raw = otel_thread_ctx_v1;
210
+ if (!raw) return Qnil;
211
+
212
+ uint16_t attrs_data_size = (uint16_t) raw[26] | ((uint16_t) raw[27] << 8);
213
+
214
+ if (attrs_data_size > ddog_MAX_ATTRS_DATA_SIZE) {
215
+ raise_error(
216
+ rb_eRuntimeError,
217
+ "Invalid OTel thread context record: attrs_data_size (%d) exceeds maximum (%d)",
218
+ attrs_data_size, ddog_MAX_ATTRS_DATA_SIZE
219
+ );
220
+ }
221
+
222
+ VALUE result = rb_hash_new();
223
+ rb_hash_aset(result, ID2SYM(rb_intern("trace_id")), rb_str_new((const char *) raw, 16));
224
+ rb_hash_aset(result, ID2SYM(rb_intern("span_id")), rb_str_new((const char *) (raw + 16), 8));
225
+ rb_hash_aset(result, ID2SYM(rb_intern("valid")), rb_str_new((const char *) (raw + 24), 1));
226
+ rb_hash_aset(result, ID2SYM(rb_intern("attrs")), rb_str_new((const char *) (raw + 28), attrs_data_size));
227
+
228
+ return result;
229
+ #else
230
+ return Qnil;
231
+ #endif
232
+ }
@@ -0,0 +1,5 @@
1
+ #pragma once
2
+
3
+ #include <ruby.h>
4
+
5
+ void otel_thread_context_init(VALUE tracing_module);
@@ -10,7 +10,7 @@ module Datadog
10
10
  module LibdatadogExtconfHelpers
11
11
  # Used to make sure the correct gem version gets loaded, as extconf.rb does not get run with "bundle exec" and thus
12
12
  # may see multiple libdatadog versions. See https://github.com/DataDog/dd-trace-rb/pull/2531 for the horror story.
13
- LIBDATADOG_VERSION = "~> 40.0.0.1.0"
13
+ LIBDATADOG_VERSION = "~> 40.0.0.2.0"
14
14
 
15
15
  # Used as an workaround for a limitation with how dynamic linking works in environments where the datadog gem and
16
16
  # libdatadog are moved after the extension gets compiled.
@@ -1,108 +1 @@
1
- <!-- Sorry, you've been blocked -->
2
- <!DOCTYPE html>
3
- <html lang="en">
4
-
5
- <head>
6
- <meta charset="UTF-8">
7
- <meta name="viewport" content="width=device-width,initial-scale=1">
8
- <title>You've been blocked</title>
9
- <style>
10
- a,
11
- body,
12
- div,
13
- html,
14
- span {
15
- margin: 0;
16
- padding: 0;
17
- border: 0;
18
- font-size: 100%;
19
- font: inherit;
20
- vertical-align: baseline
21
- }
22
-
23
- body {
24
- background: -webkit-radial-gradient(26% 19%, circle, #fff, #f4f7f9);
25
- background: radial-gradient(circle at 26% 19%, #fff, #f4f7f9);
26
- display: -webkit-box;
27
- display: -ms-flexbox;
28
- display: flex;
29
- -webkit-box-pack: center;
30
- -ms-flex-pack: center;
31
- justify-content: center;
32
- -webkit-box-align: center;
33
- -ms-flex-align: center;
34
- align-items: center;
35
- -ms-flex-line-pack: center;
36
- align-content: center;
37
- width: 100%;
38
- min-height: 100vh;
39
- line-height: 1;
40
- flex-direction: column
41
- }
42
-
43
- p {
44
- display: block
45
- }
46
-
47
-
48
- main {
49
- text-align: center;
50
- flex: 1;
51
- display: -webkit-box;
52
- display: -ms-flexbox;
53
- display: flex;
54
- -webkit-box-pack: center;
55
- -ms-flex-pack: center;
56
- justify-content: center;
57
- -webkit-box-align: center;
58
- -ms-flex-align: center;
59
- align-items: center;
60
- -ms-flex-line-pack: center;
61
- align-content: center;
62
- flex-direction: column
63
- }
64
-
65
- p {
66
- font-size: 18px;
67
- line-height: normal;
68
- color: #646464;
69
- font-family: sans-serif;
70
- font-weight: 400
71
- }
72
-
73
- a {
74
- color: #4842b7
75
- }
76
-
77
- footer {
78
- width: 100%;
79
- text-align: center
80
- }
81
-
82
- footer p {
83
- font-size: 16px
84
- }
85
-
86
- .security-response-id {
87
- font-size:14px;
88
- color:#999;
89
- margin-top:20px;
90
- font-family:monospace
91
- }
92
- </style>
93
- </head>
94
-
95
- <body>
96
- <main>
97
- <p>Sorry, you cannot access this page. Please contact the customer service team.</p>
98
- <p class="security-response-id">Security Response ID: [security_response_id]</p>
99
- </main>
100
- <footer>
101
- <p>Security provided by <a
102
- href="https://www.datadoghq.com/product/security-platform/application-security-monitoring/"
103
- target="_blank"
104
- rel="noopener noreferrer">Datadog</a></p>
105
- </footer>
106
- </body>
107
-
108
- </html>
1
+ <!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>You've been blocked</title><style>a,body,div,html,span{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}body{background:-webkit-radial-gradient(26% 19%,circle,#fff,#f4f7f9);background:radial-gradient(circle at 26% 19%,#fff,#f4f7f9);display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-ms-flex-line-pack:center;align-content:center;width:100%;min-height:100vh;line-height:1;flex-direction:column}p{display:block}main{text-align:center;flex:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-ms-flex-line-pack:center;align-content:center;flex-direction:column}p{font-size:18px;line-height:normal;color:#646464;font-family:sans-serif;font-weight:400}a{color:#4842b7}footer{width:100%;text-align:center}footer p{font-size:16px}.security-response-id{font-size:14px;color:#999;margin-top:20px;font-family:monospace}</style></head><body><main><p>Sorry, you cannot access this page. Please contact the customer service team.</p><p class="security-response-id">Security Response ID: [security_response_id]</p></main><footer><p>Security provided by <a href="https://www.datadoghq.com/product/security-platform/application-security-monitoring/" target="_blank" rel="noopener noreferrer">Datadog</a></p></footer></body></html>
@@ -248,6 +248,7 @@ module Datadog
248
248
  crashtracker&.update_on_fork
249
249
  ProcessDiscovery.after_fork
250
250
  symbol_database&.after_fork!
251
+ data_streams&.restart_flush_thread
251
252
  end
252
253
 
253
254
  # Hot-swaps with a new sampler.
@@ -44,7 +44,11 @@ module Datadog
44
44
  # Reports unhandled exceptions to the crash tracker if available and appropriate.
45
45
  # This is called from the at_exit hook to report unhandled exceptions.
46
46
  def self.report_unhandled_exception(exception)
47
- return unless exception && !exception.is_a?(SystemExit) && !exception.is_a?(NoMemoryError)
47
+ return unless exception &&
48
+ !exception.is_a?(SystemExit) &&
49
+ !exception.is_a?(SignalException) &&
50
+ !exception.is_a?(NoMemoryError) &&
51
+ !exception.is_a?(SystemStackError)
48
52
 
49
53
  begin
50
54
  crashtracker = Datadog.send(:components, allow_initialization: false)&.crashtracker
@@ -98,40 +98,38 @@ module Datadog
98
98
  end
99
99
  private_class_method :decode
100
100
 
101
- # Encode an unsigned 64-bit integer using LEB128 variable-length encoding.
101
+ # Encode a signed 64-bit integer using ZigZag + LEB128 variable-length encoding.
102
102
  #
103
- # This implements unsigned LEB128 (Little Endian Base 128) encoding as specified
104
- # in DWARF5 standard section 7.6:
105
- # https://dwarfstd.org/doc/DWARF5.pdf#page=301
103
+ # dd-trace-go's DSM pathway encoder (internal/datastreams/propagator.go, via
104
+ # sketches-go's ddsketch/encoding package) ZigZag-maps the signed value before
105
+ # applying plain unsigned LEB128 (DWARF5 section 7.6:
106
+ # https://dwarfstd.org/doc/DWARF5.pdf#page=301). This implementation previously
107
+ # skipped the ZigZag step, which silently decoded to the wrong timestamp on any
108
+ # dd-trace-go consumer -- see https://github.com/DataDog/dd-trace-go/issues/5163.
106
109
  #
107
- # Each byte uses 7 bits for data and 1 bit to indicate continuation.
108
- # The high bit is set if more bytes follow, clear for the final byte.
110
+ # ZigZag maps signed values to unsigned ones so small-magnitude negative numbers
111
+ # still encode to few bytes: 0, -1, 1, -2, 2, ... -> 0, 1, 2, 3, 4, ...
109
112
  #
110
- # @param value [Integer] Unsigned integer value to encode
113
+ # @param value [Integer] Signed integer value to encode
111
114
  # @return [String] Binary string of encoded bytes
112
115
  def encode_var_int_64(value)
116
+ zigzag = (value << 1) ^ (value >> 63)
113
117
  bytes = []
114
- while value >= 0x80
115
- bytes << ((value & 0x7F) | 0x80)
116
- value >>= 7
118
+ while zigzag >= 0x80
119
+ bytes << ((zigzag & 0x7F) | 0x80)
120
+ zigzag >>= 7
117
121
  end
118
- bytes << value
122
+ bytes << zigzag
119
123
  bytes.pack("C*")
120
124
  end
121
125
 
122
- # Decode an unsigned LEB128 variable-length integer from IO stream.
126
+ # Decode a signed 64-bit integer encoded with ZigZag + LEB128 from an IO stream.
123
127
  #
124
- # This implements unsigned LEB128 (Little Endian Base 128) decoding as specified
125
- # in DWARF5 standard section 7.6:
126
- # https://dwarfstd.org/doc/DWARF5.pdf#page=301
127
- #
128
- # VarInt format: Each byte uses 7 bits for data, 1 bit for continuation
129
- # - High bit set = more bytes follow
130
- # - High bit clear = final byte
131
- # - Data bits accumulated in little-endian order
128
+ # See #encode_var_int_64 for why the ZigZag un-map is required to match
129
+ # dd-trace-go's wire format.
132
130
  #
133
131
  # @param io [StringIO] IO stream to read from
134
- # @return [Integer, nil] Decoded unsigned integer, or nil if malformed
132
+ # @return [Integer, nil] Decoded signed integer, or nil if malformed
135
133
  def self.decode_varint(io)
136
134
  value = 0
137
135
  shift = 0
@@ -143,7 +141,7 @@ module Datadog
143
141
  value |= (byte & 0x7F) << shift
144
142
 
145
143
  # If high bit is clear, we're done
146
- return value unless (byte & 0x80).nonzero?
144
+ return (value >> 1) ^ -(value & 1) unless (byte & 0x80).nonzero?
147
145
 
148
146
  shift += 7
149
147
 
@@ -62,10 +62,27 @@ module Datadog
62
62
 
63
63
  super()
64
64
  self.loop_base_interval = interval
65
+ # Without this, a preload-then-fork deployment model (e.g. Puma cluster mode with
66
+ # preload_app!, or Sidekiq Enterprise's sidekiqswarm with SIDEKIQ_PRELOAD_APP) leaves
67
+ # every forked child with a dead flush thread: Ruby threads don't survive fork, and the
68
+ # default FORK_POLICY_STOP means #perform would otherwise just mark the worker stopped
69
+ # instead of restarting it. See #restart_flush_thread.
70
+ self.fork_policy = Core::Workers::Async::Thread::FORK_POLICY_RESTART
65
71
 
66
72
  perform
67
73
  end
68
74
 
75
+ # Restarts the flush thread if a fork has been detected since this processor was created.
76
+ # Called by Components#after_fork. Safe to call even when no fork occurred: #perform (via
77
+ # Workers::Async::Thread) only restarts the worker when +forked?+ is true. Deliberately not
78
+ # named +after_fork+: Workers::Async::Thread already defines a protected +after_fork+
79
+ # template method that #perform's internal restart path (+restart_after_fork+) calls on
80
+ # every restart -- overriding it here would recurse.
81
+ def restart_flush_thread
82
+ discard_inherited_state_after_fork! if forked?
83
+ perform
84
+ end
85
+
69
86
  # Track Kafka produce offset for lag monitoring
70
87
  # @param topic [String] The Kafka topic name
71
88
  # @param partition [Integer] The partition number
@@ -302,6 +319,20 @@ module Datadog
302
319
  PathwayContext.decode_b64(encoded_ctx)
303
320
  end
304
321
 
322
+ # A fork copies whatever was buffered in the parent (unflushed events, aggregated
323
+ # buckets, consumer stats) into the child. If the parent process is still alive and
324
+ # flushing (e.g. a supervisor process that stays up after forking workers), letting
325
+ # the child flush that same inherited data as well would double-report it to the
326
+ # agent. Discard it instead: losing a few seconds of pre-fork stats is preferable to
327
+ # corrupting pipeline throughput/lag numbers with duplicates.
328
+ def discard_inherited_state_after_fork!
329
+ @event_buffer.pop
330
+ @stats_mutex.synchronize do
331
+ @buckets.clear
332
+ @consumer_stats.clear
333
+ end
334
+ end
335
+
305
336
  def flush_stats
306
337
  stats_buckets = @stats_mutex.synchronize do
307
338
  return if @buckets.empty? && @consumer_stats.empty?
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../core/rate_limiter"
3
4
  require_relative "../core/utils/time"
4
5
  require_relative "../ruby_version"
5
6
  require_relative "fatal_exceptions"
@@ -69,12 +70,22 @@ module Datadog
69
70
  #
70
71
  # @api private
71
72
  class Instrumenter
73
+ # Maximum number of capturing-probe snapshots admitted per second across the
74
+ # whole process.
75
+ GLOBAL_SNAPSHOT_RATE_LIMIT = 20
76
+
77
+ # Maximum number of non-capturing probe emissions admitted per second across
78
+ # the whole process.
79
+ GLOBAL_LOG_RATE_LIMIT = 5000
80
+
72
81
  def initialize(settings, serializer, logger, code_tracker: nil, telemetry: nil)
73
82
  @settings = settings
74
83
  @serializer = serializer
75
84
  @logger = logger
76
85
  @telemetry = telemetry
77
86
  @code_tracker = code_tracker
87
+ @global_snapshot_rate_limiter = Datadog::Core::TokenBucket.new(GLOBAL_SNAPSHOT_RATE_LIMIT)
88
+ @global_log_rate_limiter = Datadog::Core::TokenBucket.new(GLOBAL_LOG_RATE_LIMIT)
78
89
 
79
90
  @lock = Mutex.new
80
91
  end
@@ -91,12 +102,31 @@ module Datadog
91
102
  # Component#start! assigns the now-current tracker here.
92
103
  attr_writer :code_tracker
93
104
 
105
+ # Process-wide snapshot rate limiter.
106
+ # @return [Datadog::Core::TokenBucket]
107
+ attr_reader :global_snapshot_rate_limiter
108
+ # Process-wide log rate limiter.
109
+ # @return [Datadog::Core::TokenBucket]
110
+ attr_reader :global_log_rate_limiter
111
+
94
112
  def capture_expression_evaluator
95
113
  @capture_expression_evaluator ||= CaptureExpressionEvaluator.new(
96
114
  settings: settings, serializer: serializer, logger: logger, telemetry: telemetry,
97
115
  )
98
116
  end
99
117
 
118
+ # Returns the process-wide rate limiter for the probe.
119
+ #
120
+ # @param probe [Probe] the probe whose invocation is being rate limited
121
+ # @return [Datadog::Core::TokenBucket] the shared limiter for the probe's category
122
+ def probe_global_rate_limiter(probe)
123
+ if probe.capturing?
124
+ global_snapshot_rate_limiter
125
+ else
126
+ global_log_rate_limiter
127
+ end
128
+ end
129
+
100
130
  # This is a substitute for Thread::Backtrace::Location
101
131
  # which does not have a public constructor.
102
132
  # Used for the fabricated stack frame for the method itself
@@ -529,7 +559,12 @@ module Datadog
529
559
  end
530
560
 
531
561
  rate_limiter = probe.rate_limiter
532
- if continue and rate_limiter.nil? || rate_limiter.allow?
562
+ admitted = continue && (rate_limiter.nil? || rate_limiter.allow?)
563
+ if admitted && !probe_global_rate_limiter(probe).allow?
564
+ admitted = false
565
+ logger.trace { "di: #{probe.type} probe #{probe.id}: skipping due to global rate limit" }
566
+ end
567
+ if admitted
533
568
  # Arguments may be mutated by the method, therefore
534
569
  # they need to be serialized prior to method invocation.
535
570
  serialized_entry_args = if probe.capture_snapshot?
@@ -794,6 +829,11 @@ module Datadog
794
829
  # and check that it is in fact set.
795
830
  return if probe.rate_limiter && !probe.rate_limiter.allow?
796
831
 
832
+ unless probe_global_rate_limiter(probe).allow?
833
+ logger.trace { "di: #{probe.type} probe #{probe.id}: skipping due to global rate limit" }
834
+ return
835
+ end
836
+
797
837
  # The context creation is relatively expensive and we don't
798
838
  # want to run it if the callback won't be executed due to the
799
839
  # rate limit.
@@ -20,9 +20,9 @@ module Datadog
20
20
 
21
21
  def_delegators :target, :debug
22
22
 
23
- def trace(&block)
23
+ def trace
24
24
  if settings.dynamic_instrumentation.internal.trace_logging
25
- debug(&block)
25
+ debug { yield }
26
26
  end
27
27
  end
28
28
  end
@@ -97,7 +97,7 @@ module Datadog
97
97
  @evaluate_at = evaluate_at
98
98
  @condition = condition
99
99
 
100
- @rate_limit = rate_limit || ((@capture_snapshot || !@capture_expressions.empty?) ? 1 : 5000)
100
+ @rate_limit = rate_limit || (capturing? ? 1 : 5000)
101
101
  @rate_limiter = Datadog::Core::TokenBucket.new(@rate_limit)
102
102
 
103
103
  # At most one report per second.
@@ -166,6 +166,14 @@ module Datadog
166
166
  !@capture_expressions.empty?
167
167
  end
168
168
 
169
+ # Whether this probe captures user data, either as a full snapshot
170
+ # or via capture expressions.
171
+ #
172
+ # @return [Boolean]
173
+ def capturing?
174
+ capture_snapshot? || capture_expressions?
175
+ end
176
+
169
177
  def evaluate_at_entry?
170
178
  evaluate_at == :entry
171
179
  end
@@ -379,6 +379,7 @@ module Datadog
379
379
  thread_id: nil,
380
380
  version: 2,
381
381
  },
382
+ runtime_id: Core::Environment::Identity.id,
382
383
  # TODO add tests that the trace/span id is correctly propagated
383
384
  "dd.trace_id": active_trace&.id&.to_s,
384
385
  "dd.span_id": active_span&.id&.to_s,
@@ -22,7 +22,7 @@ module Datadog
22
22
  # when DI is explicitly disabled, or the runtime cannot run DI
23
23
  # (JRuby, Ruby 2.5), that block — including this bit — is skipped.
24
24
  # The enable signal itself is delivered in APM_TRACING payloads and
25
- # routed here by Tracing::Remote.process_config.
25
+ # routed here by Tracing::Remote.merge_and_apply_configs.
26
26
  CAPABILITIES = [
27
27
  1 << 38, # APM_TRACING_ENABLE_DYNAMIC_INSTRUMENTATION: Implicit DI enablement
28
28
  ].freeze
@@ -37,7 +37,7 @@ module Datadog
37
37
 
38
38
  # Entry point for the RC-driven DI enable/disable path.
39
39
  #
40
- # Invoked from {Datadog::Tracing::Remote.process_config} when an
40
+ # Invoked from {Datadog::Tracing::Remote.merge_and_apply_configs} when an
41
41
  # APM_TRACING payload carries `dynamic_instrumentation_enabled`. Runs
42
42
  # on the remote-config thread; never raises.
43
43
  #
@@ -45,7 +45,7 @@ module Datadog
45
45
  # false to stop it. The `DD_DYNAMIC_INSTRUMENTATION_ENABLED=false`
46
46
  # env var blocks an enable here (see {.explicitly_disabled?}).
47
47
  # @param repository [Datadog::Core::Remote::Configuration::Repository, nil]
48
- # the RC repository, passed by {Datadog::Tracing::Remote.process_config}
48
+ # the RC repository, passed by {Datadog::Tracing::Remote.apply_lib_config}
49
49
  # so that a stopped->started transition can reconcile against probes that
50
50
  # were delivered in an earlier poll. nil when called outside the RC
51
51
  # dispatch path (e.g. unit tests), in which case no reconcile happens.