datadog 2.23.0 → 2.25.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 (145) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +67 -1
  3. data/ext/datadog_profiling_native_extension/collectors_stack.c +17 -5
  4. data/ext/datadog_profiling_native_extension/crashtracking_runtime_stacks.c +239 -0
  5. data/ext/datadog_profiling_native_extension/extconf.rb +4 -1
  6. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +12 -0
  7. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +4 -0
  8. data/ext/datadog_profiling_native_extension/profiling.c +2 -0
  9. data/lib/datadog/ai_guard/api_client.rb +82 -0
  10. data/lib/datadog/ai_guard/component.rb +42 -0
  11. data/lib/datadog/ai_guard/configuration/ext.rb +17 -0
  12. data/lib/datadog/ai_guard/configuration/settings.rb +98 -0
  13. data/lib/datadog/ai_guard/configuration.rb +11 -0
  14. data/lib/datadog/ai_guard/evaluation/message.rb +25 -0
  15. data/lib/datadog/ai_guard/evaluation/no_op_result.rb +34 -0
  16. data/lib/datadog/ai_guard/evaluation/request.rb +81 -0
  17. data/lib/datadog/ai_guard/evaluation/result.rb +43 -0
  18. data/lib/datadog/ai_guard/evaluation/tool_call.rb +18 -0
  19. data/lib/datadog/ai_guard/evaluation.rb +72 -0
  20. data/lib/datadog/ai_guard/ext.rb +16 -0
  21. data/lib/datadog/ai_guard.rb +153 -0
  22. data/lib/datadog/appsec/context.rb +2 -1
  23. data/lib/datadog/appsec/remote.rb +5 -12
  24. data/lib/datadog/appsec/security_engine/engine.rb +3 -3
  25. data/lib/datadog/appsec/security_engine/result.rb +2 -1
  26. data/lib/datadog/appsec/security_engine/runner.rb +2 -2
  27. data/lib/datadog/core/configuration/components.rb +6 -0
  28. data/lib/datadog/core/configuration/config_helper.rb +1 -1
  29. data/lib/datadog/core/configuration/deprecations.rb +2 -2
  30. data/lib/datadog/core/configuration/option_definition.rb +4 -2
  31. data/lib/datadog/core/configuration/options.rb +8 -5
  32. data/lib/datadog/core/configuration/settings.rb +14 -3
  33. data/lib/datadog/core/configuration/supported_configurations.rb +8 -1
  34. data/lib/datadog/core/environment/cgroup.rb +52 -25
  35. data/lib/datadog/core/environment/container.rb +140 -46
  36. data/lib/datadog/core/environment/ext.rb +1 -0
  37. data/lib/datadog/core/environment/process.rb +9 -1
  38. data/lib/datadog/core/error.rb +6 -6
  39. data/lib/datadog/core/pin.rb +4 -0
  40. data/lib/datadog/core/rate_limiter.rb +9 -1
  41. data/lib/datadog/core/remote/client.rb +14 -6
  42. data/lib/datadog/core/remote/component.rb +6 -4
  43. data/lib/datadog/core/remote/configuration/content.rb +15 -2
  44. data/lib/datadog/core/remote/configuration/digest.rb +14 -7
  45. data/lib/datadog/core/remote/configuration/repository.rb +1 -1
  46. data/lib/datadog/core/remote/configuration/target.rb +13 -6
  47. data/lib/datadog/core/remote/transport/config.rb +3 -16
  48. data/lib/datadog/core/remote/transport/http/config.rb +4 -44
  49. data/lib/datadog/core/remote/transport/http/negotiation.rb +0 -39
  50. data/lib/datadog/core/remote/transport/http.rb +13 -24
  51. data/lib/datadog/core/remote/transport/negotiation.rb +7 -16
  52. data/lib/datadog/core/semaphore.rb +1 -4
  53. data/lib/datadog/core/telemetry/component.rb +52 -13
  54. data/lib/datadog/core/telemetry/event/app_started.rb +36 -1
  55. data/lib/datadog/core/telemetry/event/synth_app_client_configuration_change.rb +27 -4
  56. data/lib/datadog/core/telemetry/metrics_manager.rb +9 -0
  57. data/lib/datadog/core/telemetry/request.rb +17 -3
  58. data/lib/datadog/core/telemetry/transport/http/telemetry.rb +2 -32
  59. data/lib/datadog/core/telemetry/transport/http.rb +21 -16
  60. data/lib/datadog/core/telemetry/transport/telemetry.rb +3 -10
  61. data/lib/datadog/core/telemetry/worker.rb +88 -32
  62. data/lib/datadog/core/transport/ext.rb +2 -0
  63. data/lib/datadog/core/transport/http/api/endpoint.rb +9 -4
  64. data/lib/datadog/core/transport/http/api/instance.rb +4 -21
  65. data/lib/datadog/core/transport/http/builder.rb +9 -5
  66. data/lib/datadog/core/transport/http/client.rb +19 -8
  67. data/lib/datadog/core/transport/http.rb +22 -19
  68. data/lib/datadog/core/transport/response.rb +12 -1
  69. data/lib/datadog/core/transport/transport.rb +90 -0
  70. data/lib/datadog/core/utils/only_once_successful.rb +2 -0
  71. data/lib/datadog/core/utils/safe_dup.rb +2 -2
  72. data/lib/datadog/core/utils/sequence.rb +2 -0
  73. data/lib/datadog/core/utils/time.rb +1 -1
  74. data/lib/datadog/core/workers/async.rb +10 -1
  75. data/lib/datadog/core/workers/interval_loop.rb +44 -3
  76. data/lib/datadog/core/workers/polling.rb +2 -0
  77. data/lib/datadog/core/workers/queue.rb +100 -1
  78. data/lib/datadog/data_streams/processor.rb +1 -1
  79. data/lib/datadog/data_streams/transport/http/stats.rb +1 -36
  80. data/lib/datadog/data_streams/transport/http.rb +5 -6
  81. data/lib/datadog/data_streams/transport/stats.rb +3 -17
  82. data/lib/datadog/di/boot.rb +4 -2
  83. data/lib/datadog/di/contrib/active_record.rb +30 -5
  84. data/lib/datadog/di/el/compiler.rb +8 -4
  85. data/lib/datadog/di/error.rb +5 -0
  86. data/lib/datadog/di/instrumenter.rb +26 -7
  87. data/lib/datadog/di/logger.rb +2 -2
  88. data/lib/datadog/di/probe_builder.rb +2 -1
  89. data/lib/datadog/di/probe_file_loader/railtie.rb +1 -1
  90. data/lib/datadog/di/probe_manager.rb +37 -31
  91. data/lib/datadog/di/probe_notification_builder.rb +15 -2
  92. data/lib/datadog/di/probe_notifier_worker.rb +5 -5
  93. data/lib/datadog/di/remote.rb +89 -84
  94. data/lib/datadog/di/transport/diagnostics.rb +7 -35
  95. data/lib/datadog/di/transport/http/diagnostics.rb +1 -31
  96. data/lib/datadog/di/transport/http/input.rb +1 -31
  97. data/lib/datadog/di/transport/http.rb +28 -17
  98. data/lib/datadog/di/transport/input.rb +7 -34
  99. data/lib/datadog/di.rb +61 -5
  100. data/lib/datadog/error_tracking/filters.rb +2 -2
  101. data/lib/datadog/kit/appsec/events/v2.rb +2 -3
  102. data/lib/datadog/open_feature/evaluation_engine.rb +2 -1
  103. data/lib/datadog/open_feature/remote.rb +3 -10
  104. data/lib/datadog/open_feature/transport.rb +9 -11
  105. data/lib/datadog/opentelemetry/api/baggage.rb +1 -1
  106. data/lib/datadog/opentelemetry/configuration/settings.rb +2 -2
  107. data/lib/datadog/opentelemetry/metrics.rb +21 -14
  108. data/lib/datadog/opentelemetry/sdk/metrics_exporter.rb +5 -8
  109. data/lib/datadog/profiling/collectors/code_provenance.rb +27 -2
  110. data/lib/datadog/profiling/collectors/info.rb +5 -4
  111. data/lib/datadog/profiling/component.rb +12 -11
  112. data/lib/datadog/profiling/ext/dir_monkey_patches.rb +18 -0
  113. data/lib/datadog/profiling/http_transport.rb +4 -1
  114. data/lib/datadog/tracing/contrib/extensions.rb +10 -2
  115. data/lib/datadog/tracing/contrib/karafka/patcher.rb +31 -32
  116. data/lib/datadog/tracing/contrib/status_range_matcher.rb +2 -1
  117. data/lib/datadog/tracing/contrib/utils/quantization/hash.rb +3 -1
  118. data/lib/datadog/tracing/contrib/waterdrop/patcher.rb +6 -3
  119. data/lib/datadog/tracing/contrib/waterdrop.rb +4 -0
  120. data/lib/datadog/tracing/diagnostics/environment_logger.rb +1 -1
  121. data/lib/datadog/tracing/distributed/baggage.rb +3 -2
  122. data/lib/datadog/tracing/remote.rb +1 -9
  123. data/lib/datadog/tracing/sampling/priority_sampler.rb +3 -1
  124. data/lib/datadog/tracing/span.rb +1 -1
  125. data/lib/datadog/tracing/span_event.rb +2 -2
  126. data/lib/datadog/tracing/span_operation.rb +20 -9
  127. data/lib/datadog/tracing/trace_operation.rb +44 -6
  128. data/lib/datadog/tracing/tracer.rb +42 -16
  129. data/lib/datadog/tracing/transport/http/traces.rb +2 -50
  130. data/lib/datadog/tracing/transport/http.rb +15 -9
  131. data/lib/datadog/tracing/transport/io/client.rb +1 -1
  132. data/lib/datadog/tracing/transport/traces.rb +6 -66
  133. data/lib/datadog/tracing/workers/trace_writer.rb +5 -0
  134. data/lib/datadog/tracing/writer.rb +1 -0
  135. data/lib/datadog/version.rb +2 -2
  136. data/lib/datadog.rb +1 -0
  137. metadata +24 -17
  138. data/lib/datadog/core/remote/transport/http/api.rb +0 -53
  139. data/lib/datadog/core/telemetry/transport/http/api.rb +0 -43
  140. data/lib/datadog/core/transport/http/api/spec.rb +0 -36
  141. data/lib/datadog/data_streams/transport/http/api.rb +0 -33
  142. data/lib/datadog/data_streams/transport/http/client.rb +0 -21
  143. data/lib/datadog/di/transport/http/api.rb +0 -42
  144. data/lib/datadog/opentelemetry/api/baggage.rbs +0 -26
  145. data/lib/datadog/tracing/transport/http/api.rb +0 -44
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a168735b19c403a74cd23f49a301a74006d827ff24f62cc93255a353d0415359
4
- data.tar.gz: 7f6df03ec7834bf5709b36d8fa9a8213ad5c5f9bf04e299acb7d3c1e781760e7
3
+ metadata.gz: b31bd418a350c8999b821462c0699a19af1b02242a7a22f4fee10859cd520d4c
4
+ data.tar.gz: 4a77d7cb714bfb82312d642b583d23f7b9407435c6577b864f1ca98751826ec6
5
5
  SHA512:
