datadog 2.32.0 → 2.33.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/ext/datadog_profiling_native_extension/clock_id.h +9 -1
  3. data/ext/datadog_profiling_native_extension/clock_id_from_mach.c +73 -0
  4. data/ext/datadog_profiling_native_extension/clock_id_from_pthread.c +1 -1
  5. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +5 -1
  6. data/ext/datadog_profiling_native_extension/extconf.rb +3 -0
  7. data/ext/datadog_profiling_native_extension/stack_recorder.c +3 -9
  8. data/ext/datadog_profiling_native_extension/time_helpers.h +1 -0
  9. data/ext/libdatadog_api/crashtracker.c +2 -0
  10. data/ext/libdatadog_extconf_helpers.rb +1 -1
  11. data/lib/datadog/ai_guard/autoload.rb +10 -0
  12. data/lib/datadog/ai_guard/component.rb +1 -1
  13. data/lib/datadog/ai_guard/contrib/auto_instrument.rb +24 -0
  14. data/lib/datadog/ai_guard/contrib/rack/integration.rb +42 -0
  15. data/lib/datadog/ai_guard/contrib/rack/patcher.rb +26 -0
  16. data/lib/datadog/ai_guard/contrib/rack/request_middleware.rb +83 -0
  17. data/lib/datadog/ai_guard/contrib/rails/integration.rb +41 -0
  18. data/lib/datadog/ai_guard/contrib/rails/patcher.rb +97 -0
  19. data/lib/datadog/ai_guard/evaluation.rb +1 -0
  20. data/lib/datadog/ai_guard/ext.rb +1 -0
  21. data/lib/datadog/ai_guard.rb +8 -0
  22. data/lib/datadog/appsec/contrib/aws_lambda/gateway/watcher.rb +75 -0
  23. data/lib/datadog/appsec/contrib/aws_lambda/integration.rb +39 -0
  24. data/lib/datadog/appsec/contrib/aws_lambda/patcher.rb +30 -0
  25. data/lib/datadog/appsec/contrib/aws_lambda/waf_addresses.rb +111 -0
  26. data/lib/datadog/appsec.rb +1 -0
  27. data/lib/datadog/core/configuration/settings.rb +10 -0
  28. data/lib/datadog/core/configuration/supported_configurations.rb +2 -0
  29. data/lib/datadog/core/environment/ext.rb +1 -0
  30. data/lib/datadog/core/environment/socket.rb +13 -0
  31. data/lib/datadog/opentelemetry/metrics.rb +10 -1
  32. data/lib/datadog/opentelemetry/sdk/id_generator.rb +16 -10
  33. data/lib/datadog/profiling/component.rb +0 -1
  34. data/lib/datadog/profiling/stack_recorder.rb +0 -4
  35. data/lib/datadog/symbol_database/extractor.rb +17 -26
  36. data/lib/datadog/symbol_database/scope.rb +16 -12
  37. data/lib/datadog/symbol_database/scope_batcher.rb +280 -0
  38. data/lib/datadog/symbol_database/service_version.rb +4 -4
  39. data/lib/datadog/symbol_database/uploader.rb +3 -0
  40. data/lib/datadog/tracing/contrib/rack/configuration/settings.rb +6 -0
  41. data/lib/datadog/tracing/contrib/rack/ext.rb +27 -0
  42. data/lib/datadog/tracing/contrib/rack/trace_proxy_middleware.rb +117 -1
  43. data/lib/datadog/tracing/tracer.rb +1 -3
  44. data/lib/datadog/version.rb +1 -1
  45. metadata +19 -7
  46. 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: ff1c01535f382bb6e3de5b529bf8ec30a6deda1d1c4aba3450bb21dbd80407bf
4
+ data.tar.gz: 966a01b956c9df2cd8d451df9175b66e0dbae3b7b3da7732ea998c7adca70442
5
5
  SHA512:
