datadog 2.32.0 → 2.34.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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -1
  3. data/ext/datadog_profiling_native_extension/clock_id.h +9 -1
  4. data/ext/datadog_profiling_native_extension/clock_id_from_mach.c +73 -0
  5. data/ext/datadog_profiling_native_extension/clock_id_from_pthread.c +1 -1
  6. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +20 -0
  7. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +5 -1
  8. data/ext/datadog_profiling_native_extension/extconf.rb +3 -0
  9. data/ext/datadog_profiling_native_extension/macos_sampler_thread.h +55 -0
  10. data/ext/datadog_profiling_native_extension/stack_recorder.c +3 -9
  11. data/ext/datadog_profiling_native_extension/time_helpers.h +1 -0
  12. data/ext/libdatadog_api/crashtracker.c +2 -0
  13. data/ext/libdatadog_extconf_helpers.rb +1 -1
  14. data/lib/datadog/ai_guard/autoload.rb +10 -0
  15. data/lib/datadog/ai_guard/component.rb +1 -1
  16. data/lib/datadog/ai_guard/contrib/auto_instrument.rb +24 -0
  17. data/lib/datadog/ai_guard/contrib/rack/integration.rb +42 -0
  18. data/lib/datadog/ai_guard/contrib/rack/patcher.rb +26 -0
  19. data/lib/datadog/ai_guard/contrib/rack/request_middleware.rb +83 -0
  20. data/lib/datadog/ai_guard/contrib/rails/integration.rb +41 -0
  21. data/lib/datadog/ai_guard/contrib/rails/patcher.rb +97 -0
  22. data/lib/datadog/ai_guard/evaluation.rb +1 -0
  23. data/lib/datadog/ai_guard/ext.rb +1 -0
  24. data/lib/datadog/ai_guard.rb +8 -0
  25. data/lib/datadog/appsec/component.rb +4 -1
  26. data/lib/datadog/appsec/compressed_json.rb +2 -2
  27. data/lib/datadog/appsec/contrib/aws_lambda/gateway/watcher.rb +75 -0
  28. data/lib/datadog/appsec/contrib/aws_lambda/integration.rb +39 -0
  29. data/lib/datadog/appsec/contrib/aws_lambda/patcher.rb +30 -0
  30. data/lib/datadog/appsec/contrib/aws_lambda/waf_addresses.rb +111 -0
  31. data/lib/datadog/appsec/contrib/rack/ext.rb +1 -1
  32. data/lib/datadog/appsec.rb +1 -0
  33. data/lib/datadog/core/configuration/components.rb +8 -1
  34. data/lib/datadog/core/configuration/settings.rb +16 -1
  35. data/lib/datadog/core/configuration/supported_configurations.rb +12 -0
  36. data/lib/datadog/core/environment/ext.rb +5 -0
  37. data/lib/datadog/core/environment/identity.rb +15 -1
  38. data/lib/datadog/core/environment/process.rb +48 -27
  39. data/lib/datadog/core/environment/socket.rb +13 -0
  40. data/lib/datadog/core/remote/client/capabilities.rb +11 -2
  41. data/lib/datadog/core/remote/transport/http/config.rb +5 -5
  42. data/lib/datadog/core/telemetry/request.rb +0 -2
  43. data/lib/datadog/core/transport/response.rb +1 -1
  44. data/lib/datadog/core/utils/{base64.rb → base64_codec.rb} +3 -2
  45. data/lib/datadog/core/utils/hash.rb +0 -23
  46. data/lib/datadog/core/utils/spawn_monkey_patch.rb +46 -16
  47. data/lib/datadog/data_streams/pathway_context.rb +3 -3
  48. data/lib/datadog/di/code_tracker.rb +43 -22
  49. data/lib/datadog/di/contrib/active_record.rb +6 -2
  50. data/lib/datadog/di/instrumenter.rb +24 -4
  51. data/lib/datadog/di/probe_notification_builder.rb +1 -1
  52. data/lib/datadog/di/remote.rb +4 -4
  53. data/lib/datadog/di/serializer.rb +5 -5
  54. data/lib/datadog/di/utils.rb +42 -14
  55. data/lib/datadog/opentelemetry/configuration/settings.rb +65 -0
  56. data/lib/datadog/opentelemetry/ext.rb +9 -0
  57. data/lib/datadog/opentelemetry/logs.rb +98 -0
  58. data/lib/datadog/opentelemetry/metrics.rb +10 -37
  59. data/lib/datadog/opentelemetry/sdk/configurator.rb +40 -0
  60. data/lib/datadog/opentelemetry/sdk/id_generator.rb +16 -10
  61. data/lib/datadog/opentelemetry/sdk/logs_exporter.rb +37 -0
  62. data/lib/datadog/opentelemetry/signal_configuration.rb +53 -0
  63. data/lib/datadog/opentelemetry.rb +1 -0
  64. data/lib/datadog/profiling/component.rb +0 -1
  65. data/lib/datadog/profiling/stack_recorder.rb +0 -4
  66. data/lib/datadog/symbol_database/component.rb +409 -0
  67. data/lib/datadog/symbol_database/configuration.rb +2 -2
  68. data/lib/datadog/symbol_database/extractor.rb +45 -26
  69. data/lib/datadog/symbol_database/remote.rb +175 -0
  70. data/lib/datadog/symbol_database/scope.rb +16 -12
  71. data/lib/datadog/symbol_database/scope_batcher.rb +288 -0
  72. data/lib/datadog/symbol_database/service_version.rb +15 -6
  73. data/lib/datadog/symbol_database/symbol.rb +6 -3
  74. data/lib/datadog/symbol_database/uploader.rb +65 -8
  75. data/lib/datadog/tracing/contrib/action_pack/action_dispatch/instrumentation.rb +8 -0
  76. data/lib/datadog/tracing/contrib/active_record/events/sql.rb +0 -4
  77. data/lib/datadog/tracing/contrib/active_support/cache/events/cache.rb +0 -4
  78. data/lib/datadog/tracing/contrib/active_support/cache/instrumentation.rb +0 -4
  79. data/lib/datadog/tracing/contrib/aws/instrumentation.rb +0 -5
  80. data/lib/datadog/tracing/contrib/dalli/instrumentation.rb +0 -5
  81. data/lib/datadog/tracing/contrib/elasticsearch/patcher.rb +0 -5
  82. data/lib/datadog/tracing/contrib/ethon/easy_patch.rb +0 -5
  83. data/lib/datadog/tracing/contrib/ethon/multi_patch.rb +0 -8
  84. data/lib/datadog/tracing/contrib/excon/middleware.rb +0 -5
  85. data/lib/datadog/tracing/contrib/ext.rb +2 -3
  86. data/lib/datadog/tracing/contrib/faraday/middleware.rb +0 -5
  87. data/lib/datadog/tracing/contrib/grpc/datadog_interceptor/client.rb +0 -5
  88. data/lib/datadog/tracing/contrib/grpc/datadog_interceptor/server.rb +0 -5
  89. data/lib/datadog/tracing/contrib/http/instrumentation.rb +0 -5
  90. data/lib/datadog/tracing/contrib/httpclient/instrumentation.rb +0 -5
  91. data/lib/datadog/tracing/contrib/httprb/instrumentation.rb +0 -5
  92. data/lib/datadog/tracing/contrib/mongodb/subscribers.rb +0 -5
  93. data/lib/datadog/tracing/contrib/mysql2/instrumentation.rb +0 -5
  94. data/lib/datadog/tracing/contrib/opensearch/patcher.rb +0 -5
  95. data/lib/datadog/tracing/contrib/pg/instrumentation.rb +0 -5
  96. data/lib/datadog/tracing/contrib/presto/instrumentation.rb +0 -5
  97. data/lib/datadog/tracing/contrib/racecar/event.rb +0 -5
  98. data/lib/datadog/tracing/contrib/rack/configuration/settings.rb +6 -0
  99. data/lib/datadog/tracing/contrib/rack/ext.rb +27 -0
  100. data/lib/datadog/tracing/contrib/rack/trace_proxy_middleware.rb +117 -1
  101. data/lib/datadog/tracing/contrib/redis/tags.rb +0 -5
  102. data/lib/datadog/tracing/contrib/rest_client/request_patch.rb +0 -5
  103. data/lib/datadog/tracing/contrib/sequel/utils.rb +0 -5
  104. data/lib/datadog/tracing/contrib/trilogy/instrumentation.rb +0 -5
  105. data/lib/datadog/tracing/distributed/datadog_tags_codec.rb +0 -13
  106. data/lib/datadog/tracing/distributed/trace_context.rb +0 -28
  107. data/lib/datadog/tracing/metadata/ext.rb +3 -0
  108. data/lib/datadog/tracing/span_operation.rb +13 -0
  109. data/lib/datadog/tracing/trace_operation.rb +22 -0
  110. data/lib/datadog/tracing/tracer.rb +7 -3
  111. data/lib/datadog/version.rb +1 -1
  112. metadata +27 -8
  113. data/ext/datadog_profiling_native_extension/clock_id_noop.c +0 -21
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 04ff51661b0a7d494e4beaf4d142c723bce97e2c2a14ad708e1bfd12e780129a
4
- data.tar.gz: 0a4a3f4ccc112c3c805a035072270781e360f68ae1f3464356abde84b97944ea
3
+ metadata.gz: aa3ddf8f18dda4b503c654911332fb5cdc50356d4fa8223d43a993be204618ac
4
+ data.tar.gz: d917e203823268d36fc01c086969052160a6afdfb2c3cfdc29a081d8ce23a384
5
5
  SHA512:
