datadog 2.26.0 → 2.27.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -1
  3. data/ext/datadog_profiling_native_extension/clock_id_from_pthread.c +2 -1
  4. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +7 -6
  5. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +2 -2
  6. data/ext/datadog_profiling_native_extension/collectors_gc_profiling_helper.c +3 -2
  7. data/ext/datadog_profiling_native_extension/collectors_stack.c +6 -5
  8. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +16 -12
  9. data/ext/datadog_profiling_native_extension/crashtracking_runtime_stacks.c +2 -2
  10. data/ext/datadog_profiling_native_extension/datadog_ruby_common.c +48 -1
  11. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +41 -0
  12. data/ext/datadog_profiling_native_extension/encoded_profile.c +2 -1
  13. data/ext/datadog_profiling_native_extension/heap_recorder.c +24 -24
  14. data/ext/datadog_profiling_native_extension/http_transport.c +10 -5
  15. data/ext/datadog_profiling_native_extension/libdatadog_helpers.c +3 -22
  16. data/ext/datadog_profiling_native_extension/libdatadog_helpers.h +0 -5
  17. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +9 -8
  18. data/ext/datadog_profiling_native_extension/profiling.c +20 -15
  19. data/ext/datadog_profiling_native_extension/ruby_helpers.c +55 -44
  20. data/ext/datadog_profiling_native_extension/ruby_helpers.h +17 -5
  21. data/ext/datadog_profiling_native_extension/setup_signal_handler.c +8 -2
  22. data/ext/datadog_profiling_native_extension/setup_signal_handler.h +3 -0
  23. data/ext/datadog_profiling_native_extension/stack_recorder.c +16 -16
  24. data/ext/datadog_profiling_native_extension/unsafe_api_calls_check.c +2 -1
  25. data/ext/datadog_profiling_native_extension/unsafe_api_calls_check.h +5 -2
  26. data/ext/libdatadog_api/crashtracker.c +5 -8
  27. data/ext/libdatadog_api/datadog_ruby_common.c +48 -1
  28. data/ext/libdatadog_api/datadog_ruby_common.h +41 -0
  29. data/ext/libdatadog_api/ddsketch.c +4 -8
  30. data/ext/libdatadog_api/feature_flags.c +5 -5
  31. data/ext/libdatadog_api/helpers.h +27 -0
  32. data/ext/libdatadog_api/init.c +4 -0
  33. data/lib/datadog/appsec/api_security/endpoint_collection/rails_collector.rb +8 -1
  34. data/lib/datadog/appsec/api_security/endpoint_collection/rails_route_serializer.rb +9 -2
  35. data/lib/datadog/appsec/component.rb +1 -1
  36. data/lib/datadog/appsec/context.rb +3 -3
  37. data/lib/datadog/appsec/contrib/excon/integration.rb +1 -1
  38. data/lib/datadog/appsec/contrib/excon/patcher.rb +1 -1
  39. data/lib/datadog/appsec/contrib/excon/ssrf_detection_middleware.rb +47 -12
  40. data/lib/datadog/appsec/contrib/faraday/ssrf_detection_middleware.rb +32 -15
  41. data/lib/datadog/appsec/contrib/rest_client/integration.rb +1 -1
  42. data/lib/datadog/appsec/contrib/rest_client/patcher.rb +1 -1
  43. data/lib/datadog/appsec/contrib/rest_client/request_ssrf_detection_patch.rb +50 -14
  44. data/lib/datadog/appsec/ext.rb +2 -0
  45. data/lib/datadog/appsec/metrics/collector.rb +8 -3
  46. data/lib/datadog/appsec/metrics/exporter.rb +7 -0
  47. data/lib/datadog/appsec/metrics/telemetry.rb +7 -2
  48. data/lib/datadog/appsec/metrics.rb +5 -5
  49. data/lib/datadog/appsec/remote.rb +4 -4
  50. data/lib/datadog/appsec.rb +7 -1
  51. data/lib/datadog/core/configuration/settings.rb +17 -0
  52. data/lib/datadog/core/configuration/supported_configurations.rb +1 -0
  53. data/lib/datadog/core/telemetry/logger.rb +2 -0
  54. data/lib/datadog/core/telemetry/logging.rb +20 -2
  55. data/lib/datadog/profiling/component.rb +13 -0
  56. data/lib/datadog/profiling/exporter.rb +4 -0
  57. data/lib/datadog/profiling/ext/exec_monkey_patch.rb +32 -0
  58. data/lib/datadog/profiling/flush.rb +3 -0
  59. data/lib/datadog/profiling/profiler.rb +3 -5
  60. data/lib/datadog/profiling/scheduler.rb +8 -7
  61. data/lib/datadog/profiling/tag_builder.rb +1 -0
  62. data/lib/datadog/version.rb +1 -1
  63. metadata +6 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cbf89a9582401ec9fcab36ed1c165ccd447e286085668c768c4df83b5dd27d81