6
- metadata.gz: a933fb99fd374b1da93fd0a70b7cb33b2396977e8388ebfd5e12d968da7158ef8fc23a73672c308f8a72e433525c99ac1182285adc2df1eb244082efeef374bb
7
- data.tar.gz: 224bbcc48cde29a3a85c1eea9d5e1653ed26ad2d195389e599f2e4614158d63284c92fbf155cbd7bf584930195d4b30b7bf5a8a1fd6951ac798c1d3a1b35969b
6
+ metadata.gz: f51a7d85ab5e057e5a485f7dffe7d329b4aacde56d784239a15dbb8a48508d082ba69dbecb992336d132965b642ccdf39cd4f4e2378d749174cab64496b1fe2f
7
+ data.tar.gz: 83504124b5fad578d31002d5ab4d19450c14c8a857b7b553925bbff2cb2fdb53ee2dbeb2e7c774128c92203e69b3c484896591435ec4df01740e2028b70ff7fe
data/CHANGELOG.md CHANGED
@@ -2,6 +2,51 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [2.25.0] - 2026-01-13
6
+
7
+ ### Added
8
+
9
+ AI Guard: Add SDK for evaluating the safety of user messages and assistant commands for LLM session ([#5144][])
10
+
11
+ ### Changed
12
+
13
+ Core: Bump minimum version of `datadog-ruby_core_source` dependency ([#5215][])
14
+
15
+ ### Fixed
16
+
17
+ AppSec: Fix processing of numeric data for WAF and RASP checks ([#5222][])
18
+
19
+ ## [2.24.0] - 2026-01-08
20
+
21
+ ### Added
22
+
23
+ * Core: Add support for installing the gem on Ruby 4.0.x stable ([#5157][])
24
+ * Tracing: Add origin detection using extra headers and the `DD_EXTERNAL_ENV` variable. ([#5028][])
25
+ * Dynamic Instrumentation: Add one-click enablement support ([#5150][])
26
+ * SSI: Add support for Bundler deployment mode ([#5053][])
27
+ * SSI: Report UI-oriented injection results ([#5053][])
28
+ * SSI: Guard against Bundler global force_ruby_platform ([#5053][])
29
+ * SSI: Guard against Bundler 4.0 and Bundler 2.7 in 4.0 mode ([#5053][])
30
+ * SSI: Guard against Ruby 3.5+ ([#5053][])
31
+
32
+ ### Changed
33
+
34
+ * Profiling: Remove profiler warning related to the Ractor issue ([#5194][])
35
+ * Profiling: Disable heap profiling on Ruby 4 due to incompatibility ([#5148][])
36
+ * Dynamic Instrumentation: Stop using customer-provided time provider for method duration calculation ([#5153][])
37
+ * Live Debugger / Dynamic Instrumentation: Improve probe instrumentation ([#5165][])
38
+ * Live Debugger / Dynamic Instrumentation: Improve instrumentation reliability for probes ([#5169][])
39
+
40
+ ### Fixed
41
+
42
+ * Core: Improve reliability of worker shutdown ([#5176][])
43
+ * Core: Fix RDoc error when installing the `datadog` gem ([#5145][])
44
+ * Tracing: Ensure `Tracing.continue_from!` keeps the active trace for the full block duration. ([#4941][])
45
+ * Profiling: Fix and refine profiler thread state categorization for Ruby 4 ([#5197][])
46
+ * Profiling: Fix profiler error triggering `Bundler::PermissionError` ([#5146][])
47
+ * Live Debugger / Dynamic Instrumentation: Fix Live Debugger and Dynamic Instrumentation UI for forking web servers ([#5159][])
48
+ * Live Debugger / Dynamic Instrumentation: Fix method probe leak when a referenced class loads after the probe reaches the application ([#5168][])
49
+
5
50
  ## [2.23.0] - 2025-12-11
6
51
 
7
52
  ### Added
@@ -3392,7 +3437,9 @@ Release notes: https://github.com/DataDog/dd-trace-rb/releases/tag/v0.3.1
3392
3437
  Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
3393
3438
 
3394
3439
 
3395
- [Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v2.23.0...master
3440
+ [Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v2.25.0...master
3441
+ [2.25.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.24.0...v2.25.0
3442
+ [2.24.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.23.0...v2.24.0
3396
3443
  [2.23.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.22.0...v2.23.0
3397
3444
  [2.22.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.21.0...v2.22.0
3398
3445
  [2.21.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.20.0...v2.21.0
@@ -5010,6 +5057,7 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
5010
5057
  [#4914]: https://github.com/DataDog/dd-trace-rb/issues/4914
5011
5058
  [#4918]: https://github.com/DataDog/dd-trace-rb/issues/4918
5012
5059
  [#4919]: https://github.com/DataDog/dd-trace-rb/issues/4919
5060
+ [#4941]: https://github.com/DataDog/dd-trace-rb/issues/4941
5013
5061
  [#4957]: https://github.com/DataDog/dd-trace-rb/issues/4957
5014
5062
  [#4965]: https://github.com/DataDog/dd-trace-rb/issues/4965
5015
5063
  [#4969]: https://github.com/DataDog/dd-trace-rb/issues/4969
@@ -5024,18 +5072,36 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
5024
5072
  [#5021]: https://github.com/DataDog/dd-trace-rb/issues/5021
5025
5073
  [#5024]: https://github.com/DataDog/dd-trace-rb/issues/5024
5026
5074
  [#5025]: https://github.com/DataDog/dd-trace-rb/issues/5025
5075
+ [#5028]: https://github.com/DataDog/dd-trace-rb/issues/5028
5027
5076
  [#5031]: https://github.com/DataDog/dd-trace-rb/issues/5031
5028
5077
  [#5033]: https://github.com/DataDog/dd-trace-rb/issues/5033
5029
5078
  [#5042]: https://github.com/DataDog/dd-trace-rb/issues/5042
5030
5079
  [#5044]: https://github.com/DataDog/dd-trace-rb/issues/5044
5031
5080
  [#5045]: https://github.com/DataDog/dd-trace-rb/issues/5045
5032
5081
  [#5049]: https://github.com/DataDog/dd-trace-rb/issues/5049
5082
+ [#5053]: https://github.com/DataDog/dd-trace-rb/issues/5053
5033
5083
  [#5054]: https://github.com/DataDog/dd-trace-rb/issues/5054
5034
5084
  [#5058]: https://github.com/DataDog/dd-trace-rb/issues/5058
5035
5085
  [#5073]: https://github.com/DataDog/dd-trace-rb/issues/5073
5036
5086
  [#5086]: https://github.com/DataDog/dd-trace-rb/issues/5086
5037
5087
  [#5091]: https://github.com/DataDog/dd-trace-rb/issues/5091
5038
5088
  [#5122]: https://github.com/DataDog/dd-trace-rb/issues/5122
5089
+ [#5144]: https://github.com/DataDog/dd-trace-rb/issues/5144
5090
+ [#5145]: https://github.com/DataDog/dd-trace-rb/issues/5145
5091
+ [#5146]: https://github.com/DataDog/dd-trace-rb/issues/5146
5092
+ [#5148]: https://github.com/DataDog/dd-trace-rb/issues/5148
5093
+ [#5150]: https://github.com/DataDog/dd-trace-rb/issues/5150
5094
+ [#5153]: https://github.com/DataDog/dd-trace-rb/issues/5153
5095
+ [#5157]: https://github.com/DataDog/dd-trace-rb/issues/5157
5096
+ [#5159]: https://github.com/DataDog/dd-trace-rb/issues/5159
5097
+ [#5165]: https://github.com/DataDog/dd-trace-rb/issues/5165
5098
+ [#5168]: https://github.com/DataDog/dd-trace-rb/issues/5168
5099
+ [#5169]: https://github.com/DataDog/dd-trace-rb/issues/5169
5100
+ [#5176]: https://github.com/DataDog/dd-trace-rb/issues/5176
5101
+ [#5194]: https://github.com/DataDog/dd-trace-rb/issues/5194
5102
+ [#5197]: https://github.com/DataDog/dd-trace-rb/issues/5197
5103
+ [#5215]: https://github.com/DataDog/dd-trace-rb/issues/5215
5104
+ [#5222]: https://github.com/DataDog/dd-trace-rb/issues/5222
5039
5105
  [@AdrianLC]: https://github.com/AdrianLC
5040
5106
  [@Azure7111]: https://github.com/Azure7111
5041
5107
  [@BabyGroot]: https://github.com/BabyGroot
@@ -347,8 +347,10 @@ void sample_thread(
347
347
  } else if (CHARSLICE_EQUALS("select", name_slice)) { // Expected to be Kernel.select
348
348
  state_label->str = DDOG_CHARSLICE_C("waiting");
349
349
  } else if (
350
- CHARSLICE_EQUALS("synchronize", name_slice) || // Expected to be Monitor/Mutex#synchronize
351
- CHARSLICE_EQUALS("lock", name_slice) || // Expected to be Mutex#lock
350
+ CHARSLICE_EQUALS("synchronize", name_slice) || // Expected to be Monitor/Mutex#synchronize on Ruby 2 & 3, and Monitor#synchronize on 4 (Mutex becomes <internal:thread_sync>)
351
+ #ifdef NO_PRIMITIVE_MUTEX_AND_CONDITION_VARIABLE // Ruby < 4
352
+ CHARSLICE_EQUALS("lock", name_slice) || // Expected to be Mutex#lock
353
+ #endif
352
354
  CHARSLICE_EQUALS("join", name_slice) // Expected to be Thread#join
353
355
  ) {
354
356
  state_label->str = DDOG_CHARSLICE_C("blocked");
@@ -366,9 +368,19 @@ void sample_thread(
366
368
  #endif
367
369
  } else {
368
370
  #ifndef NO_PRIMITIVE_POP // Ruby >= 3.2
369
- // Unlike the above, Ruby actually treats this one specially and gives it a nice file name we can match on!
370
- if (CHARSLICE_EQUALS("pop", name_slice) && CHARSLICE_EQUALS("<internal:thread_sync>", filename_slice)) { // Expected to be Queue/SizedQueue#pop
371
- state_label->str = DDOG_CHARSLICE_C("waiting");
371
+ if (CHARSLICE_EQUALS("<internal:thread_sync>", filename_slice)) {
372
+ if (CHARSLICE_EQUALS("pop", name_slice)) { // Expected to be Queue/SizedQueue#pop
373
+ state_label->str = DDOG_CHARSLICE_C("waiting");
374
+ }
375
+ #ifndef NO_PRIMITIVE_MUTEX_AND_CONDITION_VARIABLE // Ruby >= 4
376
+ else if (CHARSLICE_EQUALS("synchronize", name_slice) || CHARSLICE_EQUALS("lock", name_slice)) { // Expected to be Mutex#lock/synchronize
377
+ state_label->str = DDOG_CHARSLICE_C("blocked");
378
+ } else if (CHARSLICE_EQUALS("sleep", name_slice)) { // Expected to be Mutex#sleep
379
+ state_label->str = DDOG_CHARSLICE_C("sleeping");
380
+ } else if (CHARSLICE_EQUALS("wait", name_slice)) { // Expected to be ConditionVariable#wait
381
+ state_label->str = DDOG_CHARSLICE_C("waiting");
382
+ }
383
+ #endif
372
384
  }
373
385
  #endif
374
386
  }
@@ -0,0 +1,239 @@
1
+ // NOTE: This file is a part of the profiling native extension even though the
2
+ // runtime stacks feature is consumed by the crashtracker. The profiling
3
+ // extension already carries all the Ruby VM private header access and build
4
+ // plumbing required to safely poke at internal structures. Sharing that setup
5
+ // avoids duplicating another native extension with the same (fragile) access
6
+ // patterns, and keeps the overall install/build surface area smaller.
7
+ //
8
+ // This also means that this functionality is depend on the profiling native extension
9
+ // being available and built
10
+ #include "extconf.h"
11
+
12
+ #if defined(__linux__)
13
+
14
+ #include <datadog/crashtracker.h>
15
+ #include "datadog_ruby_common.h"
16
+ #include "private_vm_api_access.h"
17
+ #include <sys/mman.h>
18
+ #include <unistd.h>
19
+ #include <errno.h>
20
+ #include <string.h>
21
+
22
+ static const rb_data_type_t *crashtracker_thread_data_type = NULL;
23
+
24
+ static void ruby_runtime_stack_callback(
25
+ void (*emit_frame)(const ddog_crasht_RuntimeStackFrame*)
26
+ );
27
+
28
+ // Use a fixed, preallocated buffer for crash-time runtime stacks to avoid
29
+ // heap allocation in the signal/crash path.
30
+ #define RUNTIME_STACK_MAX_FRAMES 512
31
+ static frame_info runtime_stack_buffer[RUNTIME_STACK_MAX_FRAMES];
32
+
33
+ #if defined(__x86_64__)
34
+ # define SYS_MINCORE 0x1B
35
+ #elif defined(__aarch64__)
36
+ # define SYS_MINCORE 0xE8
37
+ #endif
38
+
39
+ long syscall(long number, ...);
40
+
41
+ // align down to power of two
42
+ static inline uintptr_t align_down(uintptr_t x, uintptr_t align) {
43
+ return x & ~(align - 1u);
44
+ }
45
+
46
+ static uintptr_t cached_page_size = 0;
47
+
48
+ // TODO: This function is not necessarily Ruby specific. This will be moved to
49
+ // `libdatadog` in the future as a shared utility function.
50
+ static inline bool is_pointer_readable(const void *ptr, size_t size) {
51
+ if (!ptr || size == 0) return false;
52
+
53
+ uintptr_t page_size = cached_page_size;
54
+
55
+ const uintptr_t start = align_down((uintptr_t)ptr, page_size);
56
+ const uintptr_t end = ((uintptr_t)ptr + size - 1u);
57
+ const uintptr_t last = align_down(end, page_size);
58
+
59
+ // Number of pages spanned
60
+ size_t pages = 1u + (last != start);
61
+ if (pages > 2u) pages = 2u;
62
+
63
+ unsigned char vec[2];
64
+
65
+ int retries = 5;
66
+ for (;;) {
67
+ size_t len = pages * (size_t)page_size;
68
+ long rc = syscall(SYS_MINCORE, (void*)start, len, vec);
69
+
70
+ if (rc == 0) {
71
+ return true;
72
+ }
73
+
74
+ int e = errno;
75
+ if (e == ENOMEM || e == EFAULT) {
76
+ return false;
77
+ }
78
+
79
+ if (e == EAGAIN && retries-- > 0) {
80
+ continue;
81
+ }
82
+
83
+ // Unknown errno, we assume mapped to avoid cascading faults in crash path
84
+ return true;
85
+ }
86
+ }
87
+
88
+ static inline ddog_CharSlice char_slice_from_cstr(const char *cstr) {
89
+ if (cstr == NULL) {
90
+ return (ddog_CharSlice){.ptr = NULL, .len = 0};
91
+ }
92
+ return (ddog_CharSlice){.ptr = cstr, .len = strlen(cstr)};
93
+ }
94
+
95
+ static ddog_CharSlice safe_string_value(VALUE str) {
96
+ if (str == Qnil) return DDOG_CHARSLICE_C("<nil>");
97
+
98
+ // Validate object and payload readability in one check
99
+ if (!is_pointer_readable((const void *)str, sizeof(struct RString))) return DDOG_CHARSLICE_C("<corrupted>");
100
+ if (!RB_TYPE_P(str, T_STRING)) return DDOG_CHARSLICE_C("<not_string>");
101
+
102
+ long len = RSTRING_LEN(str);
103
+ if (len < 0 || len > 1024) return DDOG_CHARSLICE_C("<length_over_limit>");
104
+
105
+ const char *ptr = RSTRING_PTR(str);
106
+ if (!ptr) return DDOG_CHARSLICE_C("<null>");
107
+
108
+ if (!is_pointer_readable(ptr, len > 0 ? len : 1)) return DDOG_CHARSLICE_C("<unreadable>");
109
+
110
+ return (ddog_CharSlice){.ptr = ptr, .len = (size_t)len};
111
+ }
112
+
113
+ static void emit_placeholder_frame(
114
+ void (*emit_frame)(const ddog_crasht_RuntimeStackFrame*),
115
+ const char *label
116
+ ) {
117
+ ddog_crasht_RuntimeStackFrame frame = {
118
+ .type_name = DDOG_CHARSLICE_C(""),
119
+ .function = char_slice_from_cstr(label),
120
+ .file = DDOG_CHARSLICE_C("<unknown>"),
121
+ .line = 0,
122
+ .column = 0
123
+ };
124
+ emit_frame(&frame);
125
+ }
126
+
127
+ // Collect the crashing thread's frames via ddtrace_rb_profile_frames into a static buffer, then emit
128
+ // them newest-first. If corruption is detected, emit placeholder frames so the crash report still
129
+ // completes. We lean on the Ruby VM helpers we already use for profiling and rely on crashtracker's
130
+ // safety nets so a failure here should not impact customers.
131
+ static void ruby_runtime_stack_callback(
132
+ void (*emit_frame)(const ddog_crasht_RuntimeStackFrame*)
133
+ ) {
134
+ // Grab the Ruby thread we crashed on; crashtracker only runs once.
135
+ VALUE current_thread = rb_thread_current();
136
+ if (current_thread == Qnil) return;
137
+
138
+ if (crashtracker_thread_data_type == NULL) return;
139
+
140
+ if (!rb_typeddata_is_kind_of(current_thread, crashtracker_thread_data_type)) {
141
+ emit_placeholder_frame(emit_frame, "<runtime stack not found>");
142
+ return;
143
+ }
144
+
145
+ // Use the profiling helper to gather frames into our static buffer.
146
+ int frame_count = ddtrace_rb_profile_frames(
147
+ current_thread,
148
+ 0,
149
+ RUNTIME_STACK_MAX_FRAMES,
150
+ runtime_stack_buffer
151
+ );
152
+
153
+ if (frame_count <= 0) {
154
+ emit_placeholder_frame(emit_frame, "<runtime stack not found>");
155
+ return;
156
+ }
157
+
158
+ for (int i = frame_count - 1; i >= 0; i--) {
159
+ frame_info *info = &runtime_stack_buffer[i];
160
+
161
+ if (info->is_ruby_frame) {
162
+ const void *iseq = (const void *)info->as.ruby_frame.iseq;
163
+ ddog_CharSlice function_slice = DDOG_CHARSLICE_C("<unknown>");
164
+ ddog_CharSlice file_slice = DDOG_CHARSLICE_C("<unknown>");
165
+
166
+ if (iseq && is_pointer_readable(iseq, sizeof_rb_iseq_t())) {
167
+ function_slice = safe_string_value(ddtrace_iseq_base_label(iseq));
168
+ file_slice = safe_string_value(ddtrace_iseq_path(iseq));
169
+ }
170
+
171
+ ddog_crasht_RuntimeStackFrame frame = {
172
+ .type_name = DDOG_CHARSLICE_C(""),
173
+ .function = function_slice,
174
+ .file = file_slice,
175
+ .line = info->as.ruby_frame.line,
176
+ .column = 0
177
+ };
178
+
179
+ emit_frame(&frame);
180
+ } else {
181
+ ddog_CharSlice function_slice = DDOG_CHARSLICE_C("<C method>");
182
+ ddog_CharSlice file_slice = DDOG_CHARSLICE_C("<C extension>");
183
+
184
+ if (info->as.native_frame.method_id) {
185
+ const char *method_name = rb_id2name(info->as.native_frame.method_id);
186
+ if (is_pointer_readable(method_name, 256)) {
187
+ size_t method_name_len = strnlen(method_name, 256);
188
+ if (method_name_len > 0 && method_name_len < 256) {
189
+ function_slice = char_slice_from_cstr(method_name);
190
+ }
191
+ }
192
+ }
193
+
194
+ ddog_crasht_RuntimeStackFrame frame = {
195
+ .type_name = DDOG_CHARSLICE_C(""),
196
+ .function = function_slice,
197
+ .file = file_slice,
198
+ .line = 0,
199
+ .column = 0
200
+ };
201
+
202
+ emit_frame(&frame);
203
+ }
204
+ }
205
+
206
+ if (frame_count == RUNTIME_STACK_MAX_FRAMES) {
207
+ emit_placeholder_frame(emit_frame, "<truncated frames>");
208
+ }
209
+ }
210
+
211
+ void crashtracking_runtime_stacks_init(void) {
212
+ cached_page_size = (uintptr_t)sysconf(_SC_PAGESIZE);
213
+ if (cached_page_size == 0 || (cached_page_size & (cached_page_size - 1u))) {
214
+ cached_page_size = 4096;
215
+ }
216
+
217
+ if (crashtracker_thread_data_type == NULL) {
218
+ VALUE current_thread = rb_thread_current();
219
+ if (current_thread == Qnil) {
220
+ rb_raise(rb_eRuntimeError, "crashtracking_runtime_stacks_init: rb_thread_current returned Qnil");
221
+ }
222
+
223
+ const rb_data_type_t *thread_data_type = RTYPEDDATA_TYPE(current_thread);
224
+ if (!thread_data_type) {
225
+ rb_raise(rb_eRuntimeError, "crashtracking_runtime_stacks_init: RTYPEDDATA_TYPE returned NULL");
226
+ }
227
+
228
+ crashtracker_thread_data_type = thread_data_type;
229
+ }
230
+
231
+ // Register immediately so Ruby doesn't need to manage this explicitly.
232
+ ddog_crasht_register_runtime_frame_callback(ruby_runtime_stack_callback);
233
+ }
234
+
235
+ #else
236
+ // Keep init symbol to satisfy linkage on non linux platforms, but do nothing
237
+ void crashtracking_runtime_stacks_init(void) {}
238
+ #endif
239
+
@@ -138,8 +138,11 @@ if have_header("dlfcn.h")
138
138
  have_func("dladdr")
139
139
  end
140
140
 
141
+ # On older Rubies, there was no primitive mutex and condition variable implemented in `thread_sync.rb` (internal)
142
+ $defs << "-DNO_PRIMITIVE_MUTEX_AND_CONDITION_VARIABLE" if RUBY_VERSION < "4"
143
+
141
144
  # On Ruby 4, we can't ask the object_id from IMEMOs (https://github.com/ruby/ruby/pull/13347)
142
- $defs << "-DNO_IMEMO_OBJECT_ID" unless RUBY_VERSION < "4.0"
145
+ $defs << "-DNO_IMEMO_OBJECT_ID" unless RUBY_VERSION < "4"
143
146
 
144
147
  # This symbol is exclusively visible on certain Ruby versions: 2.6 to 3.2, as well as 3.4 (but not 4.0+)
145
148
  # It's only used to get extra information about an object when a failure happens, so it's a "very nice to have" but not
@@ -76,6 +76,18 @@ rb_nativethread_id_t pthread_id_for(VALUE thread) {
76
76
  #endif
77
77
  }
78
78
 
79
+ size_t sizeof_rb_iseq_t(void) {
80
+ return sizeof(rb_iseq_t);
81
+ }
82
+
83
+ VALUE ddtrace_iseq_base_label(const void *iseq) {
84
+ return rb_iseq_base_label((const rb_iseq_t *)iseq);
85
+ }
86
+
87
+ VALUE ddtrace_iseq_path(const void *iseq) {
88
+ return rb_iseq_path((const rb_iseq_t *)iseq);
89
+ }
90
+
79
91
  // Queries if the current thread is the owner of the global VM lock.
80
92
  //
81
93
  // @ivoanjo: Ruby has a similarly-named `ruby_thread_has_gvl_p` but that API is insufficient for our needs because it can
@@ -47,6 +47,10 @@ VALUE thread_name_for(VALUE thread);
47
47
 
48
48
  int ddtrace_rb_profile_frames(VALUE thread, int start, int limit, frame_info *stack_buffer);
49
49
 
50
+ size_t sizeof_rb_iseq_t(void);
51
+ VALUE ddtrace_iseq_base_label(const void *iseq);
52
+ VALUE ddtrace_iseq_path(const void *iseq);
53
+
50
54
  // Returns true if the current thread belongs to the main Ractor or if Ruby has no Ractor support
51
55
  bool ddtrace_rb_ractor_main_p(void);
52
56
 
@@ -23,6 +23,7 @@ void collectors_thread_context_init(VALUE profiling_module);
23
23
  void encoded_profile_init(VALUE profiling_module);
24
24
  void http_transport_init(VALUE profiling_module);
25
25
  void stack_recorder_init(VALUE profiling_module);
26
+ void crashtracking_runtime_stacks_init(void);
26
27
 
27
28
  static VALUE native_working_p(VALUE self);
28
29
  static VALUE _native_grab_gvl_and_raise(DDTRACE_UNUSED VALUE _self, VALUE exception_class, VALUE test_message, VALUE test_message_arg, VALUE release_gvl);
@@ -65,6 +66,7 @@ void DDTRACE_EXPORT Init_datadog_profiling_native_extension(void) {
65
66
  encoded_profile_init(profiling_module);
66
67
  http_transport_init(profiling_module);
67
68
  stack_recorder_init(profiling_module);
69
+ crashtracking_runtime_stacks_init();
68
70
  unsafe_api_calls_check_init();
69
71
 
70
72
  // Hosts methods used for testing the native code using RSpec
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "net/http"
5
+ require "json"
6
+
7
+ module Datadog
8
+ module AIGuard
9
+ # API Client for AI Guard API.
10
+ # Uses net/http to perform request. Raises on client and server errors.
11
+ class APIClient
12
+ DEFAULT_SITE = "app.datadoghq.com"
13
+ DEFAULT_PATH = "/api/v2/ai-guard"
14
+
15
+ def initialize(endpoint:, api_key:, application_key:, timeout:)
16
+ @timeout = timeout
17
+
18
+ @endpoint_uri = if endpoint
19
+ URI(endpoint) #: URI::HTTP
20
+ else
21
+ URI::HTTPS.build(
22
+ host: Datadog.configuration.site || DEFAULT_SITE,
23
+ path: DEFAULT_PATH
24
+ )
25
+ end
26
+
27
+ @headers = {
28
+ "DD-API-KEY": api_key.to_s,
29
+ "DD-APPLICATION-KEY": application_key.to_s,
30
+ "DD-AI-GUARD-VERSION": Datadog::VERSION::STRING,
31
+ "DD-AI-GUARD-SOURCE": "SDK",
32
+ "DD-AI-GUARD-LANGUAGE": "ruby",
33
+ "content-type": "application/json"
34
+ }.freeze
35
+ end
36
+
37
+ def post(path, body:)
38
+ Net::HTTP.start(@endpoint_uri.host.to_s, @endpoint_uri.port, use_ssl: use_ssl?, read_timeout: @timeout) do |http|
39
+ request = Net::HTTP::Post.new(@endpoint_uri.request_uri + path, @headers)
40
+ request.body = body.to_json
41
+
42
+ response = http.request(request)
43
+ raise_on_http_error!(response)
44
+
45
+ parse_response_body(response.body)
46
+ end
47
+ rescue Net::ReadTimeout
48
+ raise AIGuardClientError, "Request to AI Guard timed out"
49
+ end
50
+
51
+ private
52
+
53
+ def raise_on_http_error!(response)
54
+ case response
55
+ when Net::HTTPSuccess
56
+ # do nothing
57
+ when Net::HTTPRedirection
58
+ raise AIGuardClientError, "Redirects for AI Guard API are not supported"
59
+ else
60
+ error_message = begin
61
+ parsed_body = JSON.parse(response.body)
62
+ Array(parsed_body.fetch('errors')).join(', ')
63
+ rescue JSON::ParserError, KeyError
64
+ response.body
65
+ end
66
+
67
+ raise AIGuardClientError, error_message
68
+ end
69
+ end
70
+
71
+ def parse_response_body(body)
72
+ JSON.parse(body)
73
+ rescue JSON::ParserError
74
+ raise AIGuardClientError, "Could not parse response body"
75
+ end
76
+
77
+ def use_ssl?
78
+ @endpoint_uri.scheme == 'https'
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'api_client'
4
+ require_relative 'evaluation'
5
+ require_relative 'evaluation/request'
6
+ require_relative 'evaluation/result'
7
+ require_relative 'evaluation/no_op_result'
8
+ require_relative 'evaluation/message'
9
+ require_relative 'evaluation/tool_call'
10
+ require_relative 'ext'
11
+
12
+ module Datadog
13
+ module AIGuard
14
+ # Component for API Guard product
15
+ class Component
16
+ attr_reader :api_client, :logger
17
+
18
+ def self.build(settings, logger:, telemetry:)
19
+ return unless settings.respond_to?(:ai_guard) && settings.ai_guard.enabled
20
+
21
+ api_client = APIClient.new(
22
+ endpoint: settings.ai_guard.endpoint,
23
+ api_key: settings.api_key,
24
+ application_key: settings.ai_guard.app_key,
25
+ timeout: settings.ai_guard.timeout_ms / 1_000
26
+ )
27
+
28
+ new(api_client, logger: logger, telemetry: telemetry)
29
+ end
30
+
31
+ def initialize(api_client, logger:, telemetry:)
32
+ @api_client = api_client
33
+ @logger = logger
34
+ @telemetry = telemetry
35
+ end
36
+
37
+ def shutdown!
38
+ # no-op
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module AIGuard
5
+ module Configuration
6
+ # This module contains constants for AI Guard component
7
+ module Ext
8
+ ENV_AI_GUARD_ENABLED = "DD_AI_GUARD_ENABLED"
9
+ ENV_AI_GUARD_ENDPOINT = "DD_AI_GUARD_ENDPOINT"
10
+ ENV_AI_GUARD_TIMEOUT = "DD_AI_GUARD_TIMEOUT"
11
+ ENV_AI_GUARD_MAX_CONTENT_SIZE = "DD_AI_GUARD_MAX_CONTENT_SIZE"
12
+ ENV_AI_GUARD_MAX_MESSAGES_LENGTH = "DD_AI_GUARD_MAX_MESSAGES_LENGTH"
13
+ ENV_APP_KEY = "DD_APP_KEY"
14
+ end
15
+ end
16
+ end
17
+ end