datadog 2.36.0 → 2.38.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 (119) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +74 -1
  3. data/ext/datadog_profiling_native_extension/clock_id_from_mach.c +5 -12
  4. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +29 -18
  5. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +17 -3
  6. data/ext/datadog_profiling_native_extension/collectors_stack.c +19 -24
  7. data/ext/datadog_profiling_native_extension/collectors_stack.h +0 -1
  8. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +348 -213
  9. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +3 -0
  10. data/ext/datadog_profiling_native_extension/extconf.rb +11 -3
  11. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +5 -0
  12. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +2 -0
  13. data/ext/datadog_profiling_native_extension/ruby_helpers.c +7 -0
  14. data/ext/datadog_profiling_native_extension/time_helpers.h +12 -7
  15. data/ext/libdatadog_api/di.c +76 -0
  16. data/ext/libdatadog_api/extconf.rb +6 -0
  17. data/ext/libdatadog_extconf_helpers.rb +1 -1
  18. data/lib/datadog/ai_guard/api_client.rb +6 -4
  19. data/lib/datadog/appsec/api_security/route_extractor.rb +7 -4
  20. data/lib/datadog/appsec/configuration.rb +6 -0
  21. data/lib/datadog/appsec/context.rb +4 -2
  22. data/lib/datadog/appsec/contrib/excon/ssrf_detection_middleware.rb +59 -4
  23. data/lib/datadog/appsec/contrib/faraday/ssrf_detection_middleware.rb +59 -5
  24. data/lib/datadog/appsec/contrib/graphql/gateway/multiplex.rb +1 -1
  25. data/lib/datadog/appsec/contrib/rack/gateway/request.rb +5 -0
  26. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +4 -0
  27. data/lib/datadog/appsec/contrib/rack/input_peeker.rb +4 -17
  28. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +21 -0
  29. data/lib/datadog/appsec/contrib/rails/gateway/request.rb +7 -1
  30. data/lib/datadog/appsec/contrib/rest_client/request_ssrf_detection_patch.rb +60 -7
  31. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +4 -0
  32. data/lib/datadog/appsec/ext.rb +1 -0
  33. data/lib/datadog/appsec/metrics/collector.rb +18 -1
  34. data/lib/datadog/appsec/metrics/exporter.rb +9 -3
  35. data/lib/datadog/appsec/remote.rb +9 -3
  36. data/lib/datadog/appsec/route_normalizer/rails_route_pattern.rb +176 -0
  37. data/lib/datadog/appsec/route_normalizer/route_pattern.rb +378 -0
  38. data/lib/datadog/appsec/route_normalizer/route_text.rb +57 -0
  39. data/lib/datadog/appsec/route_normalizer.rb +80 -0
  40. data/lib/datadog/appsec/utils/http/body_reader.rb +61 -0
  41. data/lib/datadog/core/configuration/agentless_settings_resolver.rb +2 -2
  42. data/lib/datadog/core/configuration/components.rb +82 -2
  43. data/lib/datadog/core/configuration/components_state.rb +6 -1
  44. data/lib/datadog/core/configuration/settings.rb +4 -7
  45. data/lib/datadog/core/configuration/supported_configurations.rb +3 -0
  46. data/lib/datadog/core/evp.rb +11 -0
  47. data/lib/datadog/core/knuth_sampler.rb +3 -1
  48. data/lib/datadog/core/metrics/client.rb +1 -1
  49. data/lib/datadog/core/remote/client/capabilities.rb +34 -7
  50. data/lib/datadog/core/telemetry/event/app_started.rb +8 -1
  51. data/lib/datadog/core/transport/http/builder.rb +1 -1
  52. data/lib/datadog/core/utils/at_fork_monkey_patch.rb +1 -1
  53. data/lib/datadog/core/utils/time.rb +5 -1
  54. data/lib/datadog/di/base.rb +3 -1
  55. data/lib/datadog/di/boot.rb +3 -0
  56. data/lib/datadog/di/capture_expression.rb +21 -0
  57. data/lib/datadog/di/capture_expression_evaluator.rb +71 -0
  58. data/lib/datadog/di/capture_limits.rb +41 -0
  59. data/lib/datadog/di/code_tracker.rb +5 -2
  60. data/lib/datadog/di/component.rb +112 -38
  61. data/lib/datadog/di/configuration.rb +6 -0
  62. data/lib/datadog/di/context.rb +9 -3
  63. data/lib/datadog/di/el/compiler.rb +1 -1
  64. data/lib/datadog/di/error.rb +10 -0
  65. data/lib/datadog/di/fatal_exceptions.rb +26 -0
  66. data/lib/datadog/di/instrumenter.rb +412 -165
  67. data/lib/datadog/di/probe.rb +61 -2
  68. data/lib/datadog/di/probe_builder.rb +83 -1
  69. data/lib/datadog/di/probe_file_loader.rb +8 -3
  70. data/lib/datadog/di/probe_manager.rb +67 -9
  71. data/lib/datadog/di/probe_notification_builder.rb +52 -8
  72. data/lib/datadog/di/probe_notifier_worker.rb +52 -32
  73. data/lib/datadog/di/redactor.rb +16 -1
  74. data/lib/datadog/di/remote.rb +176 -15
  75. data/lib/datadog/di/serializer.rb +34 -15
  76. data/lib/datadog/di/transport/input.rb +8 -4
  77. data/lib/datadog/di.rb +63 -0
  78. data/lib/datadog/kit/appsec/events/v2.rb +60 -2
  79. data/lib/datadog/open_feature/component.rb +29 -6
  80. data/lib/datadog/open_feature/configuration.rb +8 -0
  81. data/lib/datadog/open_feature/ext.rb +2 -0
  82. data/lib/datadog/open_feature/flag_evaluation/aggregator.rb +286 -0
  83. data/lib/datadog/open_feature/flag_evaluation/writer.rb +433 -0
  84. data/lib/datadog/open_feature/hooks/flag_eval_evp_hook.rb +101 -0
  85. data/lib/datadog/open_feature/hooks/{flag_eval_hook.rb → flag_eval_metrics_hook.rb} +1 -1
  86. data/lib/datadog/open_feature/native_evaluator.rb +22 -0
  87. data/lib/datadog/open_feature/provider.rb +59 -27
  88. data/lib/datadog/open_feature/transport.rb +55 -1
  89. data/lib/datadog/opentelemetry/sdk/logs_exporter.rb +5 -12
  90. data/lib/datadog/opentelemetry/sdk/metrics_exporter.rb +4 -11
  91. data/lib/datadog/opentelemetry/sdk.rb +19 -0
  92. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +6 -1
  93. data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +5 -3
  94. data/lib/datadog/profiling/collectors/thread_context.rb +9 -1
  95. data/lib/datadog/profiling/component.rb +17 -0
  96. data/lib/datadog/symbol_database/component.rb +164 -47
  97. data/lib/datadog/symbol_database/configuration.rb +12 -4
  98. data/lib/datadog/symbol_database/extensions.rb +19 -0
  99. data/lib/datadog/symbol_database/extractor.rb +124 -87
  100. data/lib/datadog/symbol_database/file_hash.rb +3 -1
  101. data/lib/datadog/symbol_database/remote.rb +8 -3
  102. data/lib/datadog/symbol_database/scope_batcher.rb +7 -3
  103. data/lib/datadog/symbol_database/uploader.rb +9 -2
  104. data/lib/datadog/symbol_database.rb +22 -0
  105. data/lib/datadog/tracing/contrib/active_job/data_streams.rb +62 -0
  106. data/lib/datadog/tracing/contrib/active_job/patcher.rb +12 -0
  107. data/lib/datadog/tracing/contrib/active_model_serializers/events/serialize.rb +2 -3
  108. data/lib/datadog/tracing/contrib/pg/instrumentation.rb +12 -12
  109. data/lib/datadog/tracing/contrib/rack/patcher.rb +1 -1
  110. data/lib/datadog/tracing/contrib/sequel/utils.rb +2 -2
  111. data/lib/datadog/tracing/contrib/sidekiq/server_tracer.rb +1 -1
  112. data/lib/datadog/tracing/contrib/sinatra/framework.rb +1 -1
  113. data/lib/datadog/tracing/distributed/baggage.rb +2 -1
  114. data/lib/datadog/tracing/ext.rb +9 -0
  115. data/lib/datadog/tracing/remote.rb +34 -3
  116. data/lib/datadog/tracing/transport/trace_formatter.rb +1 -1
  117. data/lib/datadog/version.rb +1 -1
  118. data/lib/datadog.rb +1 -0
  119. metadata +25 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fd8fcd94fa20eb591676abd0f96d6ef69f475f10c08e53d7e4dad1c0cab35e7c