4
- data.tar.gz: 0c915a22c6ba56f7d39cd03a278466100e72769ac013bb26e9bd2204b4f280ee
3
+ metadata.gz: fec5b056193bb744192c4df3a9843e630d0675a53d88162ba51d5f250ca7a31d
4
+ data.tar.gz: bb542f2f2c8164afc1426c103bd49113b10a5282be3ee13872fc13d138fa6183
5
5
  SHA512:
6
- metadata.gz: 1d2fefe85cab7cff7273196e57638a62da64b6ab0f84b0a350266b0adbc64ae57249d1ab6beb74bfe0d42a233f5b36855a4210ffeb69573fa4cafa2decc12820
7
- data.tar.gz: 6b41c594384690eed2a84f1e8046046a72bb29c276e87d6cf048b34df2d24cf54725526f0d6eebed0a523f4334385a1f796cba8a6c1112e46e5d58787410611d
6
+ metadata.gz: cd063e5e9e617d771603c6eae2305b1c5677587b3ae75690e1e96d273957fc20dc911bdd03264982ded4353c263fd06423f990a00135be3ed310cfd9a1a9c259
7
+ data.tar.gz: dbeea8e08bdd6e1f2fb592532410008980a44a10d349437da297fae0f49f21aa93ec72f0af909e5844d5bf5ddf3fa7be36762627505508bf509d78f57c81220e
data/CHANGELOG.md CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [2.27.0] - 2026-01-21
6
+
7
+ ### Added
8
+
9
+ * AppSec: Add analysis of the downstream requests ([#5206][])
10
+ * Telemetry: Add static error reporting for native extensions ([#5076][])
11
+
12
+ ### Changed
13
+
14
+ * SSI: Update injector to v1.2.1 ([#5254][])
15
+ * SSI: Prepare for expanded platform support ([#5254][])
16
+ * SSI: Improve remote resolution with expanded platform support via fallback to local gems ([#5254][])
17
+ * SSI: Introduce experimental fully local resolution support ([#5254][])
18
+ * Profiling: Telemetry-safe error reporting for native extensions ([#5076][])
19
+
20
+
21
+ ### Fixed
22
+
23
+ * Profiler: Fix interrupting new processes with the message `Profiling timer expired` during `exec` ([#5246][])
24
+ * Profiler: Fix rare race in profiler causing flaky spec on Ruby 2.7 ([#5247][])
25
+ * Appsec: Fix reporting of multi-method routes for Endpoint Collection ([#5240][])
26
+ * AppSec: Fix reporting of Rails routes that accept multiple request methods. ([#5240][])
27
+
5
28
  ## [2.26.0] - 2026-01-16
6
29
 
7
30
  ### Added
@@ -3449,7 +3472,8 @@ Release notes: https://github.com/DataDog/dd-trace-rb/releases/tag/v0.3.1
3449
3472
  Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
3450
3473
 
3451
3474
 
3452
- [Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v2.26.0...master
3475
+ [Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v2.27.0...master
3476
+ [2.27.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.26.0...v2.27.0
3453
3477
  [2.26.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.25.0...v2.26.0
3454
3478
  [2.25.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.24.0...v2.25.0
3455
3479
  [2.24.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.23.0...v2.24.0
@@ -5096,6 +5120,7 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
5096
5120
  [#5054]: https://github.com/DataDog/dd-trace-rb/issues/5054
5097
5121
  [#5058]: https://github.com/DataDog/dd-trace-rb/issues/5058
5098
5122
  [#5073]: https://github.com/DataDog/dd-trace-rb/issues/5073
5123
+ [#5076]: https://github.com/DataDog/dd-trace-rb/issues/5076
5099
5124
  [#5086]: https://github.com/DataDog/dd-trace-rb/issues/5086
5100
5125
  [#5091]: https://github.com/DataDog/dd-trace-rb/issues/5091
5101
5126
  [#5122]: https://github.com/DataDog/dd-trace-rb/issues/5122
@@ -5113,11 +5138,16 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
5113
5138
  [#5176]: https://github.com/DataDog/dd-trace-rb/issues/5176
5114
5139
  [#5194]: https://github.com/DataDog/dd-trace-rb/issues/5194
5115
5140
  [#5197]: https://github.com/DataDog/dd-trace-rb/issues/5197
5141
+ [#5206]: https://github.com/DataDog/dd-trace-rb/issues/5206
5116
5142
  [#5210]: https://github.com/DataDog/dd-trace-rb/issues/5210
5117
5143
  [#5215]: https://github.com/DataDog/dd-trace-rb/issues/5215
5118
5144
  [#5222]: https://github.com/DataDog/dd-trace-rb/issues/5222
5119
5145
  [#5237]: https://github.com/DataDog/dd-trace-rb/issues/5237
5120
5146
  [#5238]: https://github.com/DataDog/dd-trace-rb/issues/5238
5147
+ [#5240]: https://github.com/DataDog/dd-trace-rb/issues/5240
5148
+ [#5246]: https://github.com/DataDog/dd-trace-rb/issues/5246
5149
+ [#5247]: https://github.com/DataDog/dd-trace-rb/issues/5247
5150
+ [#5254]: https://github.com/DataDog/dd-trace-rb/issues/5254
5121
5151
  [@AdrianLC]: https://github.com/AdrianLC
5122
5152
  [@Azure7111]: https://github.com/Azure7111
5123
5153
  [@BabyGroot]: https://github.com/BabyGroot
@@ -11,6 +11,7 @@
11
11
  #include "clock_id.h"
12
12
  #include "helpers.h"
13
13
  #include "private_vm_api_access.h"
14
+ #include "ruby_helpers.h"
14
15
  #include "time_helpers.h"
15
16
 
16
17
  // Validate that our home-cooked pthread_id_for() matches pthread_self() for the current thread
@@ -18,7 +19,7 @@ void self_test_clock_id(void) {
18
19
  rb_nativethread_id_t expected_pthread_id = pthread_self();
19
20
  rb_nativethread_id_t actual_pthread_id = pthread_id_for(rb_thread_current());
20
21
 
21
- if (expected_pthread_id != actual_pthread_id) rb_raise(rb_eRuntimeError, "pthread_id_for() self-test failed");
22
+ if (expected_pthread_id != actual_pthread_id) raise_error(rb_eRuntimeError, "pthread_id_for() self-test failed");
22
23
  }
23
24
 
24
25
  // Safety: This function is assumed never to raise exceptions by callers
@@ -299,7 +299,7 @@ void collectors_cpu_and_wall_time_worker_init(VALUE profiling_module) {
299
299
  after_gc_from_postponed_job_handle == POSTPONED_JOB_HANDLE_INVALID ||
300
300
  after_gvl_running_from_postponed_job_handle == POSTPONED_JOB_HANDLE_INVALID
301
301
  ) {
302
- rb_raise(rb_eRuntimeError, "Failed to register profiler postponed jobs (got POSTPONED_JOB_HANDLE_INVALID)");
302
+ raise_error(rb_eRuntimeError, "Failed to register profiler postponed jobs (got POSTPONED_JOB_HANDLE_INVALID)");
303
303
  }
304
304
  #else
305
305
  gc_finalize_deferred_workaround = objspace_ptr_for_gc_finalize_deferred_workaround();
@@ -486,10 +486,7 @@ static VALUE _native_sampling_loop(DDTRACE_UNUSED VALUE _self, VALUE instance) {
486
486
  cpu_and_wall_time_worker_state *old_state = active_sampler_instance_state;
487
487
  if (old_state != NULL) {
488
488
  if (is_thread_alive(old_state->owner_thread)) {
489
- rb_raise(
490
- rb_eRuntimeError,
491
- "Could not start CpuAndWallTimeWorker: There's already another instance of CpuAndWallTimeWorker active in a different thread"
492
- );
489
+ raise_error(rb_eRuntimeError, "Could not start CpuAndWallTimeWorker: There's already another instance of CpuAndWallTimeWorker active in a different thread");
493
490
  } else {
494
491
  // The previously active thread seems to have died without cleaning up after itself.
495
492
  // In this case, we can still go ahead and start the profiler BUT we make sure to disable any existing tracepoint
@@ -685,6 +682,10 @@ static void *run_sampling_trigger_loop(void *state_ptr) {
685
682
  // we're doing this, so we may still not signal the correct thread from time to time, but our signal handler
686
683
  // includes a check to see if it got called in the right thread
687
684
  state->stats.interrupt_thread_attempts++;
685
+
686
+ // Pick up any last-minute attempts to stop before we send the signal
687
+ if (!atomic_load(&state->should_run)) return NULL;
688
+
688
689
  pthread_kill(owner.owner, SIGPROF);
689
690
  } else {
690
691
  if (state->skip_idle_samples_for_testing) {
@@ -837,7 +838,7 @@ static VALUE release_gvl_and_run_sampling_trigger_loop(VALUE instance) {
837
838
  NULL
838
839
  );
839
840
  #else
840
- rb_raise(rb_eArgError, "GVL profiling is not supported in this Ruby version");
841
+ raise_error(rb_eArgError, "GVL profiling is not supported in this Ruby version");
841
842
  #endif
842
843
  }
843
844
 
@@ -51,7 +51,7 @@ void discrete_dynamic_sampler_reset(discrete_dynamic_sampler *sampler, long now_
51
51
 
52
52
  void discrete_dynamic_sampler_set_overhead_target_percentage(discrete_dynamic_sampler *sampler, double target_overhead, long now_ns) {
53
53
  if (target_overhead <= 0 || target_overhead > 100) {
54
- rb_raise(rb_eArgError, "Target overhead must be a double between ]0,100] was %f", target_overhead);
54
+ raise_error(rb_eArgError, "Target overhead must be a double between ]0,100] was %f", target_overhead);
55
55
  }
56
56
  sampler->target_overhead = target_overhead;
57
57
  return discrete_dynamic_sampler_reset(sampler, now_ns);
@@ -369,7 +369,7 @@ static VALUE _native_new(VALUE klass) {
369
369
 
370
370
  long now_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE);
371
371
  if (now_ns == 0) {
372
- rb_raise(rb_eRuntimeError, "failed to get clock time");
372
+ raise_error(rb_eRuntimeError, "failed to get clock time");
373
373
  }
374
374
  discrete_dynamic_sampler_init(&state->sampler, "test sampler", now_ns);
375
375
 
@@ -2,6 +2,7 @@
2
2
  #include <datadog/profiling.h>
3
3
 
4
4
  #include "collectors_gc_profiling_helper.h"
5
+ #include "ruby_helpers.h"
5
6
 
6
7
  // This helper is used by the Datadog::Profiling::Collectors::ThreadContext to profile garbage collection.
7
8
  // It's tested through that class' interfaces.
@@ -71,7 +72,7 @@ uint8_t gc_profiling_set_metadata(ddog_prof_Label *labels, int labels_length) {
71
72
  1; // gc type
72
73
 
73
74
  if (max_label_count > labels_length) {
74
- rb_raise(rb_eArgError, "BUG: gc_profiling_set_metadata invalid labels_length (%d) < max_label_count (%d)", labels_length, max_label_count);
75
+ raise_error(rb_eArgError, "BUG: gc_profiling_set_metadata invalid labels_length (%d) < max_label_count (%d)", labels_length, max_label_count);
75
76
  }
76
77
 
77
78
  uint8_t label_pos = 0;
@@ -119,7 +120,7 @@ uint8_t gc_profiling_set_metadata(ddog_prof_Label *labels, int labels_length) {
119
120
  };
120
121
 
121
122
  if (label_pos > max_label_count) {
122
- rb_raise(rb_eRuntimeError, "BUG: gc_profiling_set_metadata unexpected label_pos (%d) > max_label_count (%d)", label_pos, max_label_count);
123
+ raise_error(rb_eRuntimeError, "BUG: gc_profiling_set_metadata unexpected label_pos (%d) > max_label_count (%d)", label_pos, max_label_count);
123
124
  }
124
125
 
125
126
  return label_pos;
@@ -17,6 +17,7 @@
17
17
 
18
18
  #include "datadog_ruby_common.h"
19
19
  #include "private_vm_api_access.h"
20
+ #include "ruby_helpers.h"
20
21
  #include "stack_recorder.h"
21
22
  #include "collectors_stack.h"
22
23
 
@@ -284,11 +285,11 @@ void sample_thread(
284
285
  // here, but >= 0 makes this easier to understand/debug.
285
286
  bool only_wall_time = cpu_or_wall_sample && values.cpu_time_ns == 0 && values.wall_time_ns >= 0;
286
287
 
287
- if (cpu_or_wall_sample && state_label == NULL) rb_raise(rb_eRuntimeError, "BUG: Unexpected missing state_label");
288
+ if (cpu_or_wall_sample && state_label == NULL) raise_error(rb_eRuntimeError, "BUG: Unexpected missing state_label");
288
289
 
289
290
  if (has_cpu_time) {
290
291
  state_label->str = DDOG_CHARSLICE_C("had cpu");
291
- if (labels.is_gvl_waiting_state) rb_raise(rb_eRuntimeError, "BUG: Unexpected combination of cpu-time with is_gvl_waiting");
292
+ if (labels.is_gvl_waiting_state) raise_error(rb_eRuntimeError, "BUG: Unexpected combination of cpu-time with is_gvl_waiting");
292
293
  }
293
294
 
294
295
  int top_of_stack_position = captured_frames - 1;
@@ -612,8 +613,8 @@ bool prepare_sample_thread(VALUE thread, sampling_buffer *buffer) {
612
613
  }
613
614
 
614
615
  uint16_t sampling_buffer_check_max_frames(int max_frames) {
615
- if (max_frames < 5) rb_raise(rb_eArgError, "Invalid max_frames: value must be >= 5");
616
- if (max_frames > MAX_FRAMES_LIMIT) rb_raise(rb_eArgError, "Invalid max_frames: value must be <= " MAX_FRAMES_LIMIT_AS_STRING);
616
+ if (max_frames < 5) raise_error(rb_eArgError, "Invalid max_frames: value must be >= 5");
617
+ if (max_frames > MAX_FRAMES_LIMIT) raise_error(rb_eArgError, "Invalid max_frames: value must be <= " MAX_FRAMES_LIMIT_AS_STRING);
617
618
  return max_frames;
618
619
  }
619
620
 
@@ -630,7 +631,7 @@ void sampling_buffer_initialize(sampling_buffer *buffer, uint16_t max_frames, dd
630
631
 
631
632
  void sampling_buffer_free(sampling_buffer *buffer) {
632
633
  if (buffer->max_frames == 0 || buffer->locations == NULL || buffer->stack_buffer == NULL) {
633
- rb_raise(rb_eArgError, "sampling_buffer_free called with invalid buffer");
634
+ raise_error(rb_eArgError, "sampling_buffer_free called with invalid buffer");
634
635
  }
635
636
 
636
637
  ruby_xfree(buffer->stack_buffer);
@@ -8,6 +8,7 @@
8
8
  #include "helpers.h"
9
9
  #include "libdatadog_helpers.h"
10
10
  #include "private_vm_api_access.h"
11
+ #include "ruby_helpers.h"
11
12
  #include "stack_recorder.h"
12
13
  #include "time_helpers.h"
13
14
  #include "unsafe_api_calls_check.h"
@@ -292,7 +293,7 @@ static bool handle_gvl_waiting(
292
293
  static VALUE _native_on_gvl_waiting(DDTRACE_UNUSED VALUE self, VALUE thread);
293
294
  static VALUE _native_gvl_waiting_at_for(DDTRACE_UNUSED VALUE self, VALUE thread);
294
295
  static VALUE _native_on_gvl_running(DDTRACE_UNUSED VALUE self, VALUE thread);
295
- static VALUE _native_sample_after_gvl_running(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread);
296
+ static VALUE _native_sample_after_gvl_running(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread, VALUE allow_exception);
296
297
  static VALUE _native_apply_delta_to_cpu_time_at_previous_sample_ns(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread, VALUE delta_ns);
297
298
  static void otel_without_ddtrace_trace_identifiers_for(
298
299
  thread_context_collector_state *state,
@@ -342,7 +343,7 @@ void collectors_thread_context_init(VALUE profiling_module) {
342
343
  rb_define_singleton_method(testing_module, "_native_on_gvl_waiting", _native_on_gvl_waiting, 1);
343
344
  rb_define_singleton_method(testing_module, "_native_gvl_waiting_at_for", _native_gvl_waiting_at_for, 1);
344
345
  rb_define_singleton_method(testing_module, "_native_on_gvl_running", _native_on_gvl_running, 1);
345
- rb_define_singleton_method(testing_module, "_native_sample_after_gvl_running", _native_sample_after_gvl_running, 2);
346
+ rb_define_singleton_method(testing_module, "_native_sample_after_gvl_running", _native_sample_after_gvl_running, 3);
346
347
  rb_define_singleton_method(testing_module, "_native_apply_delta_to_cpu_time_at_previous_sample_ns", _native_apply_delta_to_cpu_time_at_previous_sample_ns, 3);
347
348
  #endif
348
349
 
@@ -518,7 +519,7 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
518
519
  } else if (otel_context_enabled == ID2SYM(rb_intern("both"))) {
519
520
  state->otel_context_enabled = OTEL_CONTEXT_ENABLED_BOTH;
520
521
  } else {
521
- rb_raise(rb_eArgError, "Unexpected value for otel_context_enabled: %+" PRIsVALUE, otel_context_enabled);
522
+ raise_error(rb_eArgError, "Unexpected value for otel_context_enabled: %+" PRIsVALUE, otel_context_enabled);
522
523
  }
523
524
 
524
525
  global_waiting_for_gvl_threshold_ns = NUM2UINT(waiting_for_gvl_threshold_ns);
@@ -539,7 +540,7 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
539
540
  static VALUE _native_sample(DDTRACE_UNUSED VALUE _self, VALUE collector_instance, VALUE profiler_overhead_stack_thread, VALUE allow_exception) {
540
541
  ENFORCE_BOOLEAN(allow_exception);
541
542
 
542
- if (!is_thread_alive(profiler_overhead_stack_thread)) rb_raise(rb_eArgError, "Unexpected: profiler_overhead_stack_thread is not alive");
543
+ if (!is_thread_alive(profiler_overhead_stack_thread)) raise_error(rb_eArgError, "Unexpected: profiler_overhead_stack_thread is not alive");
543
544
 
544
545
  if (allow_exception == Qfalse) debug_enter_unsafe_context();
545
546
 
@@ -831,7 +832,7 @@ VALUE thread_context_collector_sample_after_gc(VALUE self_instance) {
831
832
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
832
833
 
833
834
  if (state->gc_tracking.wall_time_at_previous_gc_ns == INVALID_TIME) {
834
- rb_raise(rb_eRuntimeError, "BUG: Unexpected call to sample_after_gc without valid GC information available");
835
+ raise_error(rb_eRuntimeError, "BUG: Unexpected call to sample_after_gc without valid GC information available");
835
836
  }
836
837
 
837
838
  int max_labels_needed_for_gc = 7; // Magic number gets validated inside gc_profiling_set_metadata
@@ -998,7 +999,7 @@ static void trigger_sample_for_thread(
998
999
  // @ivoanjo: I wonder if C compilers are smart enough to statically prove this check never triggers unless someone
999
1000
  // changes the code erroneously and remove it entirely?
1000
1001
  if (label_pos > max_label_count) {
1001
- rb_raise(rb_eRuntimeError, "BUG: Unexpected label_pos (%d) > max_label_count (%d)", label_pos, max_label_count);
1002
+ raise_error(rb_eRuntimeError, "BUG: Unexpected label_pos (%d) > max_label_count (%d)", label_pos, max_label_count);
1002
1003
  }
1003
1004
 
1004
1005
  ddog_prof_Slice_Label slice_labels = {.ptr = labels, .len = label_pos};
@@ -1295,7 +1296,7 @@ static long update_time_since_previous_sample(long *time_at_previous_sample_ns,
1295
1296
  elapsed_time_ns = 0;
1296
1297
  } else {
1297
1298
  // We don't expect non-wall time to go backwards, so let's flag this as a bug
1298
- rb_raise(rb_eRuntimeError, "BUG: Unexpected negative elapsed_time_ns between samples");
1299
+ raise_error(rb_eRuntimeError, "BUG: Unexpected negative elapsed_time_ns between samples");
1299
1300
  }
1300
1301
  }
1301
1302
 
@@ -1961,7 +1962,7 @@ static uint64_t otel_span_id_to_uint(VALUE otel_span_id) {
1961
1962
  thread_context_collector_state *state;
1962
1963
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
1963
1964
 
1964
- if (!state->timeline_enabled) rb_raise(rb_eRuntimeError, "GVL profiling requires timeline to be enabled");
1965
+ if (!state->timeline_enabled) raise_error(rb_eRuntimeError, "GVL profiling requires timeline to be enabled");
1965
1966
 
1966
1967
  intptr_t gvl_waiting_at = gvl_profiling_state_thread_object_get(current_thread);
1967
1968
 
@@ -2131,10 +2132,13 @@ static uint64_t otel_span_id_to_uint(VALUE otel_span_id) {
2131
2132
  return result;
2132
2133
  }
2133
2134
 
2134
- static VALUE _native_sample_after_gvl_running(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread) {
2135
+ static VALUE _native_sample_after_gvl_running(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread, VALUE allow_exception) {
2135
2136
  ENFORCE_THREAD(thread);
2137
+ ENFORCE_BOOLEAN(allow_exception);
2138
+
2136
2139
 
2137
- debug_enter_unsafe_context();
2140
+
2141
+ if (allow_exception == Qfalse) debug_enter_unsafe_context();
2138
2142
 
2139
2143
  VALUE result = thread_context_collector_sample_after_gvl_running(
2140
2144
  collector_instance,
@@ -2142,7 +2146,7 @@ static uint64_t otel_span_id_to_uint(VALUE otel_span_id) {
2142
2146
  monotonic_wall_time_now_ns(RAISE_ON_FAILURE)
2143
2147
  );
2144
2148
 
2145
- debug_leave_unsafe_context();
2149
+ if (allow_exception == Qfalse) debug_leave_unsafe_context();
2146
2150
 
2147
2151
  return result;
2148
2152
  }
@@ -2154,7 +2158,7 @@ static uint64_t otel_span_id_to_uint(VALUE otel_span_id) {
2154
2158
  TypedData_Get_Struct(collector_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
2155
2159
 
2156
2160
  per_thread_context *thread_context = get_context_for(thread, state);
2157
- if (thread_context == NULL) rb_raise(rb_eArgError, "Unexpected: This method cannot be used unless the per-thread context for the thread already exists");
2161
+ if (thread_context == NULL) raise_error(rb_eArgError, "Unexpected: This method cannot be used unless the per-thread context for the thread already exists");
2158
2162
 
2159
2163
  thread_context->cpu_time_at_previous_sample_ns += NUM2LONG(delta_ns);
2160
2164
 
@@ -217,12 +217,12 @@ void crashtracking_runtime_stacks_init(void) {
217
217
  if (crashtracker_thread_data_type == NULL) {
218
218
  VALUE current_thread = rb_thread_current();
219
219
  if (current_thread == Qnil) {
220
- rb_raise(rb_eRuntimeError, "crashtracking_runtime_stacks_init: rb_thread_current returned Qnil");
220
+ raise_error(rb_eRuntimeError, "crashtracking_runtime_stacks_init: rb_thread_current returned Qnil");
221
221
  }
222
222
 
223
223
  const rb_data_type_t *thread_data_type = RTYPEDDATA_TYPE(current_thread);
224
224
  if (!thread_data_type) {
225
- rb_raise(rb_eRuntimeError, "crashtracking_runtime_stacks_init: RTYPEDDATA_TYPE returned NULL");
225
+ raise_error(rb_eRuntimeError, "crashtracking_runtime_stacks_init: RTYPEDDATA_TYPE returned NULL");
226
226
  }
227
227
 
228
228
  crashtracker_thread_data_type = thread_data_type;
@@ -1,8 +1,11 @@
1
1
  #include "datadog_ruby_common.h"
2
+ #include <stdarg.h>
2
3
 
3
4
  // IMPORTANT: Currently this file is copy-pasted between extensions. Make sure to update all versions when doing any change!
4
5
 
5
- void raise_unexpected_type(VALUE value, const char *value_name, const char *type_name, const char *file, int line, const char* function_name) {
6
+ static ID telemetry_message_id;
7
+
8
+ void raise_unexpected_type(VALUE value, const char *value_name, const char *type_name, const char *file, int line, const char *function_name) {
6
9
  rb_exc_raise(
7
10
  rb_exc_new_str(
8
11
  rb_eTypeError,
@@ -18,6 +21,26 @@ void raise_unexpected_type(VALUE value, const char *value_name, const char *type
18
21
  );
19
22
  }
20
23
 
24
+ // Raises an exception with separate telemetry-safe and detailed messages.
25
+ // NOTE: Raising an exception always invokes Ruby code so it requires the GVL and is not compatible with "debug_enter_unsafe_context".
26
+ // @see debug_enter_unsafe_context
27
+ void private_raise_exception(VALUE exception, const char *static_message) {
28
+ rb_ivar_set(exception, telemetry_message_id, rb_str_new_cstr(static_message));
29
+ rb_exc_raise(exception);
30
+ }
31
+
32
+ // Helper for raising pre-formatted exceptions
33
+ void private_raise_error_formatted(VALUE exception_class, const char *detailed_message, const char *static_message) {
34
+ VALUE exception = rb_exc_new_cstr(exception_class, detailed_message);
35
+ private_raise_exception(exception, static_message);
36
+ }
37
+
38
+ // Use `raise_error` the macro instead, as it provides additional argument checks.
39
+ void private_raise_error(VALUE exception_class, const char *fmt, ...) {
40
+ FORMAT_VA_ERROR_MESSAGE(detailed_message, fmt);
41
+ private_raise_error_formatted(exception_class, detailed_message, fmt);
42
+ }
43
+
21
44
  VALUE datadog_gem_version(void) {
22
45
  VALUE ddtrace_module = rb_const_get(rb_cObject, rb_intern("Datadog"));
23
46
  ENFORCE_TYPE(ddtrace_module, T_MODULE);
@@ -78,3 +101,27 @@ ddog_Vec_Tag convert_tags(VALUE tags_as_array) {
78
101
 
79
102
  return tags;
80
103
  }
104
+
105
+ size_t read_ddogerr_string_and_drop(ddog_Error *error, char *string, size_t capacity) {
106
+ if (capacity == 0 || string == NULL) {
107
+ // short-circuit, we can't write anything
108
+ ddog_Error_drop(error);
109
+ return 0;
110
+ }
111
+
112
+ ddog_CharSlice error_msg_slice = ddog_Error_message(error);
113
+ size_t error_msg_size = error_msg_slice.len;
114
+ // Account for extra null char for proper cstring
115
+ if (error_msg_size >= capacity) {
116
+ // Error message too big, lets truncate it to capacity - 1 to allow for extra null at end
117
+ error_msg_size = capacity - 1;
118
+ }
119
+ strncpy(string, error_msg_slice.ptr, error_msg_size);
120
+ string[error_msg_size] = '\0';
121
+ ddog_Error_drop(error);
122
+ return error_msg_size;
123
+ }
124
+
125
+ void datadog_ruby_common_init(void) {
126
+ telemetry_message_id = rb_intern("@telemetry_message");
127
+ }
@@ -5,6 +5,9 @@
5
5
  #include <ruby.h>
6
6
  #include <datadog/common.h>
7
7
 
8
+ // Must be called once during initialization
9
+ void datadog_ruby_common_init(void);
10
+
8
11
  // Used to mark symbols to be exported to the outside of the extension.
9
12
  // Consider very carefully before tagging a function with this.
10
13
  #define DDTRACE_EXPORT __attribute__ ((visibility ("default")))
@@ -32,6 +35,39 @@
32
35
 
33
36
  NORETURN(void raise_unexpected_type(VALUE value, const char *value_name, const char *type_name, const char *file, int line, const char* function_name));
34
37
 
38
+ // Raises an exception of the specified class with the formatted string as its message.
39
+ // This macro ensures that the literal string is sent for telemetry, while the formatted
40
+ // message is the default `Exception#message`.
41
+ // *Ruby exceptions not raised through this function will not be reported via telemetry.*
42
+ #define raise_error(exception_class, fmt, ...) \
43
+ private_raise_error(exception_class, "" fmt, ##__VA_ARGS__)
44
+
45
+ NORETURN(
46
+ void private_raise_error(VALUE exception_class, const char *fmt, ...)
47
+ __attribute__ ((format (printf, 2, 3)));
48
+ );
49
+
50
+ // Internal helper for raising pre-formatted exceptions
51
+ NORETURN(
52
+ void private_raise_error_formatted(VALUE exception_class, const char *detailed_message, const char *static_message)
53
+ );
54
+
55
+ // Raises an exception with separate telemetry-safe and detailed messages.
56
+ // NOTE: Raising an exception always invokes Ruby code so it requires the GVL and is not compatible with "debug_enter_unsafe_context".
57
+ // @see debug_enter_unsafe_context
58
+ NORETURN(
59
+ void private_raise_exception(VALUE exception, const char *static_message)
60
+ );
61
+
62
+ #define MAX_RAISE_MESSAGE_SIZE 256
63
+
64
+ #define FORMAT_VA_ERROR_MESSAGE(buf, fmt) \
65
+ char buf[MAX_RAISE_MESSAGE_SIZE]; \
66
+ va_list buf##_args; \
67
+ va_start(buf##_args, fmt); \
68
+ vsnprintf(buf, MAX_RAISE_MESSAGE_SIZE, fmt, buf##_args); \
69
+ va_end(buf##_args);
70
+
35
71
  // Helper to retrieve Datadog::VERSION::STRING
36
72
  VALUE datadog_gem_version(void);
37
73
 
@@ -61,3 +97,8 @@ static inline VALUE get_error_details_and_drop(ddog_Error *error) {
61
97
  ddog_Error_drop(error);
62
98
  return result;
63
99
  }
100
+
101
+ // Utility function to be able to extract an error cstring from a ddog_Error.
102
+ // Returns the amount of characters written to string (which are necessarily
103
+ // bounded by capacity - 1 since the string will be null-terminated).
104
+ size_t read_ddogerr_string_and_drop(ddog_Error *error, char *string, size_t capacity);
@@ -1,6 +1,7 @@
1
1
  #include "encoded_profile.h"
2
2
  #include "datadog_ruby_common.h"
3
3
  #include "libdatadog_helpers.h"
4
+ #include "ruby_helpers.h"
4
5
 
5
6
  // This class exists to wrap a ddog_prof_EncodedProfile into a Ruby object
6
7
  // This file implements the native bits of the Datadog::Profiling::EncodedProfile class
@@ -41,7 +42,7 @@ VALUE from_ddog_prof_EncodedProfile(ddog_prof_EncodedProfile profile) {
41
42
  static ddog_ByteSlice get_bytes(ddog_prof_EncodedProfile *state) {
42
43
  ddog_prof_Result_ByteSlice raw_bytes = ddog_prof_EncodedProfile_bytes(state);
43
44
  if (raw_bytes.tag == DDOG_PROF_RESULT_BYTE_SLICE_ERR_BYTE_SLICE) {
44
- rb_raise(rb_eRuntimeError, "Failed to get bytes from profile: %"PRIsVALUE, get_error_details_and_drop(&raw_bytes.err));
45
+ raise_error(rb_eRuntimeError, "Failed to get bytes from profile: %"PRIsVALUE, get_error_details_and_drop(&raw_bytes.err));
45
46
  }
46
47
  return raw_bytes.ok;
47
48
  }