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.
- checksums.yaml +4 -4
- data/ext/datadog_profiling_native_extension/clock_id.h +9 -1
- data/ext/datadog_profiling_native_extension/clock_id_from_mach.c +73 -0
- data/ext/datadog_profiling_native_extension/clock_id_from_pthread.c +1 -1
- data/ext/datadog_profiling_native_extension/collectors_thread_context.c +5 -1
- data/ext/datadog_profiling_native_extension/extconf.rb +3 -0
- data/ext/datadog_profiling_native_extension/stack_recorder.c +3 -9
- data/ext/datadog_profiling_native_extension/time_helpers.h +1 -0
- data/ext/libdatadog_api/crashtracker.c +2 -0
- data/ext/libdatadog_extconf_helpers.rb +1 -1
- data/lib/datadog/ai_guard/autoload.rb +10 -0
- data/lib/datadog/ai_guard/component.rb +1 -1
- data/lib/datadog/ai_guard/contrib/auto_instrument.rb +24 -0
- data/lib/datadog/ai_guard/contrib/rack/integration.rb +42 -0
- data/lib/datadog/ai_guard/contrib/rack/patcher.rb +26 -0
- data/lib/datadog/ai_guard/contrib/rack/request_middleware.rb +83 -0
- data/lib/datadog/ai_guard/contrib/rails/integration.rb +41 -0
- data/lib/datadog/ai_guard/contrib/rails/patcher.rb +97 -0
- data/lib/datadog/ai_guard/evaluation.rb +1 -0
- data/lib/datadog/ai_guard/ext.rb +1 -0
- data/lib/datadog/ai_guard.rb +8 -0
- data/lib/datadog/appsec/contrib/aws_lambda/gateway/watcher.rb +75 -0
- data/lib/datadog/appsec/contrib/aws_lambda/integration.rb +39 -0
- data/lib/datadog/appsec/contrib/aws_lambda/patcher.rb +30 -0
- data/lib/datadog/appsec/contrib/aws_lambda/waf_addresses.rb +111 -0
- data/lib/datadog/appsec.rb +1 -0
- data/lib/datadog/core/configuration/settings.rb +10 -0
- data/lib/datadog/core/configuration/supported_configurations.rb +2 -0
- data/lib/datadog/core/environment/ext.rb +1 -0
- data/lib/datadog/core/environment/socket.rb +13 -0
- data/lib/datadog/opentelemetry/metrics.rb +10 -1
- data/lib/datadog/opentelemetry/sdk/id_generator.rb +16 -10
- data/lib/datadog/profiling/component.rb +0 -1
- data/lib/datadog/profiling/stack_recorder.rb +0 -4
- data/lib/datadog/symbol_database/extractor.rb +17 -26
- data/lib/datadog/symbol_database/scope.rb +16 -12
- data/lib/datadog/symbol_database/scope_batcher.rb +280 -0
- data/lib/datadog/symbol_database/service_version.rb +4 -4
- data/lib/datadog/symbol_database/uploader.rb +3 -0
- data/lib/datadog/tracing/contrib/rack/configuration/settings.rb +6 -0
- data/lib/datadog/tracing/contrib/rack/ext.rb +27 -0
- data/lib/datadog/tracing/contrib/rack/trace_proxy_middleware.rb +117 -1
- data/lib/datadog/tracing/tracer.rb +1 -3
- data/lib/datadog/version.rb +1 -1
- metadata +19 -7
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ff1c01535f382bb6e3de5b529bf8ec30a6deda1d1c4aba3450bb21dbd80407bf
|
|
4
|
+
data.tar.gz: 966a01b956c9df2cd8d451df9175b66e0dbae3b7b3da7732ea998c7adca70442
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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;
|
|
@@ -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 = '~>
|
|
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
|
data/lib/datadog/ai_guard/ext.rb
CHANGED
data/lib/datadog/ai_guard.rb
CHANGED
|
@@ -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
|