4
- data.tar.gz: 6e3036aedb4b47ccb9d8c7ec74d070f52e755796e729d99f83c171ca1879ba59
3
+ metadata.gz: a10640edadba480a4014cb7861ed0eaa21dac0718709266f59fc1307b8fa64bd
4
+ data.tar.gz: cb0b0bcc5423c0e0398277c3535ea52b485967879cf0ede20eedcfb513f1389f
5
5
  SHA512:
6
- metadata.gz: 8de4af357d5807e1855eb4dc078d291af8b601bda3fac1d44d442634dd1b241c7a6b27397095f54a9d931110b503398a43de70878e62c9525629c2b17bcbd97a
7
- data.tar.gz: 6f8acf814a862cfe261b6a3e97f40c37d0cb86b36deeadf253b14c0b17647e570eca9fca2a4d3dc86126a63d9fa5c0d83f750d495d0f6df0e1d23f43493dad6b
6
+ metadata.gz: c77d4b07287c8ee87feb82d541269faa5213861509b99d9e66948022f8c98c005d5b8a8205a263d1d020b8fe8b12306e89fff032d1076f652232133db5c985f2
7
+ data.tar.gz: 5799211f54f368537792d441af52dbd41403da2699f2c503d56b46a533df4b9bef6ef9abf56476f330067c8c555db79a3b1163090cc2267199d937576d210e98
data/CHANGELOG.md CHANGED
@@ -2,6 +2,51 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [2.38.0] - 2026-07-16
6
+
7
+ ### Added
8
+
9
+ * Dynamic Instrumentation: Log probes now support capture expressions as an alternative to capturing the full local/argument scope ([#5845][])
10
+ * SSI: Add support for Ruby 4.0 runtime ([#5981][])
11
+ * SSI: Add support for Bundler 4.0 and RubyGems 4.0 ([#5981][])
12
+
13
+ ### Fixed
14
+
15
+ * AppSec: Fix Rack-based and Rails request body analysis when body parsing fails ([#5993][])
16
+ * Profiling: Add workaround for crash when collecting frame information ([#6014][])
17
+ * Profiling: Fix left-over state impacting samples when profiler gets stopped and started again ([#5960][])
18
+ * Profiling: Fix Ruby 4.0.6 support by bumping minimum version of `datadog-ruby_core_source` to 3.5.3 ([#6047][])
19
+ * Profiling: Disable live heap size profiling on Ruby 4.0 due to incompatibility ([#6022][])
20
+ * Profiling: Disallow heap profiling on Ruby 4.1+ ([#6023][])
21
+ * Tracing: Fix `pg` instrumentation which raised `ArgumentError` when calling `exec_params`, `exec_prepared` without a `params` argument, or their `async_`/`sync_` variants ([#6020][])
22
+
23
+ ## [2.37.0] - 2026-07-06
24
+
25
+ ### Added
26
+
27
+ * AppSec: Add sign-up event tracking to `Datadog::Kit::AppSec::Events::V2` ([#5973][])
28
+ * AppSec: Integrations: Add normalized HTTP route for API Security Testing ([#5834][])
29
+ * Dynamic Instrumentation: DI can now be enabled from the Datadog UI without setting `DD_DYNAMIC_INSTRUMENTATION_ENABLED` or restarting the application. Setting `DD_DYNAMIC_INSTRUMENTATION_ENABLED=false` explicitly still disables it and blocks UI-driven enablement ([#5525][])
30
+
31
+ ### Changed
32
+
33
+ * Profiling: Reduce profiler overhead by sampling profiler-internal threads only once per minute ([#5955][])
34
+ * Dynamic Instrumentation: Reject method probes that target `Kernel#lambda`, which cannot be safely instrumented ([#5954][])
35
+ * Dynamic Instrumentation: Reject method probes that target classes or modules in the Datadog namespace ([#5907][])
36
+ * Dynamic Instrumentation: Enable Symbol Database by default when Dynamic Instrumentation is enabled ([#5828][])
37
+
38
+ ### Fixed
39
+
40
+ * AppSec: AI Guard: Fix endpoint configuration via `DD_SITE` environment variable ([#5940][])
41
+ * AppSec: Fix API Security schema extraction timeout and telemetry ([#5975][])
42
+ * AppSec: Fix remote configuration errors when the ruleset name changes ([#5970][])
43
+ * AppSec: Integrations: Add configurable size limit for downstream HTTP request and response body analysis ([#5990][])
44
+ * AppSec: Integrations: Fix WAF analysis for requests with cached or parsed bodies in Rack and Sinatra ([#5991][])
45
+ * AppSec: Integrations: Fix an exception for unauthorized requests when using Devise with Rails. ([#5987][])
46
+ * Tracing: Fix a `SIGSEGV` crash that could happen with experimental heap profiling enabled on Ruby 4.0 ([#5938][]) ([@navidemad][])
47
+ * Dynamic Instrumentation: Normalize Rack CGI header prefix (`HTTP_<HEADER>`) in redactor so snapshot captures redact sensitive headers like their bare names ([#5911][])
48
+ * Dynamic Instrumentation: Symbol Database and DI background threads no longer stop when application code raises a non-StandardError during introspection ([#5945][])
49
+
5
50
  ## [2.36.0] - 2026-06-24
6
51
 
7
52
  ### Added
@@ -3658,7 +3703,9 @@ Release notes: https://github.com/DataDog/dd-trace-rb/releases/tag/v0.3.1
3658
3703
  Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
3659
3704
 
3660
3705
 
3661
- [Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v2.36.0...master
3706
+ [Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v2.38.0...master
3707
+ [2.38.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.37.0...v2.38.0
3708
+ [2.37.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.36.0...v2.37.0
3662
3709
  [2.36.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.35.0...v2.36.0
3663
3710
  [2.35.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.34.0...v2.35.0
3664
3711
  [2.34.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.33.0...v2.34.0
@@ -5385,6 +5432,7 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
5385
5432
  [#5499]: https://github.com/DataDog/dd-trace-rb/issues/5499
5386
5433
  [#5501]: https://github.com/DataDog/dd-trace-rb/issues/5501
5387
5434
  [#5509]: https://github.com/DataDog/dd-trace-rb/issues/5509
5435
+ [#5525]: https://github.com/DataDog/dd-trace-rb/issues/5525
5388
5436
  [#5531]: https://github.com/DataDog/dd-trace-rb/issues/5531
5389
5437
  [#5564]: https://github.com/DataDog/dd-trace-rb/issues/5564
5390
5438
  [#5569]: https://github.com/DataDog/dd-trace-rb/issues/5569
@@ -5420,9 +5468,12 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
5420
5468
  [#5811]: https://github.com/DataDog/dd-trace-rb/issues/5811
5421
5469
  [#5812]: https://github.com/DataDog/dd-trace-rb/issues/5812
5422
5470
  [#5816]: https://github.com/DataDog/dd-trace-rb/issues/5816
5471
+ [#5828]: https://github.com/DataDog/dd-trace-rb/issues/5828
5423
5472
  [#5830]: https://github.com/DataDog/dd-trace-rb/issues/5830
5473
+ [#5834]: https://github.com/DataDog/dd-trace-rb/issues/5834
5424
5474
  [#5836]: https://github.com/DataDog/dd-trace-rb/issues/5836
5425
5475
  [#5844]: https://github.com/DataDog/dd-trace-rb/issues/5844
5476
+ [#5845]: https://github.com/DataDog/dd-trace-rb/issues/5845
5426
5477
  [#5857]: https://github.com/DataDog/dd-trace-rb/issues/5857
5427
5478
  [#5872]: https://github.com/DataDog/dd-trace-rb/issues/5872
5428
5479
  [#5877]: https://github.com/DataDog/dd-trace-rb/issues/5877
@@ -5430,8 +5481,29 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
5430
5481
  [#5883]: https://github.com/DataDog/dd-trace-rb/issues/5883
5431
5482
  [#5890]: https://github.com/DataDog/dd-trace-rb/issues/5890
5432
5483
  [#5894]: https://github.com/DataDog/dd-trace-rb/issues/5894
5484
+ [#5907]: https://github.com/DataDog/dd-trace-rb/issues/5907
5485
+ [#5911]: https://github.com/DataDog/dd-trace-rb/issues/5911
5433
5486
  [#5916]: https://github.com/DataDog/dd-trace-rb/issues/5916
5434
5487
  [#5920]: https://github.com/DataDog/dd-trace-rb/issues/5920
5488
+ [#5938]: https://github.com/DataDog/dd-trace-rb/issues/5938
5489
+ [#5940]: https://github.com/DataDog/dd-trace-rb/issues/5940
5490
+ [#5945]: https://github.com/DataDog/dd-trace-rb/issues/5945
5491
+ [#5954]: https://github.com/DataDog/dd-trace-rb/issues/5954
5492
+ [#5955]: https://github.com/DataDog/dd-trace-rb/issues/5955
5493
+ [#5960]: https://github.com/DataDog/dd-trace-rb/issues/5960
5494
+ [#5970]: https://github.com/DataDog/dd-trace-rb/issues/5970
5495
+ [#5973]: https://github.com/DataDog/dd-trace-rb/issues/5973
5496
+ [#5975]: https://github.com/DataDog/dd-trace-rb/issues/5975
5497
+ [#5981]: https://github.com/DataDog/dd-trace-rb/issues/5981
5498
+ [#5987]: https://github.com/DataDog/dd-trace-rb/issues/5987
5499
+ [#5990]: https://github.com/DataDog/dd-trace-rb/issues/5990
5500
+ [#5991]: https://github.com/DataDog/dd-trace-rb/issues/5991
5501
+ [#5993]: https://github.com/DataDog/dd-trace-rb/issues/5993
5502
+ [#6014]: https://github.com/DataDog/dd-trace-rb/issues/6014
5503
+ [#6020]: https://github.com/DataDog/dd-trace-rb/issues/6020
5504
+ [#6022]: https://github.com/DataDog/dd-trace-rb/issues/6022
5505
+ [#6023]: https://github.com/DataDog/dd-trace-rb/issues/6023
5506
+ [#6047]: https://github.com/DataDog/dd-trace-rb/issues/6047
5435
5507
  [@AdrianLC]: https://github.com/AdrianLC
5436
5508
  [@Azure7111]: https://github.com/Azure7111
5437
5509
  [@BabyGroot]: https://github.com/BabyGroot
@@ -5546,6 +5618,7 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
5546
5618
  [@mscrivo]: https://github.com/mscrivo
5547
5619
  [@mstruve]: https://github.com/mstruve
5548
5620
  [@mustela]: https://github.com/mustela
5621
+ [@navidemad]: https://github.com/navidemad
5549
5622
  [@nic-lan]: https://github.com/nic-lan
5550
5623
  [@noma4i]: https://github.com/noma4i
5551
5624
  [@norbertnytko]: https://github.com/norbertnytko
@@ -5,7 +5,6 @@
5
5
  #ifdef HAVE_MACH_THREAD_INFO
6
6
 
7
7
  #include <pthread.h>
8
- #include <time.h>
9
8
  #include <mach/mach.h>
10
9
  #include <mach/thread_info.h>
11
10
 
@@ -47,17 +46,11 @@ thread_cpu_time thread_cpu_time_for(thread_cpu_time_id time_id) {
47
46
 
48
47
  if (!time_id.valid) return error;
49
48
 
50
- // Fast path: clock_gettime(CLOCK_THREAD_CPUTIME_ID) is ~5x cheaper than thread_info()
51
- // and gives sub-microsecond precision (vs microsecond), but only measures the calling
52
- // thread on macOS (there is no pthread_getcpuclockid() equivalent).
53
- if (time_id.clock_id == pthread_mach_thread_np(pthread_self())) {
54
- struct timespec ts;
55
- if (clock_gettime(CLOCK_THREAD_CPUTIME_ID, &ts) == 0) {
56
- return (thread_cpu_time) {.valid = true, .result_ns = SECONDS_AS_NS(ts.tv_sec) + ts.tv_nsec};
57
- }
58
- // Fall through to thread_info on the unlikely failure case.
59
- }
60
-
49
+ // We intentionally use thread_info() for all threads, including the current one, even though
50
+ // clock_gettime(CLOCK_THREAD_CPUTIME_ID) is ~5x faster for the current thread. Mixing the two
51
+ // sources causes CPU time to appear to go backwards: thread_info() has microsecond precision
52
+ // while clock_gettime() has nanosecond precision, and comparing values across sources on
53
+ // successive samples produces small negative deltas.
61
54
  struct thread_basic_info info;
62
55
  mach_msg_type_number_t count = THREAD_BASIC_INFO_COUNT;
63
56
  kern_return_t kr = thread_info(time_id.clock_id, THREAD_BASIC_INFO, (thread_info_t)&info, &count);
@@ -154,8 +154,6 @@ typedef struct {
154
154
  unsigned int signal_handler_enqueued_sample;
155
155
  // How many times we prepared a sample (sampled directly) from the signal handler
156
156
  unsigned int signal_handler_prepared_sample;
157
- // How many times the signal handler was called from the wrong thread
158
- unsigned int signal_handler_wrong_thread;
159
157
  // How many times we actually tried to interrupt a thread for sampling
160
158
  unsigned int interrupt_thread_attempts;
161
159
 
@@ -530,11 +528,20 @@ static VALUE _native_sampling_loop(DDTRACE_UNUSED VALUE _self, VALUE instance) {
530
528
  // situation we stop immediately and never even start the sampling trigger loop.
531
529
  if (state->stop_thread == rb_thread_current()) return Qnil;
532
530
 
531
+ thread_context_collector_profiler_internal_thread_started();
532
+
533
533
  // Reset the dynamic sampling rate state, if any (reminder: the monotonic clock reference may change after a fork)
534
534
  dynamic_sampling_rate_reset(&state->cpu_dynamic_sampling_rate);
535
535
  long now = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
536
536
  discrete_dynamic_sampler_reset(&state->allocation_sampler, now);
537
537
 
538
+ // Reset per-thread state, if any. This ensures there's no leftover state from a previous profiler run that would
539
+ // affect or be included in samples taken by this profiler about to run.
540
+ //
541
+ // NOTE: This needs to be called before we enable any tracepoints or anything that could trigger samples (e.g.
542
+ // reset cannot be concurrent with any sampling activity)
543
+ thread_context_collector_reset_all_per_thread_contexts(state->thread_context_collector_instance);
544
+
538
545
  // This write to a global is thread-safe BECAUSE we're still holding on to the global VM lock at this point
539
546
  active_sampler_instance_state = state;
540
547
  active_sampler_instance = instance;
@@ -635,19 +642,19 @@ static VALUE stop(VALUE self_instance, VALUE optional_exception, const char *opt
635
642
  // We need to be careful not to change any state that may be observed OR to restore it if we do. For instance, if anything
636
643
  // we do here can set `errno`, then we must be careful to restore the old `errno` after the fact.
637
644
  static void handle_sampling_signal(DDTRACE_UNUSED int _signal, DDTRACE_UNUSED siginfo_t *_info, DDTRACE_UNUSED void *_ucontext) {
638
- cpu_and_wall_time_worker_state *state = active_sampler_instance_state; // Read from global variable, see "sampler global state safety" note above
639
-
640
- // This can potentially happen if the CpuAndWallTimeWorker was stopped while the signal delivery was happening; nothing to do
641
- if (state == NULL) return;
642
-
645
+ // We must first check that we landed on the correct thread and can proceed.
646
+ // We must never touch the state before we confirm that we have landed on the thread that is holding the GVL on the main
647
+ // ractor as otherwise we may be concurrent with the profiler shutting down and removing its state.
643
648
  if (
644
649
  !ruby_native_thread_p() || // Not a Ruby thread
645
650
  !is_current_thread_holding_the_gvl() || // Not safe to enqueue a sample from this thread
646
651
  !ddtrace_rb_ractor_main_p() // We're not on the main Ractor; we currently don't support profiling non-main Ractors
647
- ) {
648
- state->stats.signal_handler_wrong_thread++;
649
- return;
650
- }
652
+ ) return;
653
+
654
+ cpu_and_wall_time_worker_state *state = active_sampler_instance_state; // Read from global variable, see "sampler global state safety" note above
655
+
656
+ // This can potentially happen if the CpuAndWallTimeWorker was stopped while the signal delivery was happening; nothing to do
657
+ if (state == NULL) return;
651
658
 
652
659
  // We assume there can be no concurrent nor nested calls to handle_sampling_signal because
653
660
  // a) we get triggered using SIGPROF, and the docs state a second SIGPROF will not interrupt an existing one (see sigaction docs on sa_mask)
@@ -893,6 +900,8 @@ static VALUE release_gvl_and_run_sampling_trigger_loop(VALUE instance) {
893
900
  // If we stopped sampling due to an exception, re-raise it (now in the worker thread)
894
901
  if (state->failure_exception != Qnil) rb_exc_raise(state->failure_exception);
895
902
 
903
+ thread_context_collector_profiler_internal_thread_done(state->thread_context_collector_instance);
904
+
896
905
  return Qnil;
897
906
  }
898
907
 
@@ -1100,7 +1109,6 @@ static VALUE _native_stats(DDTRACE_UNUSED VALUE self, VALUE instance) {
1100
1109
  ID2SYM(rb_intern("simulated_signal_delivery")), /* => */ UINT2NUM(state->stats.simulated_signal_delivery),
1101
1110
  ID2SYM(rb_intern("signal_handler_enqueued_sample")), /* => */ UINT2NUM(state->stats.signal_handler_enqueued_sample),
1102
1111
  ID2SYM(rb_intern("signal_handler_prepared_sample")), /* => */ UINT2NUM(state->stats.signal_handler_prepared_sample),
1103
- ID2SYM(rb_intern("signal_handler_wrong_thread")), /* => */ UINT2NUM(state->stats.signal_handler_wrong_thread),
1104
1112
  ID2SYM(rb_intern("interrupt_thread_attempts")), /* => */ UINT2NUM(state->stats.interrupt_thread_attempts),
1105
1113
 
1106
1114
  // CPU Stats
@@ -1267,9 +1275,8 @@ static void on_newobj_event(DDTRACE_UNUSED VALUE unused1, DDTRACE_UNUSED void *u
1267
1275
 
1268
1276
  per_thread_context *thread_context = get_per_thread_context(current_thread);
1269
1277
  if (!thread_context) {
1270
- // No per_thread_context yet on this Thread, we can't use get_or_create_context_for() as that allocates,
1271
- // and we are inside on_newobj_event where we MUST NOT allocate.
1272
- // So we don't sample allocations until another hook allocates the per_thread_context.
1278
+ // Context is created eagerly via on_thread_begin_event, so this should not normally be NULL.
1279
+ // We keep the guard since we can't allocate here (inside on_newobj_event).
1273
1280
  return;
1274
1281
  }
1275
1282
 
@@ -1434,17 +1441,21 @@ static VALUE _native_resume_signals(DDTRACE_UNUSED VALUE self) {
1434
1441
  static void on_gvl_event(rb_event_flag_t event_id, const rb_internal_thread_event_data_t *event_data, DDTRACE_UNUSED void *_unused) {
1435
1442
  // Be very careful about touching the `state` here or doing anything at all:
1436
1443
  // This function gets called without the GVL, and potentially from non-main Ractors!
1444
+ //
1445
+ // Note, even though these events can get called without the GVL, they synchronize-with enabling/disabling of
1446
+ // the hook that calls us using a read-write lock. Thus, disabling the hook cannot be concurrent with calling this function,
1447
+ // and once the disable finishes there can't be "late" calls into this function.
1437
1448
 
1438
1449
  // The thread that this event is about may not be the current thread
1439
1450
  // (as documented on rb_internal_thread_add_event_hook(), and this is notably the case for READY on Ruby 4.0),
1440
1451
  // so be careful with native thread locals that are not directly tied to the thread object and the like.
1441
1452
 
1442
- // On Ruby 3.2 the event does not carry the thread, but all events always fire on the event thread on Ruby 3.2.
1443
- // However, during early thread startup rb_thread_current() can crash because the execution context (Fiber) isn't
1444
- // stored in TLS yet; ruby_native_thread_p() guards against this.
1445
1453
  #ifdef HAVE_RUBY_THREAD_STORAGE_API
1446
1454
  VALUE target_thread = event_data->thread;
1447
1455
  #else
1456
+ // On Ruby 3.2 the event does not carry the thread, but all events fire on the thread itself.
1457
+ // However, during early thread startup rb_thread_current() can crash because the execution context (Fiber) isn't
1458
+ // stored in TLS yet; ruby_native_thread_p() guards against this.
1448
1459
  if (!ruby_native_thread_p()) return;
1449
1460
  VALUE target_thread = rb_thread_current();
1450
1461
  #endif
@@ -6,6 +6,7 @@
6
6
  #include "helpers.h"
7
7
  #include "ruby_helpers.h"
8
8
  #include "collectors_idle_sampling_helper.h"
9
+ #include "collectors_thread_context.h"
9
10
 
10
11
  // Used by the Collectors::CpuAndWallTimeWorker to gather samples when the Ruby process is idle.
11
12
  //
@@ -30,7 +31,9 @@ typedef struct {
30
31
 
31
32
  static VALUE _native_new(VALUE klass);
32
33
  static void reset_state(idle_sampling_loop_state *state);
33
- static VALUE _native_idle_sampling_loop(DDTRACE_UNUSED VALUE self, VALUE self_instance);
34
+ static VALUE _native_idle_sampling_loop(DDTRACE_UNUSED VALUE self, VALUE self_instance, VALUE thread_context_collector_instance);
35
+ static VALUE idle_sampling_loop_body(VALUE self_instance);
36
+ static VALUE idle_sampling_loop_ensure(VALUE thread_context_collector_instance);
34
37
  static VALUE _native_stop(DDTRACE_UNUSED VALUE self, VALUE self_instance);
35
38
  static void *run_idle_sampling_loop(void *state_ptr);
36
39
  static void interrupt_idle_sampling_loop(void *state_ptr);
@@ -56,7 +59,7 @@ void collectors_idle_sampling_helper_init(VALUE profiling_module) {
56
59
  // https://bugs.ruby-lang.org/issues/18007 for a discussion around this.
57
60
  rb_define_alloc_func(collectors_idle_sampling_helper_class, _native_new);
58
61
 
59
- rb_define_singleton_method(collectors_idle_sampling_helper_class, "_native_idle_sampling_loop", _native_idle_sampling_loop, 1);
62
+ rb_define_singleton_method(collectors_idle_sampling_helper_class, "_native_idle_sampling_loop", _native_idle_sampling_loop, 2);
60
63
  rb_define_singleton_method(collectors_idle_sampling_helper_class, "_native_stop", _native_stop, 1);
61
64
  rb_define_singleton_method(collectors_idle_sampling_helper_class, "_native_reset", _native_reset, 1);
62
65
  rb_define_singleton_method(testing_module, "_native_idle_sampling_helper_request_action", _native_idle_sampling_helper_request_action, 1);
@@ -109,16 +112,27 @@ static VALUE _native_reset(DDTRACE_UNUSED VALUE self, VALUE self_instance) {
109
112
  return Qtrue;
110
113
  }
111
114
 
112
- static VALUE _native_idle_sampling_loop(DDTRACE_UNUSED VALUE self, VALUE self_instance) {
115
+ static VALUE _native_idle_sampling_loop(DDTRACE_UNUSED VALUE self, VALUE self_instance, VALUE thread_context_collector_instance) {
116
+ return rb_ensure(idle_sampling_loop_body, self_instance, idle_sampling_loop_ensure, thread_context_collector_instance);
117
+ }
118
+
119
+ static VALUE idle_sampling_loop_body(VALUE self_instance) {
113
120
  idle_sampling_loop_state *state;
114
121
  TypedData_Get_Struct(self_instance, idle_sampling_loop_state, &idle_sampling_helper_typed_data, state);
115
122
 
123
+ thread_context_collector_profiler_internal_thread_started();
124
+
116
125
  // Release GVL and run the loop waiting for requests
117
126
  rb_thread_call_without_gvl(run_idle_sampling_loop, state, interrupt_idle_sampling_loop, state);
118
127
 
119
128
  return Qtrue;
120
129
  }
121
130
 
131
+ static VALUE idle_sampling_loop_ensure(VALUE thread_context_collector_instance) {
132
+ thread_context_collector_profiler_internal_thread_done(thread_context_collector_instance);
133
+ return Qnil;
134
+ }
135
+
122
136
  static void *run_idle_sampling_loop(void *state_ptr) {
123
137
  idle_sampling_loop_state *state = (idle_sampling_loop_state *) state_ptr;
124
138
  int error = 0;
@@ -251,22 +251,26 @@ void sample_thread(
251
251
  bool native_filenames_enabled,
252
252
  st_table *native_filenames_cache
253
253
  ) {
254
+ if (buffer->max_frames != locations.len) {
255
+ // This shouldn't happen as thread_context_collector_reset_all_per_thread_contexts (which resizes every
256
+ // per-thread sampling buffer to match the collector's max_frames) must always be called before starting or
257
+ // restarting profiling.
258
+ raise_error(
259
+ rb_eRuntimeError,
260
+ "Unexpected: sampling buffer max_frames (%d) doesn't match locations len (%d)",
261
+ (int) buffer->max_frames,
262
+ (int) locations.len
263
+ );
264
+ }
265
+
254
266
  // If we already prepared a sample, we use it below; if not, we prepare it now.
255
267
  if (!buffer->pending_sample) {
256
- // Reconcile the sampling_buffer's max_frames with the locations size
257
- if (buffer->max_frames != locations.len) {
258
- sampling_buffer_reinitialize(buffer, locations.len);
259
- }
260
268
  prepare_sample_thread(thread, buffer);
261
269
  }
262
270
 
263
271
  buffer->pending_sample = false;
264
272
  int captured_frames = buffer->pending_sample_result;
265
273
 
266
- // The per_thread_context's sampling_buffer may have been created by a previous collector with a
267
- // different (larger) max_frames. Cap to the locations array size to prevent out-of-bounds writes.
268
- if (captured_frames > (int) locations.len) captured_frames = (int) locations.len;
269
-
270
274
  if (captured_frames == PLACEHOLDER_STACK_IN_NATIVE_CODE) {
271
275
  record_placeholder_stack_in_native_code(recorder_instance, values, labels);
272
276
  return;
@@ -312,8 +316,13 @@ void sample_thread(
312
316
  bool top_of_the_stack = i == top_of_stack_position;
313
317
 
314
318
  if (buffer->stack_buffer[i].is_ruby_frame) {
315
- VALUE name = rb_iseq_base_label(buffer->stack_buffer[i].as.ruby_frame.iseq);
316
- VALUE filename = rb_iseq_path(buffer->stack_buffer[i].as.ruby_frame.iseq);
319
+ VALUE iseq = buffer->stack_buffer[i].as.ruby_frame.iseq;
320
+ VALUE name = rb_iseq_base_label(iseq);
321
+ VALUE filename =
322
+ // Note: We saw crash reports from a customer from dereferencing a null pointer inside `rb_iseq_path`.
323
+ // It's not clear in what situation would the pathobj ever be NULL, yet it seems harmless from our side to
324
+ // skip this information if we're ever in such a situation.
325
+ pathobj_is_null(iseq) ? Qnil : rb_iseq_path(iseq);
317
326
 
318
327
  name_slice = NIL_P(name) ? DDOG_CHARSLICE_C("") : char_slice_from_ruby_string(name);
319
328
  filename_slice = NIL_P(filename) ? DDOG_CHARSLICE_C("") : char_slice_from_ruby_string(filename);
@@ -420,11 +429,6 @@ void sample_thread(
420
429
  values,
421
430
  labels
422
431
  );
423
-
424
- // Reconcile the sampling_buffer's max_frames with the locations size for future samples
425
- if (buffer->max_frames != locations.len) {
426
- sampling_buffer_reinitialize(buffer, locations.len);
427
- }
428
432
  }
429
433
 
430
434
  #if (defined(HAVE_DLADDR1) && HAVE_DLADDR1) || (defined(HAVE_DLADDR) && HAVE_DLADDR)
@@ -645,16 +649,7 @@ void sampling_buffer_initialize(sampling_buffer *buffer, uint16_t max_frames) {
645
649
  buffer->pending_sample_result = 0;
646
650
  }
647
651
 
648
- void sampling_buffer_reinitialize(sampling_buffer *buffer, uint16_t max_frames) {
649
- sampling_buffer_free(buffer);
650
- sampling_buffer_initialize(buffer, max_frames);
651
- }
652
-
653
652
  void sampling_buffer_free(sampling_buffer *buffer) {
654
- if (buffer->max_frames == 0 || buffer->stack_buffer == NULL) {
655
- raise_error(rb_eArgError, "sampling_buffer_free called with invalid buffer");
656
- }
657
-
658
653
  ruby_xfree(buffer->stack_buffer);
659
654
 
660
655
  buffer->max_frames = 0;
@@ -42,7 +42,6 @@ bool prepare_sample_thread(VALUE thread, sampling_buffer *buffer);
42
42
 
43
43
  uint16_t sampling_buffer_check_max_frames(int max_frames);
44
44
  void sampling_buffer_initialize(sampling_buffer *buffer, uint16_t max_frames);
45
- void sampling_buffer_reinitialize(sampling_buffer *buffer, uint16_t max_frames);
46
45
  void sampling_buffer_free(sampling_buffer *buffer);
47
46
  void sampling_buffer_mark(sampling_buffer *buffer);
48
47
  static inline bool sampling_buffer_needs_marking(sampling_buffer *buffer) {