6
- metadata.gz: c664b9d447c8645e4a202f04e5b2f690b85f15b62689f3312a8d5d3ccb2a08ec289a6d07fa414eee8e5dd92a3bea9f040b758fbfed97ab602bdcf4a989e47d6b
7
- data.tar.gz: 92ce248e3e351d774b043921c18cd73f3ba064eed1203246a81ef1de9500cb4c6220fc7df81187b5b49bd4062fa87228bf8be8817c599315da8853752740ab96
6
+ metadata.gz: c2d4111c1b0526122be94729577ba54d1e85fb08f2cd1ae74495d9de5bd805cb883b39c872187a5a10f2d4f67a3153d6bce2dda8192e75f4b0e808c9ab9b6a79
7
+ data.tar.gz: 413cb6dc25ca7b8a79e9e85b152bfd4eed649f72ca5275a46203e30c12f893914337cbd8e5136767588b67f81eadefdc839a5a4aafebbdf5cc17e7d089798886
@@ -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>
@@ -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"
@@ -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
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../../core/utils/only_once"
4
+ require_relative "../rack/request_middleware"
5
+ require_relative "../../../tracing/contrib"
6
+ require_relative "../../../tracing/contrib/rack/middlewares"
7
+
8
+ module Datadog
9
+ module AIGuard
10
+ module Contrib
11
+ module Rails
12
+ # Patcher for AI Guard on Rails. Inserts the AI Guard Rack middleware
13
+ # right after the Tracing Rack middleware so the request span is
14
+ # already active when AI Guard tags the client IP.
15
+ module Patcher
16
+ BEFORE_INITIALIZE_ONLY_ONCE_PER_APP = Hash.new { |h, key| h[key] = Datadog::Core::Utils::OnlyOnce.new }
17
+
18
+ module_function
19
+
20
+ def patched?
21
+ !!Patcher.instance_variable_get(:@patched)
22
+ end
23
+
24
+ def target_version
25
+ Integration.version
26
+ end
27
+
28
+ def patch
29
+ patch_before_initialize
30
+ Patcher.instance_variable_set(:@patched, true)
31
+ end
32
+
33
+ def patch_before_initialize
34
+ ::ActiveSupport.on_load(:before_initialize) do
35
+ Datadog::AIGuard::Contrib::Rails::Patcher.before_initialize(self)
36
+ end
37
+ end
38
+
39
+ def before_initialize(app)
40
+ BEFORE_INITIALIZE_ONLY_ONCE_PER_APP[app].run do
41
+ # Middleware must be added before the application is initialized.
42
+ # Otherwise the middleware stack will be frozen.
43
+ add_middleware(app) if Datadog.configuration.tracing[:rails][:middleware]
44
+ end
45
+ end
46
+
47
+ def add_middleware(app)
48
+ if include_middleware?(Datadog::Tracing::Contrib::Rack::TraceMiddleware, app)
49
+ app.middleware.insert_after(
50
+ Datadog::Tracing::Contrib::Rack::TraceMiddleware,
51
+ Datadog::AIGuard::Contrib::Rack::RequestMiddleware
52
+ )
53
+ else
54
+ app.middleware.insert_before(0, Datadog::AIGuard::Contrib::Rack::RequestMiddleware)
55
+ end
56
+ end
57
+
58
+ def include_middleware?(middleware, app)
59
+ found = false
60
+
61
+ # find tracer middleware reference in Rails::Configuration::MiddlewareStackProxy
62
+ app.middleware.instance_variable_get(:@operations).each do |operation|
63
+ args = case operation
64
+ when Array
65
+ # rails 5.2
66
+ _op, args = operation
67
+ args
68
+ when Proc
69
+ if operation.binding.local_variables.include?(:args)
70
+ # rails 6.0, 6.1
71
+ operation.binding.local_variable_get(:args)
72
+ else
73
+ # rails 7.0 uses ... to pass args
74
+ # steep:ignore:start
75
+ args_getter = Class.new do
76
+ def method_missing(_op, *args) # standard:disable Style/MissingRespondToMissing
77
+ args
78
+ end
79
+ end.new
80
+ # steep:ignore:end
81
+ operation.call(args_getter)
82
+ end
83
+ else
84
+ # unknown, pass through
85
+ []
86
+ end
87
+
88
+ found = true if args.include?(middleware)
89
+ end
90
+
91
+ found
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -16,6 +16,7 @@ module Datadog
16
16
  Tracing::Sampling::Ext::Decision::AI_GUARD
