datadog 2.42.0 → 2.44.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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +67 -2
  3. data/README.md +3 -0
  4. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +46 -5
  5. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +6 -7
  6. data/ext/datadog_profiling_native_extension/collectors_stack.c +6 -7
  7. data/ext/datadog_profiling_native_extension/collectors_stack.h +1 -1
  8. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +80 -36
  9. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +3 -2
  10. data/ext/datadog_profiling_native_extension/extconf.rb +4 -2
  11. data/ext/datadog_profiling_native_extension/heap_recorder.c +13 -0
  12. data/ext/datadog_profiling_native_extension/heap_recorder.h +4 -0
  13. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +34 -2
  14. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +1 -0
  15. data/ext/datadog_profiling_native_extension/profiling.c +1 -0
  16. data/ext/datadog_profiling_native_extension/stack_recorder.c +17 -4
  17. data/ext/datadog_profiling_native_extension/time_helpers.h +5 -4
  18. data/ext/libdatadog_api/di.c +8 -3
  19. data/ext/libdatadog_api/extconf.rb +1 -1
  20. data/ext/libdatadog_api/feature_flags.c +8 -0
  21. data/ext/libdatadog_api/init.c +3 -1
  22. data/ext/libdatadog_api/otel_thread_context.c +51 -15
  23. data/ext/libdatadog_api/trace_exporter.c +642 -22
  24. data/ext/libdatadog_extconf_helpers.rb +14 -9
  25. data/lib/datadog/ai_guard/component.rb +17 -8
  26. data/lib/datadog/ai_guard/configuration/ext.rb +2 -0
  27. data/lib/datadog/ai_guard/configuration.rb +13 -0
  28. data/lib/datadog/ai_guard/contrib/ruby_llm/chat_instrumentation.rb +48 -55
  29. data/lib/datadog/ai_guard/contrib/ruby_llm/integration.rb +1 -1
  30. data/lib/datadog/ai_guard/contrib/ruby_llm/message_adapter.rb +76 -0
  31. data/lib/datadog/ai_guard/contrib/ruby_llm/patcher.rb +1 -0
  32. data/lib/datadog/ai_guard/contrib/ruby_llm/redaction_change_set.rb +135 -0
  33. data/lib/datadog/ai_guard/evaluation/client.rb +50 -0
  34. data/lib/datadog/ai_guard/evaluation/content_part.rb +12 -0
  35. data/lib/datadog/ai_guard/evaluation/message.rb +25 -7
  36. data/lib/datadog/ai_guard/evaluation/message_builder.rb +31 -0
  37. data/lib/datadog/ai_guard/evaluation/no_op_result.rb +6 -6
  38. data/lib/datadog/ai_guard/evaluation/outcome.rb +24 -0
  39. data/lib/datadog/ai_guard/evaluation/request.rb +4 -60
  40. data/lib/datadog/ai_guard/evaluation/response.rb +33 -0
  41. data/lib/datadog/ai_guard/evaluation/result.rb +11 -18
  42. data/lib/datadog/ai_guard/evaluation/tool_call.rb +15 -2
  43. data/lib/datadog/ai_guard/evaluation.rb +25 -11
  44. data/lib/datadog/ai_guard/ext.rb +1 -0
  45. data/lib/datadog/ai_guard/{api_client.rb → http_client.rb} +6 -4
  46. data/lib/datadog/ai_guard/metrics/telemetry.rb +43 -0
  47. data/lib/datadog/ai_guard/redaction/replacements.rb +97 -0
  48. data/lib/datadog/ai_guard/redaction/result.rb +29 -0
  49. data/lib/datadog/ai_guard/redaction.rb +80 -0
  50. data/lib/datadog/ai_guard.rb +58 -57
  51. data/lib/datadog/core/configuration/components.rb +9 -1
  52. data/lib/datadog/core/configuration/settings.rb +5 -2
  53. data/lib/datadog/core/configuration/supported_configurations.rb +8 -0
  54. data/lib/datadog/core/remote/client/capabilities.rb +10 -4
  55. data/lib/datadog/core/remote/component.rb +9 -2
  56. data/lib/datadog/core/remote/worker.rb +8 -0
  57. data/lib/datadog/di/instrumenter.rb +1 -1
  58. data/lib/datadog/di/probe_manager.rb +10 -5
  59. data/lib/datadog/di/proc_responder.rb +1 -1
  60. data/lib/datadog/open_feature/agentless/configuration_source.rb +246 -0
  61. data/lib/datadog/open_feature/agentless/parser.rb +43 -0
  62. data/lib/datadog/open_feature/agentless/response.rb +19 -0
  63. data/lib/datadog/open_feature/agentless/transport.rb +103 -0
  64. data/lib/datadog/open_feature/component.rb +57 -1
  65. data/lib/datadog/open_feature/configuration/agentless_endpoint.rb +90 -0
  66. data/lib/datadog/open_feature/configuration/source/resolution.rb +22 -0
  67. data/lib/datadog/open_feature/configuration/source.rb +43 -0
  68. data/lib/datadog/open_feature/configuration.rb +118 -0
  69. data/lib/datadog/open_feature/native_evaluator.rb +1 -14
  70. data/lib/datadog/open_feature/remote.rb +5 -5
  71. data/lib/datadog/open_feature/transport.rb +5 -0
  72. data/lib/datadog/opentelemetry/signal_configuration.rb +13 -9
  73. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +2 -2
  74. data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +5 -7
  75. data/lib/datadog/profiling/collectors/info.rb +2 -6
  76. data/lib/datadog/symbol_database/extractor.rb +84 -102
  77. data/lib/datadog/tracing/component.rb +2 -0
  78. data/lib/datadog/tracing/configuration/ext.rb +1 -0
  79. data/lib/datadog/tracing/configuration/settings.rb +6 -0
  80. data/lib/datadog/tracing/context.rb +24 -2
  81. data/lib/datadog/tracing/context_provider.rb +9 -5
  82. data/lib/datadog/tracing/contrib/grape/endpoint.rb +32 -5
  83. data/lib/datadog/tracing/contrib/sequel/jdbc_connection_string.rb +140 -0
  84. data/lib/datadog/tracing/contrib/sequel/utils.rb +49 -62
  85. data/lib/datadog/tracing/metadata/ext.rb +3 -0
  86. data/lib/datadog/tracing/otel_thread_context.rb +78 -9
  87. data/lib/datadog/tracing/tracer.rb +15 -2
  88. data/lib/datadog/tracing/transport/native.rb +18 -26
  89. data/lib/datadog/tracing/transport/span_events_negotiation.rb +41 -0
  90. data/lib/datadog/tracing/transport/trace_formatter.rb +13 -3
  91. data/lib/datadog/tracing/transport/traces.rb +3 -23
  92. data/lib/datadog/version.rb +1 -1
  93. metadata +25 -7
  94. data/lib/datadog/ai_guard/evaluation/content_builder.rb +0 -31