6
- metadata.gz: c664b9d447c8645e4a202f04e5b2f690b85f15b62689f3312a8d5d3ccb2a08ec289a6d07fa414eee8e5dd92a3bea9f040b758fbfed97ab602bdcf4a989e47d6b
7
- data.tar.gz: 92ce248e3e351d774b043921c18cd73f3ba064eed1203246a81ef1de9500cb4c6220fc7df81187b5b49bd4062fa87228bf8be8817c599315da8853752740ab96
6
+ metadata.gz: 4fda14c3e26b82d5b93e3581acfa939d9ac3be695c2ad26ca720b4873127f05e949dc78f0cb1b8922ed87cd114881e9191fa36463cd86f3dcbf5d969da0da932
7
+ data.tar.gz: ca53286a2a5ad436e460ede12148faed98b9904d0823715954f602cdf292cb03966cde1f96dd2c227581b1ccd814c30fdd0236dfaa1903f59a87ae2873f40dd0
data/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [2.34.0] - 2026-05-27
6
+
7
+ ### Added
8
+
9
+ * Dynamic Instrumentation: Enable Symbol Database upload so Ruby services populate Live Debugger UI autocomplete when creating probes ([#5717][])
10
+ * Open Telemetry: Add OpenTelemetry logs support with OTLP export. Enable using `DD_LOGS_OTEL_ENABLED=true`; supports standard `OTEL_EXPORTER_OTLP_*` settings ([#5446][])
11
+
12
+ ### Changed
13
+
14
+ * Dynamic Instrumentation: Improve request latency under load by throttling symbol database extraction CPU usage during background processing ([#5776][])
15
+
16
+ ### Fixed
17
+
18
+ * Core: Fix Remote configuration `TransportError` messages removing wrapper response object memory addresses ([#5762][])
19
+ * Core: Fix `TypeError` from `Process.spawn` when passing an environment Hash ([#5773][], [#5634][])
20
+ * AppSec: Prevent host application crashes when AppSec fails to initialize ([#5768][])
21
+ * Dynamic Instrumentation: Fix off-by-one in `max_capture_depth` so snapshots respect the configured nesting limit exactly ([#5753][])
22
+ * Dynamic Instrumentation: Improve line probes to match `sourceFile` case-insensitively and support Windows-style backslashes for correct installation ([#5754][])
23
+
5
24
  ## [2.31.0] - 2026-04-20
6
25
 
7
26
  ### Added
@@ -3567,7 +3586,8 @@ Release notes: https://github.com/DataDog/dd-trace-rb/releases/tag/v0.3.1
3567
3586
  Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
3568
3587
 
3569
3588
 
3570
- [Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v2.31.0...master
3589
+ [Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v2.34.0...master
3590
+ [2.34.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.33.0...v2.34.0
3571
3591
  [2.31.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.30.0...v2.31.0
3572
3592
  [2.30.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.29.0...v2.30.0
3573
3593
  [2.29.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.28.0...v2.29.0
@@ -5278,6 +5298,7 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
5278
5298
  [#5434]: https://github.com/DataDog/dd-trace-rb/issues/5434
5279
5299
  [#5435]: https://github.com/DataDog/dd-trace-rb/issues/5435
5280
5300
  [#5436]: https://github.com/DataDog/dd-trace-rb/issues/5436
5301
+ [#5446]: https://github.com/DataDog/dd-trace-rb/issues/5446
5281
5302
  [#5448]: https://github.com/DataDog/dd-trace-rb/issues/5448
5282
5303
  [#5449]: https://github.com/DataDog/dd-trace-rb/issues/5449
5283
5304
  [#5461]: https://github.com/DataDog/dd-trace-rb/issues/5461
@@ -5295,6 +5316,14 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
5295
5316
  [#5580]: https://github.com/DataDog/dd-trace-rb/issues/5580
5296
5317
  [#5587]: https://github.com/DataDog/dd-trace-rb/issues/5587
5297
5318
  [#5594]: https://github.com/DataDog/dd-trace-rb/issues/5594
5319
+ [#5634]: https://github.com/DataDog/dd-trace-rb/issues/5634
5320
+ [#5717]: https://github.com/DataDog/dd-trace-rb/issues/5717
5321
+ [#5753]: https://github.com/DataDog/dd-trace-rb/issues/5753
5322
+ [#5754]: https://github.com/DataDog/dd-trace-rb/issues/5754
5323
+ [#5762]: https://github.com/DataDog/dd-trace-rb/issues/5762
5324
+ [#5768]: https://github.com/DataDog/dd-trace-rb/issues/5768
5325
+ [#5773]: https://github.com/DataDog/dd-trace-rb/issues/5773
5326
+ [#5776]: https://github.com/DataDog/dd-trace-rb/issues/5776
5298
5327
  [@AdrianLC]: https://github.com/AdrianLC
5299
5328
  [@Azure7111]: https://github.com/Azure7111
5300
5329
  [@BabyGroot]: https://github.com/BabyGroot
@@ -4,10 +4,18 @@
4
4
  #include <time.h>
5
5
  #include <ruby.h>
6
6
 
7
+ #ifdef __APPLE__
8
+ #include <mach/mach.h>
9
+ // On macOS, we use a mach_port_t to identify threads for CPU time queries
10
+ typedef mach_port_t cpu_time_id_t;
11
+ #else
12
+ typedef clockid_t cpu_time_id_t;
13
+ #endif
14
+
7
15
  // Contains the operating-system specific identifier needed to fetch CPU-time, and a flag to indicate if we failed to fetch it
8
16
  typedef struct {
9
17
  bool valid;
10
- clockid_t clock_id;
18
+ cpu_time_id_t clock_id;
11
19
  } thread_cpu_time_id;
12
20
 
13
21
  // Contains the current cpu time, and a flag to indicate if we failed to fetch it
@@ -0,0 +1,73 @@
1
+ #include "extconf.h"
2
+
3
+ // This file is only compiled on macOS where Mach thread APIs are available;
4
+ // Otherwise we compile clock_id_from_pthread.c (Linux)
5
+ #ifdef HAVE_MACH_THREAD_INFO
6
+
7
+ #include <pthread.h>
8
+ #include <time.h>
9
+ #include <mach/mach.h>
10
+ #include <mach/thread_info.h>
11
+
12
+ #include "clock_id.h"
13
+ #include "helpers.h"
14
+ #include "private_vm_api_access.h"
15
+ #include "ruby_helpers.h"
16
+ #include "time_helpers.h"
17
+
18
+ // Validate that our home-cooked pthread_id_for() matches pthread_self() for the current thread
19
+ void self_test_clock_id(void) {
20
+ rb_nativethread_id_t expected_pthread_id = pthread_self();
21
+ rb_nativethread_id_t actual_pthread_id = pthread_id_for(rb_thread_current());
22
+
23
+ if (expected_pthread_id != actual_pthread_id) raise_error(rb_eRuntimeError, "pthread_id_for() self-test failed");
24
+
25
+ // Also validate that we can get a valid mach thread port for the current thread
26
+ mach_port_t mach_thread = pthread_mach_thread_np(expected_pthread_id);
27
+ if (mach_thread == MACH_PORT_NULL) raise_error(rb_eRuntimeError, "pthread_mach_thread_np() self-test failed");
28
+ }
29
+
30
+ // Safety: This function is assumed never to raise exceptions by callers
31
+ thread_cpu_time_id thread_cpu_time_id_for(VALUE thread) {
32
+ rb_nativethread_id_t thread_id = pthread_id_for(thread);
33
+
34
+ if (thread_id == 0) return (thread_cpu_time_id) {.valid = false};
35
+
36
+ mach_port_t mach_thread = pthread_mach_thread_np(thread_id);
37
+
38
+ if (mach_thread == MACH_PORT_NULL) {
39
+ return (thread_cpu_time_id) {.valid = false};
40
+ }
41
+
42
+ return (thread_cpu_time_id) {.valid = true, .clock_id = mach_thread};
43
+ }
44
+
45
+ thread_cpu_time thread_cpu_time_for(thread_cpu_time_id time_id) {
46
+ thread_cpu_time error = (thread_cpu_time) {.valid = false};
47
+
48
+ if (!time_id.valid) return error;
49
+
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
+
61
+ struct thread_basic_info info;
62
+ mach_msg_type_number_t count = THREAD_BASIC_INFO_COUNT;
63
+ kern_return_t kr = thread_info(time_id.clock_id, THREAD_BASIC_INFO, (thread_info_t)&info, &count);
64
+
65
+ if (kr != KERN_SUCCESS) return error;
66
+
67
+ long user_ns = SECONDS_AS_NS(info.user_time.seconds) + MICROS_AS_NS(info.user_time.microseconds);
68
+ long system_ns = SECONDS_AS_NS(info.system_time.seconds) + MICROS_AS_NS(info.system_time.microseconds);
69
+
70
+ return (thread_cpu_time) {.valid = true, .result_ns = user_ns + system_ns};
71
+ }
72
+
73
+ #endif
@@ -1,7 +1,7 @@
1
1
  #include "extconf.h"
2
2
 
3
3
  // This file is only compiled on systems where pthread_getcpuclockid() is available;
4
- // Otherwise we compile clock_id_noop.c
4
+ // Otherwise we compile clock_id_from_mach.c (macOS)
5
5
  #ifdef HAVE_PTHREAD_GETCPUCLOCKID
6
6
 
7
7
  #include <pthread.h>
@@ -17,6 +17,10 @@
17
17
  #include "setup_signal_handler.h"
18
18
  #include "time_helpers.h"
19
19
 
20
+ #ifdef __APPLE__
21
+ #include "macos_sampler_thread.h"
22
+ #endif
23
+
20
24
  // Used to trigger the execution of Collectors::ThreadContext, which implements all of the sampling logic
21
25
  // itself; this class only implements the "when to do it" part.
22
26
  //
@@ -535,6 +539,14 @@ static VALUE _native_sampling_loop(DDTRACE_UNUSED VALUE _self, VALUE instance) {
535
539
 
536
540
  block_sigprof_signal_handler_from_running_in_current_thread(); // We want to interrupt the thread with the global VM lock, never this one
537
541
 
542
+ #ifdef __APPLE__
543
+ // Promote the native thread that will run the sampling loop to a realtime scheduling policy so
544
+ // its periodic wake-ups are not subject to the default ~1-2ms timeshare wake latency. The matching
545
+ // demote happens below in this same function, alongside the SIGPROF unblock dance, so both per-thread
546
+ // policy changes are reverted before Ruby reuses this native thread for an unrelated Ruby thread.
547
+ promote_sampler_thread_to_realtime(MILLIS_AS_NS(state->cpu_sampling_interval_ms));
548
+ #endif
549
+
538
550
  // Release GVL, get to the actual work!
539
551
  int exception_state;
540
552
  rb_protect(release_gvl_and_run_sampling_trigger_loop, instance, &exception_state);
@@ -556,6 +568,14 @@ static VALUE _native_sampling_loop(DDTRACE_UNUSED VALUE _self, VALUE instance) {
556
568
  // had SIGPROF delivery blocked. :hide_the_pain_harold:
557
569
  unblock_sigprof_signal_handler_from_running_in_current_thread();
558
570
 
571
+ #ifdef __APPLE__
572
+ // Pairs with promote_sampler_thread_to_realtime above. Same reasoning as the SIGPROF unblock:
573
+ // Ruby may reuse this native thread for an unrelated Ruby thread that would otherwise inherit
574
+ // our scheduler policy. Doing it here also covers the exception path, since grab_gvl_and_sample
575
+ // can raise and unwind out of the sampling loop.
576
+ demote_sampler_thread_from_realtime();
577
+ #endif
578
+
559
579
  // Why replace and not use remove the signal handler? We do this because when a process receives a SIGPROF without
560
580
  // having an explicit signal handler set up, the process will instantly terminate with a confusing
561
581
  // "Profiling timer expired" message left behind. (This message doesn't come from us -- it's the default message for
@@ -1201,7 +1201,11 @@ static int per_thread_context_as_ruby_hash(st_data_t key_thread, st_data_t value
1201
1201
  ID2SYM(rb_intern("thread_id")), /* => */ rb_str_new2(thread_context->thread_id),
1202
1202
  ID2SYM(rb_intern("thread_invoke_location")), /* => */ rb_str_new2(thread_context->thread_invoke_location),
1203
1203
  ID2SYM(rb_intern("thread_cpu_time_id_valid?")), /* => */ thread_context->thread_cpu_time_id.valid ? Qtrue : Qfalse,
1204
- ID2SYM(rb_intern("thread_cpu_time_id")), /* => */ CLOCKID2NUM(thread_context->thread_cpu_time_id.clock_id),
1204
+ #ifdef __APPLE__
1205
+ ID2SYM(rb_intern("thread_cpu_time_id")), /* => */ ULL2NUM(thread_context->thread_cpu_time_id.clock_id),
1206
+ #else
1207
+ ID2SYM(rb_intern("thread_cpu_time_id")), /* => */ CLOCKID2NUM(thread_context->thread_cpu_time_id.clock_id),
1208
+ #endif
1205
1209
  ID2SYM(rb_intern("cpu_time_at_previous_sample_ns")), /* => */ LONG2NUM(thread_context->cpu_time_at_previous_sample_ns),
1206
1210
  ID2SYM(rb_intern("wall_time_at_previous_sample_ns")), /* => */ LONG2NUM(thread_context->wall_time_at_previous_sample_ns),
1207
1211
 
@@ -129,6 +129,9 @@ if RUBY_PLATFORM.include?("linux")
129
129
 
130
130
  # Not available on macOS
131
131
  $defs << "-DHAVE_CLOCK_MONOTONIC_COARSE"
132
+ elsif RUBY_PLATFORM.include?("darwin")
133
+ # On macOS, we use Mach thread APIs to get per-thread CPU time
134
+ $defs << "-DHAVE_MACH_THREAD_INFO"
132
135
  end
133
136
 
134
137
  have_func "malloc_stats"
@@ -0,0 +1,55 @@
1
+ #pragma once
2
+
3
+ #include <mach/mach.h>
4
+ #include <mach/mach_time.h>
5
+ #include <mach/thread_policy.h>
6
+ #include <pthread.h>
7
+ #include <stdio.h>
8
+
9
+ #include "time_helpers.h"
10
+
11
+ // On macOS the default scheduler wake latency for a timeshare thread is ~1-2ms,
12
+ // so nanosleep regularly overshoots a 10ms request by ~2ms. Marking the worker
13
+ // thread with THREAD_TIME_CONSTRAINT_POLICY tells the scheduler this thread has
14
+ // a periodic deadline; the kernel then wakes it close to the requested time.
15
+ //
16
+ // This only affects the sampler worker thread. We pair it with a demote call
17
+ // before the thread can be reused by Ruby for unrelated Ruby threads (see the
18
+ // SIGPROF unblock dance in _native_sampling_loop).
19
+ static inline void promote_sampler_thread_to_realtime(uint64_t period_ns) {
20
+ mach_timebase_info_data_t timebase = (mach_timebase_info_data_t) {};
21
+ mach_timebase_info(&timebase);
22
+
23
+ // ns -> mach ticks: each mach tick is (numer/denom) ns, so divide by that.
24
+ #define NS_TO_MACH_TICKS(ns) ((uint32_t) (((uint64_t)(ns) * (uint64_t) timebase.denom) / (uint64_t) timebase.numer))
25
+
26
+ struct thread_time_constraint_policy policy = {
27
+ .period = NS_TO_MACH_TICKS(period_ns),
28
+ .computation = NS_TO_MACH_TICKS(MICROS_AS_NS(200)), // 200us upper bound on the work we do per period
29
+ .constraint = NS_TO_MACH_TICKS(MILLIS_AS_NS(1)), // wake us within 1ms of the deadline
30
+ .preemptible = TRUE,
31
+ };
32
+
33
+ #undef NS_TO_MACH_TICKS
34
+
35
+ kern_return_t kr = thread_policy_set(
36
+ pthread_mach_thread_np(pthread_self()),
37
+ THREAD_TIME_CONSTRAINT_POLICY,
38
+ (thread_policy_t) &policy,
39
+ THREAD_TIME_CONSTRAINT_POLICY_COUNT
40
+ );
41
+ if (kr != KERN_SUCCESS) {
42
+ // Non-fatal: we'll fall back to the default scheduler behavior (overshoot ~2ms).
43
+ fprintf(stderr, "[ddtrace] Failed to set real-time policy on profiler sampler thread (kr=%d); sample cadence may be imprecise\n", kr);
44
+ }
45
+ }
46
+
47
+ static inline void demote_sampler_thread_from_realtime(void) {
48
+ struct thread_standard_policy policy = {.no_data = 0};
49
+ thread_policy_set(
50
+ pthread_mach_thread_np(pthread_self()),
51
+ THREAD_STANDARD_POLICY,
52
+ (thread_policy_t) &policy,
53
+ THREAD_STANDARD_POLICY_COUNT
54
+ );
55
+ }
@@ -416,7 +416,6 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
416
416
  if (options == Qnil) options = rb_hash_new();
417
417
 
418
418
  VALUE recorder_instance = rb_hash_fetch(options, ID2SYM(rb_intern("self_instance")));
419
- VALUE cpu_time_enabled = rb_hash_fetch(options, ID2SYM(rb_intern("cpu_time_enabled")));
420
419
  VALUE alloc_samples_enabled = rb_hash_fetch(options, ID2SYM(rb_intern("alloc_samples_enabled")));
421
420
  VALUE heap_samples_enabled = rb_hash_fetch(options, ID2SYM(rb_intern("heap_samples_enabled")));
422
421
  VALUE heap_size_enabled = rb_hash_fetch(options, ID2SYM(rb_intern("heap_size_enabled")));
@@ -424,7 +423,6 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
424
423
  VALUE timeline_enabled = rb_hash_fetch(options, ID2SYM(rb_intern("timeline_enabled")));
425
424
  VALUE heap_clean_after_gc_enabled = rb_hash_fetch(options, ID2SYM(rb_intern("heap_clean_after_gc_enabled")));
426
425
 
427
- ENFORCE_BOOLEAN(cpu_time_enabled);
428
426
  ENFORCE_BOOLEAN(alloc_samples_enabled);
429
427
  ENFORCE_BOOLEAN(heap_samples_enabled);
430
428
  ENFORCE_BOOLEAN(heap_size_enabled);
@@ -440,7 +438,6 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
440
438
  heap_recorder_set_sample_rate(state->heap_recorder, NUM2INT(heap_sample_every));
441
439
 
442
440
  uint8_t requested_values_count = ALL_VALUE_TYPES_COUNT -
443
- (cpu_time_enabled == Qtrue ? 0 : 1) -
444
441
  (alloc_samples_enabled == Qtrue? 0 : 2) -
445
442
  (heap_samples_enabled == Qtrue ? 0 : 1) -
446
443
  (heap_size_enabled == Qtrue ? 0 : 1) -
@@ -466,12 +463,9 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
466
463
  enabled_sample_types[next_enabled_pos] = DDOG_PROF_SAMPLE_TYPE_WALL_TIME;
467
464
  state->position_for[WALL_TIME_VALUE_ID] = next_enabled_pos++;
468
465
 
469
- if (cpu_time_enabled == Qtrue) {
470
- enabled_sample_types[next_enabled_pos] = DDOG_PROF_SAMPLE_TYPE_CPU_TIME;
471
- state->position_for[CPU_TIME_VALUE_ID] = next_enabled_pos++;
472
- } else {
473
- state->position_for[CPU_TIME_VALUE_ID] = next_disabled_pos++;
474
- }
466
+ // CPU_TIME is always enabled
467
+ enabled_sample_types[next_enabled_pos] = DDOG_PROF_SAMPLE_TYPE_CPU_TIME;
468
+ state->position_for[CPU_TIME_VALUE_ID] = next_enabled_pos++;
475
469
 
476
470
  if (alloc_samples_enabled == Qtrue) {
477
471
  enabled_sample_types[next_enabled_pos] = DDOG_PROF_SAMPLE_TYPE_ALLOC_SAMPLES;
@@ -9,6 +9,7 @@
9
9
 
10
10
  #define SECONDS_AS_NS(value) (value * 1000 * 1000 * 1000L)
11
11
  #define MILLIS_AS_NS(value) (value * 1000 * 1000L)
12
+ #define MICROS_AS_NS(value) (value * 1000L)
12
13
 
13
14
  typedef enum { RAISE_ON_FAILURE, DO_NOT_RAISE_ON_FAILURE } raise_on_failure_setting;
14
15
 
@@ -72,6 +72,8 @@ static VALUE _native_start_or_update_on_fork(int argc, VALUE *argv, DDTRACE_UNUS
72
72
  .endpoint = {.url = char_slice_from_ruby_string(agent_base_url)},
73
73
  .resolve_frames = DDOG_CRASHT_STACKTRACE_COLLECTION_ENABLED_WITH_SYMBOLS_IN_RECEIVER,
74
74
  .timeout_ms = FIX2INT(upload_timeout_seconds) * 1000,
75
+ .collect_all_threads = true,
76
+ .max_threads = 128,
75
77
  };
76
78
 
77
79
  ddog_crasht_Metadata metadata = {
@@ -10,7 +10,7 @@ module Datadog
10
10
  module LibdatadogExtconfHelpers
11
11
  # Used to make sure the correct gem version gets loaded, as extconf.rb does not get run with "bundle exec" and thus
12
12
  # may see multiple libdatadog versions. See https://github.com/DataDog/dd-trace-rb/pull/2531 for the horror story.
13
- LIBDATADOG_VERSION = '~> 30.0.0.1.0'
13
+ LIBDATADOG_VERSION = '~> 33.0.0.1.0'
14
14
 
15
15
  # Used as an workaround for a limitation with how dynamic linking works in environments where the datadog gem and
16
16
  # libdatadog are moved after the extension gets compiled.
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ if %w[1 true].include?((Datadog::DATADOG_ENV["DD_AI_GUARD_ENABLED"] || "").downcase)
4
+ begin
5
+ require_relative "contrib/auto_instrument"
6
+ Datadog::AIGuard::Contrib::AutoInstrument.patch_all
7
+ rescue => e
8
+ Kernel.warn("[datadog] AI Guard failed to auto-instrument. error: #{e.class}: #{e.message}")
9
+ end
10
+ end
@@ -15,7 +15,7 @@ module Datadog
15
15
  module AIGuard
16
16
  # Component for API Guard product
17
17
  class Component
18
- attr_reader :api_client, :logger
18
+ attr_reader :api_client, :logger, :telemetry
19
19
 
20
20
  def self.build(settings, logger:, telemetry:)
21
21
  return unless settings.respond_to?(:ai_guard) && settings.ai_guard.enabled
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module AIGuard
5
+ module Contrib
6
+ # Auto-instrumentation for AI Guard integrations
7
+ module AutoInstrument
8
+ def self.patch_all
9
+ integrations = []
10
+
11
+ Datadog::AIGuard::Contrib::Integration.registry.each_value do |integration|
12
+ next unless integration.klass.auto_instrument?
13
+
14
+ integrations << integration.name
15
+ end
16
+
17
+ integrations.each do |integration_name|
18
+ Datadog.configuration.ai_guard.instrument(integration_name)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../integration"
4
+ require_relative "patcher"
5
+ require_relative "request_middleware"
6
+
7
+ module Datadog
8
+ module AIGuard
9
+ module Contrib
10
+ module Rack
11
+ # Rack integration for AI Guard
12
+ class Integration
13
+ include Datadog::AIGuard::Contrib::Integration
14
+
15
+ MINIMUM_VERSION = Gem::Version.new("1.1.0")
16
+
17
+ register_as :rack, auto_patch: false
18
+
19
+ def self.version
20
+ Gem.loaded_specs["rack"]&.version
21
+ end
22
+
23
+ def self.loaded?
24
+ !defined?(::Rack).nil?
25
+ end
26
+
27
+ def self.compatible?
28
+ super && !!(version&.>= MINIMUM_VERSION)
29
+ end
30
+
31
+ def self.auto_instrument?
32
+ false
33
+ end
34
+
35
+ def patcher
36
+ Patcher
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module AIGuard
5
+ module Contrib
6
+ module Rack
7
+ # Patcher for Rack integration
8
+ module Patcher
9
+ module_function
10
+
11
+ def patched?
12
+ !!Patcher.instance_variable_get(:@patched)
13
+ end
14
+
15
+ def target_version
16
+ Integration.version
17
+ end
18
+
19
+ def patch
20
+ Patcher.instance_variable_set(:@patched, true)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../../tracing/client_ip"
4
+ require_relative "../../../tracing/contrib/rack/header_collection"
5
+ require_relative "../../../tracing/metadata/ext"
6
+ require_relative "../../ext"
7
+
8
+ module Datadog
9
+ module AIGuard
10
+ module Contrib
11
+ module Rack
12
+ # AI Guard Rack middleware.
13
+ #
14
+ # Inserted into the middleware stack right after
15
+ # Datadog::Tracing::Contrib::Rack::TraceMiddleware (i.e. nested inside
16
+ # it). This ordering matters: on the way out of the request, AI Guard's
17
+ # `ensure` block unwinds *before* Tracing's ensure, while Tracing's
18
+ # request span is still live. We need that, because Tracing's ensure
19
+ # calls `request_span.finish`, which builds a frozen `Span` snapshot of
20
+ # the meta hash — any `set_tag` call after that point mutates the
21
+ # `SpanOperation` but never reaches the exported `Span`.
22
+ #
23
+ # So while the span is still active, we tag `http.client_ip` and
24
+ # `network.client.ip` on it — but only when an AI Guard span was
25
+ # actually recorded during the request.
26
+ class RequestMiddleware
27
+ NETWORK_CLIENT_IP_TAG = "network.client.ip"
28
+
29
+ def initialize(app, opt = {})
30
+ @app = app
31
+ end
32
+
33
+ def call(env)
34
+ @app.call(env)
35
+ ensure
36
+ tag_client_ip(env) if consume_ai_guard_executed_flag
37
+ end
38
+
39
+ private
40
+
41
+ # AI Guard's evaluation flow sets `ai_guard.executed` on the trace
42
+ # whenever an AI Guard span is created during the request. We read
43
+ # it here to know whether to tag client IP, then clear it so the
44
+ # internal flag does not propagate to the exported trace.
45
+ #
46
+ # `Tracing.active_trace` is publicly typed as `TraceSegment?` but at
47
+ # runtime returns a `TraceOperation`, which exposes `get_tag` and
48
+ # `clear_tag`. Pre-existing sig mismatch — hence the steep:ignore.
49
+ # steep:ignore:start
50
+ def consume_ai_guard_executed_flag
51
+ trace = Datadog::Tracing.active_trace
52
+ return false unless trace
53
+ return false unless trace.get_tag(Datadog::AIGuard::Ext::SERVICE_ENTRY_EXECUTED_TAG) == "1"
54
+
55
+ trace.clear_tag(Datadog::AIGuard::Ext::SERVICE_ENTRY_EXECUTED_TAG)
56
+ true
57
+ end
58
+ # steep:ignore:end
59
+
60
+ def tag_client_ip(env)
61
+ span = Datadog::Tracing.active_span
62
+ return unless span
63
+
64
+ if span.get_tag(Datadog::Tracing::Metadata::Ext::HTTP::TAG_CLIENT_IP).nil?
65
+ headers = Datadog::Tracing::Contrib::Rack::Header::RequestHeaderCollection.new(env)
66
+ Datadog::Tracing::ClientIp.set_client_ip_tag!(
67
+ span,
68
+ headers: headers,
69
+ remote_ip: env["REMOTE_ADDR"]
70
+ )
71
+ end
72
+
73
+ if env["REMOTE_ADDR"] && span.get_tag(NETWORK_CLIENT_IP_TAG).nil?
74
+ span.set_tag(NETWORK_CLIENT_IP_TAG, env["REMOTE_ADDR"])
75
+ end
76
+ rescue => e
77
+ Datadog::AIGuard.telemetry&.report(e, description: "AI Guard: failed to tag client IP on root span")
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../integration"
4
+ require_relative "patcher"
5
+
6
+ module Datadog
7
+ module AIGuard
8
+ module Contrib
9
+ module Rails
10
+ # Rails integration for AI Guard
11
+ class Integration
12
+ include Datadog::AIGuard::Contrib::Integration
13
+
14
+ MINIMUM_VERSION = Gem::Version.new("4")
15
+
16
+ register_as :rails, auto_patch: false
17
+
18
+ def self.version
19
+ Gem.loaded_specs["railties"]&.version
20
+ end
21
+
22
+ def self.loaded?
23
+ !defined?(::Rails).nil?
24
+ end
25
+
26
+ def self.compatible?
27
+ super && !!(version&.>= MINIMUM_VERSION)
28
+ end
29
+
30
+ def self.auto_instrument?
31
+ true
32
+ end
33
+
34
+ def patcher
35
+ Patcher
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end