17
17
  )
18
18
  trace.set_tag(Ext::EVENT_TAG, true)
19
+ trace.set_tag(Ext::SERVICE_ENTRY_EXECUTED_TAG, "1")
19
20
 
20
21
  if (last_message = messages.last)
21
22
  if last_message.tool_call
@@ -11,6 +11,7 @@ module Datadog
11
11
  REASON_TAG = "ai_guard.reason"
12
12
  BLOCKED_TAG = "ai_guard.blocked"
13
13
  EVENT_TAG = "ai_guard.event"
14
+ SERVICE_ENTRY_EXECUTED_TAG = "_dd.ai_guard.executed"
14
15
  METASTRUCT_TAG = "ai_guard"
15
16
  end
16
17
  end
@@ -3,6 +3,8 @@
3
3
  require_relative "core/configuration"
4
4
  require_relative "ai_guard/configuration"
5
5
 
6
+ require_relative "ai_guard/contrib/rack/integration"
7
+ require_relative "ai_guard/contrib/rails/integration"
6
8
  require_relative "ai_guard/contrib/ruby_llm/integration"
7
9
 
8
10
  module Datadog
@@ -50,6 +52,10 @@ module Datadog
50
52
  Datadog.send(:components).ai_guard&.logger
51
53
  end
52
54
 
55
+ def telemetry
56
+ Datadog.send(:components).ai_guard&.telemetry
57
+ end
58
+
53
59
  # Evaluates one or more messages using AI Guard API.
54
60
  #
55
61
  # Example:
@@ -171,3 +177,5 @@ module Datadog
171
177
  end
172
178
  end
173
179
  end
180
+
181
+ require_relative "ai_guard/autoload"
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../waf_addresses'
4
+ require_relative '../../../event'
5
+ require_relative '../../../trace_keeper'
6
+ require_relative '../../../security_event'
7
+ require_relative '../../../instrumentation/gateway'
8
+
9
+ module Datadog
10
+ module AppSec
11
+ module Contrib
12
+ module AwsLambda
13
+ module Gateway
14
+ module Watcher
15
+ class << self
16
+ def watch
17
+ gateway = Instrumentation.gateway
18
+
19
+ watch_request(gateway)
20
+ watch_response(gateway)
21
+ end
22
+
23
+ def watch_request(gateway = Instrumentation.gateway)
24
+ gateway.watch('aws_lambda.request.start') do |stack, payload|
25
+ context = payload.context
26
+ next stack.call(payload) unless context
27
+
28
+ persistent_data = WAFAddresses.from_request(payload.data)
29
+ result = context.run_waf(persistent_data, {}, Datadog.configuration.appsec.waf_timeout)
30
+
31
+ if result.match? || !result.attributes.empty?
32
+ context.events.push(
33
+ AppSec::SecurityEvent.new(result, trace: context.trace, span: context.span)
34
+ )
35
+ end
36
+
37
+ if result.match?
38
+ AppSec::Event.tag(context, result)
39
+ TraceKeeper.keep!(context.trace) if result.keep?
40
+ AppSec::ActionsHandler.handle(result.actions)
41
+ end
42
+
43
+ stack.call(payload)
44
+ end
45
+ end
46
+
47
+ def watch_response(gateway = Instrumentation.gateway)
48
+ gateway.watch('aws_lambda.response.start') do |stack, payload|
49
+ context = payload.context
50
+ next stack.call(payload) unless context
51
+
52
+ persistent_data = WAFAddresses.from_response(payload.data)
53
+ result = context.run_waf(persistent_data, {}, Datadog.configuration.appsec.waf_timeout)
54
+
55
+ if result.match?
56
+ AppSec::Event.tag(context, result)
57
+ TraceKeeper.keep!(context.trace) if result.keep?
58
+
59
+ context.events.push(
60
+ AppSec::SecurityEvent.new(result, trace: context.trace, span: context.span)
61
+ )
62
+
63
+ AppSec::ActionsHandler.handle(result.actions)
64
+ end
65
+
66
+ stack.call(payload)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end