@@ -207,6 +207,7 @@ typedef struct {
207
207
  // however this only matters for a very short period when a thread starts.
208
208
  struct per_thread_context {
209
209
  sampling_buffer sampling_buffer;
210
+ bool prepare_sample_on_gc_finish;
210
211
  char thread_id[THREAD_ID_LIMIT_CHARS];
211
212
  ddog_CharSlice thread_id_char_slice;
212
213
  char thread_invoke_location[THREAD_INVOKE_LOCATION_LIMIT_CHARS];
@@ -311,7 +312,6 @@ static void update_metrics_and_sample(
311
312
  thread_context_collector_state *state,
312
313
  VALUE thread_being_sampled,
313
314
  per_thread_context *thread_context,
314
- long current_cpu_time_ns,
315
315
  long current_monotonic_wall_time_ns,
316
316
  bool force_sample
317
317
  );
@@ -385,6 +385,7 @@ static uint64_t otel_span_id_to_uint(VALUE otel_span_id);
385
385
  static VALUE safely_lookup_hash_without_going_into_ruby_code(VALUE hash, VALUE key);
386
386
  static VALUE _native_system_epoch_time_now_ns(DDTRACE_UNUSED VALUE self, VALUE collector_instance);
387
387
  static VALUE _native_prepare_sample_inside_signal_handler(DDTRACE_UNUSED VALUE self);
388
+ static VALUE _native_request_prepare_on_gc_finish(DDTRACE_UNUSED VALUE self);
388
389
  static VALUE _native_mark_thread_as_profiler_internal(DDTRACE_UNUSED VALUE self, VALUE thread);
389
390
  static VALUE _native_remove_per_thread_context_for(DDTRACE_UNUSED VALUE self, VALUE thread);
390
391
  static VALUE _native_global_reset_per_thread_context(DDTRACE_UNUSED VALUE self, VALUE collector_instance);
@@ -424,6 +425,7 @@ void collectors_thread_context_init(VALUE profiling_module) {
424
425
  rb_define_singleton_method(testing_module, "_native_sample_skipped_allocation_samples", _native_sample_skipped_allocation_samples, 2);
425
426
  rb_define_singleton_method(testing_module, "_native_system_epoch_time_now_ns", _native_system_epoch_time_now_ns, 1);
426
427
  rb_define_singleton_method(testing_module, "_native_prepare_sample_inside_signal_handler", _native_prepare_sample_inside_signal_handler, 0);
428
+ rb_define_singleton_method(testing_module, "_native_request_prepare_on_gc_finish", _native_request_prepare_on_gc_finish, 0);
427
429
  rb_define_singleton_method(testing_module, "_native_remove_per_thread_context_for", _native_remove_per_thread_context_for, 1);
428
430
  rb_define_singleton_method(testing_module, "_native_global_reset_per_thread_context", _native_global_reset_per_thread_context, 1);
429
431
  rb_define_singleton_method(testing_module, "_native_mark_thread_as_profiler_internal", _native_mark_thread_as_profiler_internal, 1);
@@ -679,7 +681,7 @@ static VALUE _native_on_gc_start(DDTRACE_UNUSED VALUE self, VALUE collector_inst
679
681
  static VALUE _native_on_gc_finish(DDTRACE_UNUSED VALUE self, VALUE collector_instance) {
680
682
  debug_enter_unsafe_context();
681
683
 
682
- (void) !thread_context_collector_on_gc_finish(collector_instance);
684
+ (void) !thread_context_collector_on_gc_finish(collector_instance, true);
683
685
 
684
686
  debug_leave_unsafe_context();
685
687
 
@@ -741,6 +743,7 @@ static void record_sampling_overhead(thread_context_collector_state *state, per_
741
743
  // Assumption 3: This function IS NOT called from a signal handler. This function is not async-signal-safe.
742
744
  // Assumption 4: This function IS NOT called in a reentrant way.
743
745
  // Assumption 5: This function is called from the main Ractor (if Ruby has support for Ractors).
746
+ // Assumption 6: When called while the profiler is active, `during_sample` MUST be set.
744
747
  //
745
748
  bool thread_context_collector_sample(VALUE self_instance, long current_monotonic_wall_time_ns) {
746
749
  thread_context_collector_state *state;
@@ -748,25 +751,30 @@ bool thread_context_collector_sample(VALUE self_instance, long current_monotonic
748
751
 
749
752
  VALUE current_thread = rb_thread_current();
750
753
  per_thread_context *current_thread_context = get_or_create_context_for(current_thread);
751
- long cpu_time_at_sample_start_for_current_thread = cpu_time_now_ns(current_thread_context);
754
+
755
+ // Sample the current thread (which will use the current CPU-time) first.
756
+ // This ensures any CPU-time the profiler spends sampling (e.g. the rest of the function) is not wrongly blamed on this thread,
757
+ // and instead is separately accounted by `record_sampling_overhead()` below.
758
+ update_metrics_and_sample(
759
+ state,
760
+ current_thread,
761
+ current_thread_context,
762
+ current_monotonic_wall_time_ns,
763
+ false);
752
764
 
753
765
  VALUE threads = thread_list(state);
754
766
 
755
767
  const long thread_count = RARRAY_LEN(threads);
756
768
  for (long i = 0; i < thread_count; i++) {
757
- VALUE thread = RARRAY_AREF(threads, i);
758
- per_thread_context *thread_context = get_or_create_context_for(thread);
769
+ VALUE thread = rb_ary_entry(threads, i);
770
+ ENFORCE_THREAD(thread);
759
771
 
760
- // We account for cpu-time for the current thread in a different way: we use the cpu-time at sampling start,
761
- // to avoid blaming the time the profiler took on whatever is currently running on the thread,
762
- // and instead we report that time the profiler took as sampling overhead below.
763
- long current_cpu_time_ns = (thread == current_thread) ? cpu_time_at_sample_start_for_current_thread : cpu_time_now_ns(thread_context);
772
+ if (thread == current_thread) continue; // Already sampled above
764
773
 
765
774
  update_metrics_and_sample(
766
775
  state,
767
776
  thread,
768
- thread_context,
769
- current_cpu_time_ns,
777
+ get_or_create_context_for(thread),
770
778
  current_monotonic_wall_time_ns,
771
779
  false);
772
780
  }
@@ -790,10 +798,10 @@ static void update_metrics_and_sample(
790
798
  thread_context_collector_state *state,
791
799
  VALUE thread_being_sampled,
792
800
  per_thread_context *thread_context,
793
- long current_cpu_time_ns,
794
801
  long current_monotonic_wall_time_ns,
795
802
  bool force_sample
796
803
  ) {
804
+ long current_cpu_time_ns = cpu_time_now_ns(thread_context);
797
805
  bool is_gvl_waiting_state =
798
806
  handle_gvl_waiting(state, thread_being_sampled, thread_context, current_cpu_time_ns);
799
807
 
@@ -888,7 +896,12 @@ void thread_context_collector_on_gc_start(VALUE self_instance) {
888
896
  }
889
897
 
890
898
  // Here we record the wall-time first and in on_gc_finish we record it second to try to avoid having wall-time be slightly < cpu-time
891
- thread_context->gc_tracking.wall_time_at_start_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE);
899
+ long wall_time_at_start_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE);
900
+
901
+ // If our start timestamp is not OK, we skip tracking this GC as well
902
+ if (wall_time_at_start_ns == 0) return;
903
+
904
+ thread_context->gc_tracking.wall_time_at_start_ns = wall_time_at_start_ns;
892
905
  thread_context->gc_tracking.cpu_time_at_start_ns = cpu_time_now_ns(thread_context);
893
906
  }
894
907
 
@@ -902,8 +915,9 @@ void thread_context_collector_on_gc_start(VALUE self_instance) {
902
915
  //
903
916
  // Assumption 1: This function is called in a thread that is holding the Global VM Lock. Caller is responsible for enforcing this.
904
917
  // Assumption 2: This function is called from the main Ractor (if Ruby has support for Ractors).
918
+ // Assumption 3: If allow_prepare_sample is true while profiling is active, during_sample MUST be set by the caller.
905
919
  __attribute__((warn_unused_result))
906
- bool thread_context_collector_on_gc_finish(VALUE self_instance) {
920
+ bool thread_context_collector_on_gc_finish(VALUE self_instance, bool allow_prepare_sample) {
907
921
  thread_context_collector_state *state;
908
922
  if (!rb_typeddata_is_kind_of(self_instance, &thread_context_collector_typed_data)) return false;
909
923
  // This should never fail when the above check passes
@@ -914,10 +928,16 @@ bool thread_context_collector_on_gc_finish(VALUE self_instance) {
914
928
  // Context is created eagerly, so this should not normally be NULL (see on_gc_start).
915
929
  if (thread_context == NULL) return false;
916
930
 
931
+ if (thread_context->prepare_sample_on_gc_finish) {
932
+ thread_context->prepare_sample_on_gc_finish = false;
933
+ if (allow_prepare_sample) prepare_sample_thread(rb_thread_current(), &thread_context->sampling_buffer);
934
+ }
935
+
917
936
  long cpu_time_at_start_ns = thread_context->gc_tracking.cpu_time_at_start_ns;
918
937
  long wall_time_at_start_ns = thread_context->gc_tracking.wall_time_at_start_ns;
919
938
 
920
- if (cpu_time_at_start_ns == INVALID_TIME && wall_time_at_start_ns == INVALID_TIME) {
939
+ // Both fields are always set and cleared together, so checking one is enough
940
+ if (cpu_time_at_start_ns == INVALID_TIME) {
921
941
  // If this happened, it means that on_gc_start was either never called for the thread OR it was called but no thread
922
942
  // context existed at the time. The former can be the result of a bug, but since we can't distinguish them, we just
923
943
  // do nothing.
@@ -980,6 +1000,7 @@ bool thread_context_collector_on_gc_finish(VALUE self_instance) {
980
1000
  // Assumption 2: This function is allowed to raise exceptions. Caller is responsible for handling them, if needed.
981
1001
  // Assumption 3: Unlike `on_gc_start` and `on_gc_finish`, this method is allowed to allocate memory as needed.
982
1002
  // Assumption 4: This function is called from the main Ractor (if Ruby has support for Ractors).
1003
+ // Assumption 5: When called while the profiler is active, `during_sample` MUST be set.
983
1004
  VALUE thread_context_collector_sample_after_gc(VALUE self_instance) {
984
1005
  thread_context_collector_state *state;
985
1006
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
@@ -1319,6 +1340,8 @@ void thread_context_collector_reset_all_per_thread_contexts(VALUE self_instance)
1319
1340
  const long thread_count = RARRAY_LEN(threads);
1320
1341
  for (long i = 0; i < thread_count; i++) {
1321
1342
  VALUE thread = rb_ary_entry(threads, i);
1343
+ ENFORCE_THREAD(thread);
1344
+
1322
1345
  per_thread_context *thread_context = get_per_thread_context(thread);
1323
1346
  if (thread_context != NULL) {
1324
1347
  bool is_profiler_internal_thread = thread_context->is_profiler_internal_thread;
@@ -1387,6 +1410,7 @@ static VALUE per_thread_context_to_ruby_hash(per_thread_context *thread_context)
1387
1410
  ID2SYM(rb_intern("gvl_state_change_count")), /* => */ ULL2NUM(thread_context->gvl_state_change_count),
1388
1411
  ID2SYM(rb_intern("gvl_state_change_count_at_previous_sample")), /* => */ ULL2NUM(thread_context->gvl_state_change_count_at_previous_sample),
1389
1412
  ID2SYM(rb_intern("was_skipped_at_last_sample")), /* => */ thread_context->was_skipped_at_last_sample ? Qtrue : Qfalse,
1413
+ ID2SYM(rb_intern("prepare_sample_on_gc_finish")), /* => */ thread_context->prepare_sample_on_gc_finish ? Qtrue : Qfalse,
1390
1414
  ID2SYM(rb_intern("is_profiler_internal_thread")), /* => */ thread_context->is_profiler_internal_thread ? Qtrue : Qfalse,
1391
1415
  };
1392
1416
  for (long unsigned int i = 0; i < VALUE_COUNT(arguments); i += 2) rb_hash_aset(context_as_hash, arguments[i], arguments[i+1]);
@@ -1433,7 +1457,9 @@ static VALUE _native_per_thread_context(DDTRACE_UNUSED VALUE _self, VALUE collec
1433
1457
  VALUE threads = thread_list(state);
1434
1458
  const long thread_count = RARRAY_LEN(threads);
1435
1459
  for (long i = 0; i < thread_count; i++) {
1436
- VALUE thread = RARRAY_AREF(threads, i);
1460
+ VALUE thread = rb_ary_entry(threads, i);
1461
+ ENFORCE_THREAD(thread);
1462
+
1437
1463
  per_thread_context *thread_context = get_per_thread_context(thread);
1438
1464
  if (thread_context != NULL) {
1439
1465
  rb_hash_aset(result, thread, per_thread_context_to_ruby_hash(thread_context));
@@ -1443,9 +1469,6 @@ static VALUE _native_per_thread_context(DDTRACE_UNUSED VALUE _self, VALUE collec
1443
1469
  }
1444
1470
 
1445
1471
  static long update_time_since_previous_sample(long *time_at_previous_sample_ns, long current_time_ns, per_thread_context *thread_context) {
1446
- // If we didn't have a time for the previous sample, we use the current one
1447
- if (*time_at_previous_sample_ns == INVALID_TIME) *time_at_previous_sample_ns = current_time_ns;
1448
-
1449
1472
  // We don't expect to be sampling a thread (and thus updating these counters) while Ruby is doing GC (between
1450
1473
  // `thread_context_collector_on_gc_start` and `thread_context_collector_on_gc_finish`)
1451
1474
  if (thread_context->gc_tracking.cpu_time_at_start_ns != INVALID_TIME) {
@@ -1467,6 +1490,13 @@ static long update_time_since_previous_sample(long *time_at_previous_sample_ns,
1467
1490
  }
1468
1491
 
1469
1492
  static long update_cpu_time_since_previous_sample(per_thread_context *thread_context, long current_cpu_time_ns) {
1493
+ // A previous `cpu_time_now_ns` may have failed to read the clock and thus invalidated our baseline; in that case we
1494
+ // use the current time, so this sample gets no cpu-time and the next one gets an accurate delta.
1495
+ // Note wall-time never needs this: it is always seeded with a real value in `initialize_context`.
1496
+ if (thread_context->cpu_time_at_previous_sample_ns == INVALID_TIME) {
1497
+ thread_context->cpu_time_at_previous_sample_ns = current_cpu_time_ns;
1498
+ }
1499
+
1470
1500
  long elapsed_time_ns = update_time_since_previous_sample(
1471
1501
  &thread_context->cpu_time_at_previous_sample_ns,
1472
1502
  current_cpu_time_ns,
@@ -1669,7 +1699,7 @@ static VALUE thread_list(thread_context_collector_state *state) {
1669
1699
  // the current thread.
1670
1700
  //
1671
1701
  // Assumptions for this function are same as for `thread_context_collector_sample` except that this function is
1672
- // expected to be called from a signal handler and to be async-signal-safe.
1702
+ // expected to be called from a signal handler and to be async-signal-safe, and `during_sample` MUST be unset.
1673
1703
  //
1674
1704
  // Also, no allocation (Ruby or malloc) can happen.
1675
1705
  bool thread_context_collector_prepare_sample_inside_signal_handler(void) {
@@ -1680,13 +1710,20 @@ bool thread_context_collector_prepare_sample_inside_signal_handler(void) {
1680
1710
  return prepare_sample_thread(current_thread, &thread_context->sampling_buffer);
1681
1711
  }
1682
1712
 
1713
+ // Called from the sampling signal handler; must not allocate or inspect the Ruby stack during GC.
1714
+ void thread_context_collector_request_prepare_on_gc_finish(void) {
1715
+ per_thread_context *thread_context = get_per_thread_context(rb_thread_current());
1716
+ if (thread_context != NULL) thread_context->prepare_sample_on_gc_finish = true;
1717
+ }
1718
+
1683
1719
  // This method gets called from inside the RUBY_INTERNAL_EVENT_NEWOBJ tracepoint so it should neither allocate in the
1684
1720
  // Ruby heap nor release the GVL (https://github.com/DataDog/dd-trace-rb/pull/4240).
1685
1721
  //
1686
1722
  // Returns true if `thread_context_collector_commit_heap_recordings_may_lose_gvl` needs to be called (to do work
1687
1723
  // that can't be done from inside the tracepoint, such as allocate new objects), and false if it doesn't
1688
1724
  //
1689
- // The callers must ensure thread_context is non-NULL.
1725
+ // Assumption 1: The callers must ensure thread_context is non-NULL.
1726
+ // Assumption 2: When called while the profiler is active, `during_sample` MUST be set.
1690
1727
  bool thread_context_collector_sample_allocation(VALUE self_instance, per_thread_context *thread_context, unsigned int sample_weight, VALUE new_object) {
1691
1728
  thread_context_collector_state *state;
1692
1729
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
@@ -1932,6 +1969,7 @@ static void ddtrace_otel_trace_identifiers_for(
1932
1969
  *numeric_span_id = resolved_numeric_span_id;
1933
1970
  }
1934
1971
 
1972
+ // Assumption 1: When called while the profiler is active, `during_sample` MUST be set.
1935
1973
  void thread_context_collector_sample_skipped_allocation_samples(VALUE self_instance, unsigned int skipped_samples) {
1936
1974
  thread_context_collector_state *state;
1937
1975
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
@@ -2146,7 +2184,9 @@ static VALUE _native_mark_thread_as_profiler_internal(DDTRACE_UNUSED VALUE self,
2146
2184
  // flush is not lost. on_serialize (below) also flushes profiler-internal threads during periodic
2147
2185
  // serialization, but it can't help at shutdown: by the time the final serialize runs, these
2148
2186
  // threads are already dead and absent from thread_list.
2149
- void thread_context_collector_profiler_internal_thread_done(VALUE self_instance) {
2187
+ //
2188
+ // Assumption 1: When called while the profiler is active, `during_sample` MUST be set.
2189
+ VALUE thread_context_collector_profiler_internal_thread_done(VALUE self_instance) {
2150
2190
  thread_context_collector_state *state;
2151
2191
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
2152
2192
 
@@ -2156,21 +2196,23 @@ void thread_context_collector_profiler_internal_thread_done(VALUE self_instance)
2156
2196
  rb_raise(rb_eRuntimeError, "current thread %"PRIsVALUE" is not profiler-internal thread", current_thread);
2157
2197
  }
2158
2198
 
2159
- long current_cpu_time_ns = cpu_time_now_ns(thread_context);
2160
2199
  long current_monotonic_wall_time_ns = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
2161
2200
 
2162
2201
  update_metrics_and_sample(
2163
2202
  state,
2164
2203
  current_thread,
2165
2204
  thread_context,
2166
- current_cpu_time_ns,
2167
2205
  current_monotonic_wall_time_ns,
2168
2206
  true);
2207
+
2208
+ return Qnil;
2169
2209
  }
2170
2210
 
2171
2211
  // Flushes threads whose last per-tick sample was skipped (either by the SUSPENDED-skip
2172
2212
  // optimization, or by is_profiler_internal_thread) so their accumulated time is recorded.
2173
2213
  // Called by the stack recorder at the start of _native_serialize (regular periodic flush).
2214
+ //
2215
+ // Assumption 1: When called while the profiler is active, `during_sample` MUST be set.
2174
2216
  void thread_context_collector_on_serialize(VALUE self_instance) {
2175
2217
  thread_context_collector_state *state;
2176
2218
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
@@ -2180,19 +2222,19 @@ void thread_context_collector_on_serialize(VALUE self_instance) {
2180
2222
  const long thread_count = RARRAY_LEN(threads);
2181
2223
 
2182
2224
  for (long i = 0; i < thread_count; i++) {
2183
- VALUE thread = RARRAY_AREF(threads, i);
2225
+ VALUE thread = rb_ary_entry(threads, i);
2226
+ ENFORCE_THREAD(thread);
2227
+
2184
2228
  per_thread_context *thread_context = get_per_thread_context(thread);
2185
2229
 
2186
2230
  if (thread_context != NULL && (thread_context->was_skipped_at_last_sample || thread_context->is_profiler_internal_thread)) {
2187
- long current_cpu_time_ns = cpu_time_now_ns(thread_context);
2188
- // We need to force_sample=true otherwise this sample would be skipped too
2189
2231
  update_metrics_and_sample(
2190
2232
  state,
2191
2233
  thread,
2192
2234
  thread_context,
2193
- current_cpu_time_ns,
2194
2235
  current_monotonic_wall_time_ns,
2195
- true);
2236
+ true // We need to force_sample=true otherwise this sample would be skipped too
2237
+ );
2196
2238
  }
2197
2239
  }
2198
2240
  }
@@ -2236,6 +2278,8 @@ static VALUE _native_on_gvl_released(DDTRACE_UNUSED VALUE self, VALUE thread) {
2236
2278
 
2237
2279
  #ifndef NO_GVL_INSTRUMENTATION
2238
2280
  // We must only use async-signal-safe functions here, see notes in the caller
2281
+ //
2282
+ // Assumption 1: When called while the profiler is active, `during_sample` MUST be set.
2239
2283
  __attribute__((warn_unused_result))
2240
2284
  on_gvl_running_result thread_context_collector_on_gvl_running(VALUE thread, per_thread_context *thread_context, uint32_t waiting_for_gvl_threshold_ns) {
2241
2285
  // Bump the event counter and clears the state bit to "running"
@@ -2312,7 +2356,8 @@ static VALUE _native_on_gvl_released(DDTRACE_UNUSED VALUE self, VALUE thread) {
2312
2356
  //
2313
2357
  // ---
2314
2358
  //
2315
- // Always called with the GVL, either from a postponed_job or from tests.
2359
+ // Assumption 1: Always called with the GVL, either from a postponed_job or from tests.
2360
+ // Assumption 2: When called while the profiler is active, `during_sample` MUST be set.
2316
2361
  //
2317
2362
  // NOTE: In normal use, current_thread is expected to be == rb_thread_current(); the `current_thread` parameter only
2318
2363
  // exists to enable testing.
@@ -2332,18 +2377,12 @@ static VALUE _native_on_gvl_released(DDTRACE_UNUSED VALUE self, VALUE thread) {
2332
2377
  return Qfalse;
2333
2378
  }
2334
2379
 
2335
- // We don't actually account for cpu-time during Waiting for GVL. BUT, we may choose to push an
2336
- // extra sample to represent the period prior to Waiting for GVL. To support that, we retrieve the current
2337
- // cpu-time of the thread and let `update_metrics_and_sample` decide what to do with it.
2338
- long cpu_time_for_thread = cpu_time_now_ns(thread_context);
2339
-
2340
2380
  // TODO: Should we update the dynamic sampling rate overhead tracking with this sample as well?
2341
2381
 
2342
2382
  update_metrics_and_sample(
2343
2383
  state,
2344
2384
  current_thread,
2345
2385
  thread_context,
2346
- cpu_time_for_thread,
2347
2386
  current_monotonic_wall_time_ns,
2348
2387
  false);
2349
2388
 
@@ -2554,3 +2593,8 @@ static VALUE _native_system_epoch_time_now_ns(DDTRACE_UNUSED VALUE self, VALUE c
2554
2593
  static VALUE _native_prepare_sample_inside_signal_handler(DDTRACE_UNUSED VALUE self) {
2555
2594
  return thread_context_collector_prepare_sample_inside_signal_handler() ? Qtrue : Qfalse;
2556
2595
  }
2596
+
2597
+ static VALUE _native_request_prepare_on_gc_finish(DDTRACE_UNUSED VALUE self) {
2598
+ thread_context_collector_request_prepare_on_gc_finish();
2599
+ return Qnil;
2600
+ }
@@ -13,6 +13,7 @@ __attribute__((warn_unused_result)) bool thread_context_collector_sample(
13
13
  long current_monotonic_wall_time_ns
14
14
  );
15
15
  __attribute__((warn_unused_result)) bool thread_context_collector_prepare_sample_inside_signal_handler(void);
16
+ void thread_context_collector_request_prepare_on_gc_finish(void);
16
17
  __attribute__((warn_unused_result)) bool thread_context_collector_sample_allocation(VALUE self_instance, per_thread_context *thread_context, unsigned int sample_weight, VALUE new_object);
17
18
  void thread_context_collector_commit_heap_recordings_may_lose_gvl(VALUE self_instance);
18
19
  void thread_context_collector_sample_skipped_allocation_samples(VALUE self_instance, unsigned int skipped_samples);
@@ -20,14 +21,14 @@ VALUE thread_context_collector_sample_after_gc(VALUE self_instance);
20
21
  VALUE thread_context_collector_heap_update_may_lose_gvl(VALUE self_instance);
21
22
  void thread_context_collector_resolve_otel_span_key_may_lose_gvl(VALUE self_instance);
22
23
  void thread_context_collector_on_gc_start(VALUE self_instance);
23
- __attribute__((warn_unused_result)) bool thread_context_collector_on_gc_finish(VALUE self_instance);
24
+ __attribute__((warn_unused_result)) bool thread_context_collector_on_gc_finish(VALUE self_instance, bool allow_prepare_sample);
24
25
  VALUE enforce_thread_context_collector_instance(VALUE object);
25
26
  void thread_context_collector_stats(VALUE self_instance, VALUE stats_hash);
26
27
  void thread_context_collector_stats_reset_not_thread_safe(VALUE self_instance);
27
28
  void thread_context_collector_on_serialize(VALUE self_instance);
28
29
  void thread_context_collector_reset_all_per_thread_contexts(VALUE self_instance);
29
30
  void thread_context_collector_profiler_internal_thread_started(void);
30
- void thread_context_collector_profiler_internal_thread_done(VALUE self_instance);
31
+ VALUE thread_context_collector_profiler_internal_thread_done(VALUE self_instance);
31
32
 
32
33
  #ifndef NO_GVL_INSTRUMENTATION
33
34
  typedef enum {
@@ -260,7 +260,9 @@ dir_config("ruby") # allow user to pass in non-standard core include directory
260
260
  #
261
261
  # To workaround this issue, we override `with_cppflags` for datadog-ruby_core_source to still include `$CPPFLAGS`.
262
262
  Datadog::RubyCoreSource.define_singleton_method(:with_cppflags) do |newflags, &block|
263
- super("#{newflags} #{$CPPFLAGS}", &block)
263
+ # Note: The `Shellwords.escape` is an additional workaround for ruby_core_source supplying
264
+ # one unescaped -I argument containing the complete header path, not the CPPFLAGS thing above.
265
+ super("#{Shellwords.escape(newflags)} #{$CPPFLAGS}", &block)
264
266
  end
265
267
 
266
268
  Datadog::RubyCoreSource
@@ -270,7 +272,7 @@ Datadog::RubyCoreSource
270
272
  have_header("vm_core.h") &&
271
273
  have_header("iseq.h", "vm_core.h") &&
272
274
  # These are only used on Ruby 3+
273
- (RUBY_VERSION < "3" || have_header("ractor_core.h") && have_header("internal/class.h"))
275
+ (RUBY_VERSION < "3" || have_header("ractor_core.h") && have_header("internal/class.h", "vm_core.h"))
274
276
 
275
277
  if headers_available
276
278
  # Warn on unused parameters to functions. Use `DDTRACE_UNUSED` to mark things as known-to-not-be-used.
@@ -1193,6 +1193,19 @@ VALUE heap_recorder_testonly_record_id_for(heap_recorder *heap_recorder, VALUE o
1193
1193
  return context.result;
1194
1194
  }
1195
1195
 
1196
+ void heap_recorder_testonly_set_object_alloc_gen(heap_recorder *heap_recorder, long record_id, size_t alloc_gen) {
1197
+ if (heap_recorder == NULL) {
1198
+ raise_error(rb_eArgError, "heap_recorder is NULL");
1199
+ }
1200
+
1201
+ object_record *record = NULL;
1202
+ if (!st_lookup(heap_recorder->object_records, (st_data_t) record_id, (st_data_t *) &record)) {
1203
+ raise_error(rb_eArgError, "record_id %ld is not being tracked", record_id);
1204
+ }
1205
+
1206
+ record->object_data.alloc_gen = alloc_gen;
1207
+ }
1208
+
1196
1209
  void heap_recorder_testonly_exhaust_record_ids(heap_recorder *heap_recorder) {
1197
1210
  if (heap_recorder == NULL) {
1198
1211
  raise_error(rb_eArgError, "heap_recorder is NULL");
@@ -190,6 +190,10 @@ VALUE heap_recorder_testonly_is_object_recorded(heap_recorder *heap_recorder, lo
190
190
  // NOTE: This is a linear scan over the tracked objects; fine for the small numbers tests track.
191
191
  VALUE heap_recorder_testonly_record_id_for(heap_recorder *heap_recorder, VALUE obj);
192
192
 
193
+ // Overrides the GC gen a tracked object is considered to have been allocated in. This lets tests give an object an
194
+ // exact age, instead of relying on an exact number of GCs having run.
195
+ void heap_recorder_testonly_set_object_alloc_gen(heap_recorder *heap_recorder, long record_id, size_t alloc_gen);
196
+
193
197
  // Used to ensure that a GC actually triggers an update of the objects
194
198
  void heap_recorder_testonly_reset_last_update(heap_recorder *heap_recorder);
195
199
 
@@ -278,6 +278,23 @@ VALUE thread_name_for(VALUE thread) {
278
278
  return thread_struct_from_object(thread)->name;
279
279
  }
280
280
 
281
+ // This test doubles as a validation for the layout of the rb_thread_t structure. Because the `name` is almost at
282
+ // the end of the structure, if somehow we can't read the name, this probably means we're getting the structure wrong
283
+ // and we should stop immediately (this includes covering the use of `stat_insn_usage` on Ruby <= 3.2).
284
+ //
285
+ // One example where this happens is in patches such as
286
+ // https://github.com/gitlabhq/omnibus-gitlab/blob/master/config/patches/ruby/thread-memory-allocations-3.2.patch
287
+ // that add items to the structure and thus shift its position from where we expect.
288
+ void self_test_thread_name_for(void) {
289
+ VALUE thread = rb_eval_string("Thread.new { Thread.current.name = 'dd-prof-test' }.join");
290
+ VALUE expected_name = rb_funcall(thread, rb_intern("name"), 0);
291
+ if (thread_name_for(thread) != expected_name) {
292
+ rb_raise(rb_eRuntimeError,
293
+ "thread_name_for() self-test failed; this usually means the `rb_thread_t` on this Ruby doesn't match what the profiler expects"
294
+ );
295
+ }
296
+ }
297
+
281
298
  // -----------------------------------------------------------------------------
282
299
  // The sources below are modified versions of code extracted from the Ruby project.
283
300
  // Each function is annotated with its origin, why we imported it, and the changes made.
@@ -539,6 +556,14 @@ int ddtrace_rb_profile_frames(VALUE thread, int start, int limit, frame_info *st
539
556
  cfp = RUBY_VM_NEXT_CONTROL_FRAME(end_cfp);
540
557
 
541
558
  for (i=0; i<limit && cfp != top_sentinel; cfp = RUBY_VM_NEXT_CONTROL_FRAME(cfp)) {
559
+ #ifndef NO_T_MOVED
560
+ if (cfp->iseq && RB_TYPE_P((VALUE) cfp->iseq, T_MOVED)) {
561
+ // The profiler is not supposed to sample during GC compaction, so T_MOVED is not expected here.
562
+ // Yet, crash tracking also uses this walker and may run at any time. For now, we choose to skip these frames.
563
+ continue;
564
+ }
565
+ #endif
566
+
542
567
  if (cfp->iseq && !cfp->pc) {
543
568
  // Fix: Do nothing -- this frame should not be used
544
569
  //
@@ -982,8 +1007,15 @@ VALUE ddtrace_alloc_free_rb_mod_name(VALUE mod) {
982
1007
  // Checking for '#' in the name is equivalent on older Rubies:
983
1008
  // non-permanent names contain '#' from `#<Module:0x...>` or `#<Class:0x...>` prefixes.
984
1009
  static bool has_permanent_classpath(DDTRACE_UNUSED VALUE mod, DDTRACE_UNUSED VALUE mod_name) {
985
- #if defined(RCLASS_PERMANENT_CLASSPATH_P) // 4.0+
986
- return RCLASS_PERMANENT_CLASSPATH_P(mod);
1010
+ #if defined(RCLASS_EXT_PRIME) // 4.0+
1011
+ // We can't actually use RCLASS_PERMANENT_CLASSPATH_P(mod) because that uses
1012
+ // RCLASS_EXT_READABLE() which uses rb_current_box(), which is a private symbol:
1013
+ // https://github.com/DataDog/dd-trace-rb/issues/6338
1014
+ // Instead we use `RCLASS_EXT_PRIME(mod)->permanent_classpath`, which is equivalent for our usage:
1015
+ // the two can only differ for boxable (= core) modules with no permanent name, i.e. metaclasses like
1016
+ // String.singleton_class, and is_metaclass() already replaced those with the attached module, so we display
1017
+ // `String.foo` and not `#<Class:String>#foo`. Same for the name in ddtrace_alloc_free_rb_mod_name().
1018
+ return RCLASS_EXT_PRIME(mod)->permanent_classpath;
987
1019
  #elif defined(HAVE_PERMANENT_CLASSPATH) // 3.3 - 3.4
988
1020
  return RCLASS_EXT(mod)->permanent_classpath;
989
1021
  #else // 3.2 and older
@@ -45,6 +45,7 @@ uint64_t native_thread_id_for(VALUE thread);
45
45
  void ddtrace_thread_list(VALUE result_array);
46
46
  bool is_thread_alive(VALUE thread);
47
47
  VALUE thread_name_for(VALUE thread);
48
+ void self_test_thread_name_for(void);
48
49
 
49
50
  int ddtrace_rb_profile_frames(VALUE thread, int start, int limit, frame_info *stack_buffer);
50
51
 
@@ -103,6 +103,7 @@ void DDTRACE_EXPORT Init_datadog_profiling_native_extension(void) {
103
103
  }
104
104
 
105
105
  static VALUE native_working_p(DDTRACE_UNUSED VALUE _self) {
106
+ self_test_thread_name_for();
106
107
  self_test_clock_id();
107
108
  self_test_current_fiber_for();
108
109
  self_test_mn_enabled();
@@ -275,6 +275,7 @@ static VALUE _native_is_object_recorded(DDTRACE_UNUSED VALUE _self, VALUE record
275
275
  static VALUE _native_record_id_for(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance, VALUE obj);
276
276
  static VALUE _native_heap_recorder_reset_last_update(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance);
277
277
  static VALUE _native_heap_recorder_exhaust_record_ids(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance);
278
+ static VALUE _native_heap_recorder_set_object_alloc_gen(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance, VALUE record_id, VALUE alloc_gen);
278
279
  static VALUE _native_recorder_heap_update(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance);
279
280
  static VALUE _native_benchmark_intern(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance, VALUE string, VALUE times, VALUE use_all);
280
281
  static VALUE _native_test_managed_string_storage_produces_valid_profiles(DDTRACE_UNUSED VALUE _self);
@@ -313,6 +314,7 @@ void stack_recorder_init(VALUE profiling_module) {
313
314
  rb_define_singleton_method(testing_module, "_native_record_id_for", _native_record_id_for, 2);
314
315
  rb_define_singleton_method(testing_module, "_native_heap_recorder_reset_last_update", _native_heap_recorder_reset_last_update, 1);
315
316
  rb_define_singleton_method(testing_module, "_native_heap_recorder_exhaust_record_ids", _native_heap_recorder_exhaust_record_ids, 1);
317
+ rb_define_singleton_method(testing_module, "_native_heap_recorder_set_object_alloc_gen", _native_heap_recorder_set_object_alloc_gen, 3);
316
318
  rb_define_singleton_method(testing_module, "_native_recorder_heap_update", _native_recorder_heap_update, 1);
317
319
  rb_define_singleton_method(testing_module, "_native_benchmark_intern", _native_benchmark_intern, 4);
318
320
  rb_define_singleton_method(testing_module, "_native_test_managed_string_storage_produces_valid_profiles", _native_test_managed_string_storage_produces_valid_profiles, 0);
@@ -537,10 +539,6 @@ static VALUE _native_serialize(DDTRACE_UNUSED VALUE _self, VALUE recorder_instan
537
539
  stack_recorder_state *state;
538
540
  TypedData_Get_Struct(recorder_instance, stack_recorder_state, &stack_recorder_typed_data, state);
539
541
 
540
- ddog_Timespec finish_timestamp = system_epoch_now_timespec();
541
- // Need to do this while still holding the Global VM Lock; see comments on method for why
542
- serializer_set_start_timestamp_for_next_profile(state, finish_timestamp);
543
-
544
542
  if (state->thread_context_collector_instance != Qnil) {
545
543
  thread_context_collector_on_serialize(state->thread_context_collector_instance);
546
544
  }
@@ -553,6 +551,10 @@ static VALUE _native_serialize(DDTRACE_UNUSED VALUE _self, VALUE recorder_instan
553
551
  heap_recorder_prepare_iteration(state->heap_recorder);
554
552
  long heap_iteration_prep_time_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE) - heap_iteration_prep_start_time_ns;
555
553
 
554
+ ddog_Timespec finish_timestamp = system_epoch_now_timespec();
555
+ // Need to do this while still holding the Global VM Lock; see comments on method for why
556
+ serializer_set_start_timestamp_for_next_profile(state, finish_timestamp);
557
+
556
558
  // We'll release the Global VM Lock while we're calling serialize, so that the Ruby VM can continue to work while this
557
559
  // is pending
558
560
  call_serialize_without_gvl_arguments args = {
@@ -1065,6 +1067,17 @@ static VALUE _native_heap_recorder_exhaust_record_ids(DDTRACE_UNUSED VALUE _self
1065
1067
  return Qtrue;
1066
1068
  }
1067
1069
 
1070
+ static VALUE _native_heap_recorder_set_object_alloc_gen(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance, VALUE record_id, VALUE alloc_gen) {
1071
+ ENFORCE_TYPE(record_id, T_FIXNUM);
1072
+ ENFORCE_TYPE(alloc_gen, T_FIXNUM);
1073
+
1074
+ stack_recorder_state *state;
1075
+ TypedData_Get_Struct(recorder_instance, stack_recorder_state, &stack_recorder_typed_data, state);
1076
+
1077
+ heap_recorder_testonly_set_object_alloc_gen(state->heap_recorder, FIX2LONG(record_id), NUM2SIZET(alloc_gen));
1078
+ return Qtrue;
1079
+ }
1080
+
1068
1081
  static VALUE _native_heap_recorder_reset_last_update(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance) {
1069
1082
  stack_recorder_state *state;
1070
1083
  TypedData_Get_Struct(recorder_instance, stack_recorder_state, &stack_recorder_typed_data, state);
@@ -7,20 +7,21 @@
7
7
  #include "extconf.h"
8
8
  #include "ruby_helpers.h"
9
9
 
10
- #define SECONDS_AS_NS(value) (value * 1000 * 1000 * 1000L)
11
- #define MILLIS_AS_NS(value) (value * 1000 * 1000L)
12
- #define MICROS_AS_NS(value) (value * 1000L)
10
+ #define SECONDS_AS_NS(value) ((value) * 1000L * 1000L * 1000L)
11
+ #define MILLIS_AS_NS(value) ((value) * 1000L * 1000L)
12
+ #define MICROS_AS_NS(value) ((value) * 1000L)
13
13
 
14
14
  typedef enum { RAISE_ON_FAILURE, DO_NOT_RAISE_ON_FAILURE } raise_on_failure_setting;
15
15
 
16
16
  #define INVALID_TIME -1
17
17
 
18
18
  typedef struct {
19
+ // INVALID_TIME until the first conversion, and the only field used to decide if the state needs to be (re)computed
19
20
  long system_epoch_ns_reference;
20
21
  long delta_to_epoch_ns;
21
22
  } monotonic_to_system_epoch_state;
22
23
 
23
- #define MONOTONIC_TO_SYSTEM_EPOCH_INITIALIZER {.system_epoch_ns_reference = INVALID_TIME, .delta_to_epoch_ns = INVALID_TIME}
24
+ #define MONOTONIC_TO_SYSTEM_EPOCH_INITIALIZER {.system_epoch_ns_reference = INVALID_TIME}
24
25
 
25
26
  static inline long retrieve_clock_as_ns(clockid_t clock_id, raise_on_failure_setting raise_on_failure) {
26
27
  struct timespec clock_value;
@@ -2,6 +2,9 @@
2
2
 
3
3
  #include "datadog_ruby_common.h"
4
4
 
5
+ // rb_iseq is CRuby-specific
6
+ #ifndef TRUFFLERUBY
7
+
5
8
  // Prototypes for Ruby functions declared in internal Ruby headers.
6
9
  // rb_iseqw_new wraps an internal iseq pointer into a Ruby-visible
7
10
  // RubyVM::InstructionSequence object.
@@ -207,7 +210,9 @@ void di_init(VALUE datadog_module) {
207
210
  rb_define_singleton_method(di_module, "enter_probe", enter_probe, 0);
208
211
  rb_define_singleton_method(di_module, "leave_probe", leave_probe, 0);
209
212
  rb_define_singleton_method(di_module, "hash?", is_hash, 1);
210
- #ifdef HAVE_RB_ISEQ_TYPE
211
- rb_define_singleton_method(di_module, "iseq_type", iseq_type, 1);
212
- #endif
213
+ #ifdef HAVE_RB_ISEQ_TYPE
214
+ rb_define_singleton_method(di_module, "iseq_type", iseq_type, 1);
215
+ #endif
213
216
  }
217
+
218
+ #endif // TRUFFLERUBY
@@ -24,7 +24,7 @@ end
24
24
  if ENV["DD_NO_EXTENSION"].to_s.strip.downcase == "true"
25
25
  skip_building_extension!("the `DD_NO_EXTENSION` environment variable is/was set to `true` during installation")
26
26
  end
27
- skip_building_extension!("current Ruby VM is not supported") if RUBY_ENGINE != "ruby"
27
+ skip_building_extension!("current Ruby VM is not supported") unless %w[ruby truffleruby].include?(RUBY_ENGINE)
28
28
  skip_building_extension!("Microsoft Windows is not supported") if Gem.win_platform?
29
29
 
30
30
  libdatadog_issue = Datadog::LibdatadogExtconfHelpers.load_libdatadog_or_get_issue
@@ -13,6 +13,7 @@
13
13
  // Forward declarations
14
14
  static VALUE configuration_new(VALUE klass, VALUE json_str);
15
15
  static void configuration_free(void *ptr);
16
+ static VALUE configuration_get_observe_full_evaluation_data(VALUE self);
16
17
  static VALUE configuration_get_assignment(
17
18
  VALUE self, VALUE flag_key, VALUE expected_type, VALUE context);
18
19
 
@@ -94,6 +95,7 @@ void feature_flags_init(VALUE core_module) {
94
95
  rb_undef_alloc_func(configuration_class);
95
96
  rb_define_singleton_method(configuration_class, "new", configuration_new, 1);
96
97
  rb_define_method(configuration_class, "get_assignment", configuration_get_assignment, 3);
98
+ rb_define_method(configuration_class, "observe_full_evaluation_data", configuration_get_observe_full_evaluation_data, 0);
97
99
 
98
100
  rb_gc_register_address(&resolution_details_class);
99
101
  resolution_details_class = rb_define_class_under(feature_flags_module, "ResolutionDetails", rb_cObject);
@@ -137,6 +139,12 @@ static VALUE configuration_new(VALUE klass, VALUE json_str) {
137
139
  return TypedData_Wrap_Struct(klass, &configuration_data_type, result.ok);
138
140
  }
139
141
 
142
+ static VALUE configuration_get_observe_full_evaluation_data(VALUE self) {
143
+ ddog_ffe_Handle_Configuration configuration =
144
+ (ddog_ffe_Handle_Configuration)rb_check_typeddata(self, &configuration_data_type);
145
+ return ddog_ffe_configuration_get_observe_full_evaluation_data(configuration) ? Qtrue : Qfalse;
146
+ }
147
+
140
148
  static void configuration_free(void *ptr) {
141
149
  ddog_ffe_Handle_Configuration config = (ddog_ffe_Handle_Configuration)ptr;
142
150
  ddog_ffe_configuration_drop(&config);