datadog 2.43.0 → 2.44.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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -1
  3. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +36 -5
  4. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +6 -7
  5. data/ext/datadog_profiling_native_extension/collectors_stack.c +6 -7
  6. data/ext/datadog_profiling_native_extension/collectors_stack.h +1 -1
  7. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +65 -31
  8. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +3 -2
  9. data/ext/datadog_profiling_native_extension/extconf.rb +4 -2
  10. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +25 -0
  11. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +1 -0
  12. data/ext/datadog_profiling_native_extension/profiling.c +1 -0
  13. data/ext/datadog_profiling_native_extension/stack_recorder.c +4 -4
  14. data/ext/datadog_profiling_native_extension/time_helpers.h +3 -3
  15. data/ext/libdatadog_api/di.c +8 -3
  16. data/ext/libdatadog_api/extconf.rb +1 -1
  17. data/ext/libdatadog_api/feature_flags.c +8 -0
  18. data/ext/libdatadog_api/init.c +3 -1
  19. data/ext/libdatadog_api/otel_thread_context.c +38 -17
  20. data/ext/libdatadog_extconf_helpers.rb +14 -9
  21. data/lib/datadog/ai_guard/component.rb +17 -8
  22. data/lib/datadog/ai_guard/configuration/ext.rb +2 -0
  23. data/lib/datadog/ai_guard/configuration.rb +13 -0
  24. data/lib/datadog/ai_guard/contrib/ruby_llm/chat_instrumentation.rb +48 -55
  25. data/lib/datadog/ai_guard/contrib/ruby_llm/integration.rb +1 -1
  26. data/lib/datadog/ai_guard/contrib/ruby_llm/message_adapter.rb +76 -0
  27. data/lib/datadog/ai_guard/contrib/ruby_llm/patcher.rb +1 -0
  28. data/lib/datadog/ai_guard/contrib/ruby_llm/redaction_change_set.rb +135 -0
  29. data/lib/datadog/ai_guard/evaluation/client.rb +50 -0
  30. data/lib/datadog/ai_guard/evaluation/content_part.rb +12 -0
  31. data/lib/datadog/ai_guard/evaluation/message.rb +25 -7
  32. data/lib/datadog/ai_guard/evaluation/message_builder.rb +31 -0
  33. data/lib/datadog/ai_guard/evaluation/no_op_result.rb +6 -6
  34. data/lib/datadog/ai_guard/evaluation/outcome.rb +24 -0
  35. data/lib/datadog/ai_guard/evaluation/request.rb +4 -60
  36. data/lib/datadog/ai_guard/evaluation/response.rb +33 -0
  37. data/lib/datadog/ai_guard/evaluation/result.rb +11 -18
  38. data/lib/datadog/ai_guard/evaluation/tool_call.rb +15 -2
  39. data/lib/datadog/ai_guard/evaluation.rb +25 -11
  40. data/lib/datadog/ai_guard/ext.rb +1 -0
  41. data/lib/datadog/ai_guard/{api_client.rb → http_client.rb} +6 -4
  42. data/lib/datadog/ai_guard/metrics/telemetry.rb +43 -0
  43. data/lib/datadog/ai_guard/redaction/replacements.rb +97 -0
  44. data/lib/datadog/ai_guard/redaction/result.rb +29 -0
  45. data/lib/datadog/ai_guard/redaction.rb +80 -0
  46. data/lib/datadog/ai_guard.rb +58 -57
  47. data/lib/datadog/core/configuration/components.rb +9 -1
  48. data/lib/datadog/core/configuration/settings.rb +5 -2
  49. data/lib/datadog/core/configuration/supported_configurations.rb +8 -0
  50. data/lib/datadog/core/remote/client/capabilities.rb +10 -4
  51. data/lib/datadog/core/remote/component.rb +9 -2
  52. data/lib/datadog/core/remote/worker.rb +8 -0
  53. data/lib/datadog/di/instrumenter.rb +1 -1
  54. data/lib/datadog/di/probe_manager.rb +10 -5
  55. data/lib/datadog/di/proc_responder.rb +1 -1
  56. data/lib/datadog/open_feature/agentless/configuration_source.rb +246 -0
  57. data/lib/datadog/open_feature/agentless/parser.rb +43 -0
  58. data/lib/datadog/open_feature/agentless/response.rb +19 -0
  59. data/lib/datadog/open_feature/agentless/transport.rb +103 -0
  60. data/lib/datadog/open_feature/component.rb +57 -1
  61. data/lib/datadog/open_feature/configuration/agentless_endpoint.rb +90 -0
  62. data/lib/datadog/open_feature/configuration/source/resolution.rb +22 -0
  63. data/lib/datadog/open_feature/configuration/source.rb +43 -0
  64. data/lib/datadog/open_feature/configuration.rb +118 -0
  65. data/lib/datadog/open_feature/native_evaluator.rb +1 -14
  66. data/lib/datadog/open_feature/remote.rb +5 -5
  67. data/lib/datadog/opentelemetry/signal_configuration.rb +13 -9
  68. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +2 -2
  69. data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +5 -7
  70. data/lib/datadog/symbol_database/extractor.rb +84 -102
  71. data/lib/datadog/tracing/component.rb +2 -0
  72. data/lib/datadog/tracing/configuration/ext.rb +1 -0
  73. data/lib/datadog/tracing/configuration/settings.rb +6 -0
  74. data/lib/datadog/tracing/context.rb +24 -2
  75. data/lib/datadog/tracing/context_provider.rb +9 -5
  76. data/lib/datadog/tracing/contrib/grape/endpoint.rb +32 -5
  77. data/lib/datadog/tracing/metadata/ext.rb +3 -0
  78. data/lib/datadog/tracing/otel_thread_context.rb +76 -11
  79. data/lib/datadog/tracing/tracer.rb +15 -2
  80. data/lib/datadog/tracing/transport/native.rb +1 -1
  81. data/lib/datadog/tracing/transport/trace_formatter.rb +13 -3
  82. data/lib/datadog/tracing/transport/traces.rb +1 -1
  83. data/lib/datadog/version.rb +1 -1
  84. metadata +23 -7
  85. data/lib/datadog/ai_guard/evaluation/content_builder.rb +0 -31
@@ -45,6 +45,7 @@ uint64_t native_thread_id_for(VALUE thread);
45
45
  void ddtrace_thread_list(VALUE result_array);
46
46
  bool is_thread_alive(VALUE thread);
47
47
  VALUE thread_name_for(VALUE thread);
48
+ void self_test_thread_name_for(void);
48
49
 
49
50
  int ddtrace_rb_profile_frames(VALUE thread, int start, int limit, frame_info *stack_buffer);
50
51
 
@@ -103,6 +103,7 @@ void DDTRACE_EXPORT Init_datadog_profiling_native_extension(void) {
103
103
  }
104
104
 
105
105
  static VALUE native_working_p(DDTRACE_UNUSED VALUE _self) {
106
+ self_test_thread_name_for();
106
107
  self_test_clock_id();
107
108
  self_test_current_fiber_for();
108
109
  self_test_mn_enabled();
@@ -539,10 +539,6 @@ static VALUE _native_serialize(DDTRACE_UNUSED VALUE _self, VALUE recorder_instan
539
539
  stack_recorder_state *state;
540
540
  TypedData_Get_Struct(recorder_instance, stack_recorder_state, &stack_recorder_typed_data, state);
541
541
 
542
- ddog_Timespec finish_timestamp = system_epoch_now_timespec();
543
- // Need to do this while still holding the Global VM Lock; see comments on method for why
544
- serializer_set_start_timestamp_for_next_profile(state, finish_timestamp);
545
-
546
542
  if (state->thread_context_collector_instance != Qnil) {
547
543
  thread_context_collector_on_serialize(state->thread_context_collector_instance);
548
544
  }
@@ -555,6 +551,10 @@ static VALUE _native_serialize(DDTRACE_UNUSED VALUE _self, VALUE recorder_instan
555
551
  heap_recorder_prepare_iteration(state->heap_recorder);
556
552
  long heap_iteration_prep_time_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE) - heap_iteration_prep_start_time_ns;
557
553
 
554
+ ddog_Timespec finish_timestamp = system_epoch_now_timespec();
555
+ // Need to do this while still holding the Global VM Lock; see comments on method for why
556
+ serializer_set_start_timestamp_for_next_profile(state, finish_timestamp);
557
+
558
558
  // We'll release the Global VM Lock while we're calling serialize, so that the Ruby VM can continue to work while this
559
559
  // is pending
560
560
  call_serialize_without_gvl_arguments args = {
@@ -7,9 +7,9 @@
7
7
  #include "extconf.h"
8
8
  #include "ruby_helpers.h"
9
9
 
10
- #define SECONDS_AS_NS(value) (value * 1000 * 1000 * 1000L)
11
- #define MILLIS_AS_NS(value) (value * 1000 * 1000L)
12
- #define MICROS_AS_NS(value) (value * 1000L)
10
+ #define SECONDS_AS_NS(value) ((value) * 1000L * 1000L * 1000L)
11
+ #define MILLIS_AS_NS(value) ((value) * 1000L * 1000L)
12
+ #define MICROS_AS_NS(value) ((value) * 1000L)
13
13
 
14
14
  typedef enum { RAISE_ON_FAILURE, DO_NOT_RAISE_ON_FAILURE } raise_on_failure_setting;
15
15
 
@@ -2,6 +2,9 @@
2
2
 
3
3
  #include "datadog_ruby_common.h"
4
4
 
5
+ // rb_iseq is CRuby-specific
6
+ #ifndef TRUFFLERUBY
7
+
5
8
  // Prototypes for Ruby functions declared in internal Ruby headers.
6
9
  // rb_iseqw_new wraps an internal iseq pointer into a Ruby-visible
7
10
  // RubyVM::InstructionSequence object.
@@ -207,7 +210,9 @@ void di_init(VALUE datadog_module) {
207
210
  rb_define_singleton_method(di_module, "enter_probe", enter_probe, 0);
208
211
  rb_define_singleton_method(di_module, "leave_probe", leave_probe, 0);
209
212
  rb_define_singleton_method(di_module, "hash?", is_hash, 1);
210
- #ifdef HAVE_RB_ISEQ_TYPE
211
- rb_define_singleton_method(di_module, "iseq_type", iseq_type, 1);
212
- #endif
213
+ #ifdef HAVE_RB_ISEQ_TYPE
214
+ rb_define_singleton_method(di_module, "iseq_type", iseq_type, 1);
215
+ #endif
213
216
  }
217
+
218
+ #endif // TRUFFLERUBY
@@ -24,7 +24,7 @@ end
24
24
  if ENV["DD_NO_EXTENSION"].to_s.strip.downcase == "true"
25
25
  skip_building_extension!("the `DD_NO_EXTENSION` environment variable is/was set to `true` during installation")
26
26
  end
27
- skip_building_extension!("current Ruby VM is not supported") if RUBY_ENGINE != "ruby"
27
+ skip_building_extension!("current Ruby VM is not supported") unless %w[ruby truffleruby].include?(RUBY_ENGINE)
28
28
  skip_building_extension!("Microsoft Windows is not supported") if Gem.win_platform?
29
29
 
30
30
  libdatadog_issue = Datadog::LibdatadogExtconfHelpers.load_libdatadog_or_get_issue
@@ -13,6 +13,7 @@
13
13
  // Forward declarations
14
14
  static VALUE configuration_new(VALUE klass, VALUE json_str);
15
15
  static void configuration_free(void *ptr);
16
+ static VALUE configuration_get_observe_full_evaluation_data(VALUE self);
16
17
  static VALUE configuration_get_assignment(
17
18
  VALUE self, VALUE flag_key, VALUE expected_type, VALUE context);
18
19
 
@@ -94,6 +95,7 @@ void feature_flags_init(VALUE core_module) {
94
95
  rb_undef_alloc_func(configuration_class);
95
96
  rb_define_singleton_method(configuration_class, "new", configuration_new, 1);
96
97
  rb_define_method(configuration_class, "get_assignment", configuration_get_assignment, 3);
98
+ rb_define_method(configuration_class, "observe_full_evaluation_data", configuration_get_observe_full_evaluation_data, 0);
97
99
 
98
100
  rb_gc_register_address(&resolution_details_class);
99
101
  resolution_details_class = rb_define_class_under(feature_flags_module, "ResolutionDetails", rb_cObject);
@@ -137,6 +139,12 @@ static VALUE configuration_new(VALUE klass, VALUE json_str) {
137
139
  return TypedData_Wrap_Struct(klass, &configuration_data_type, result.ok);
138
140
  }
139
141
 
142
+ static VALUE configuration_get_observe_full_evaluation_data(VALUE self) {
143
+ ddog_ffe_Handle_Configuration configuration =
144
+ (ddog_ffe_Handle_Configuration)rb_check_typeddata(self, &configuration_data_type);
145
+ return ddog_ffe_configuration_get_observe_full_evaluation_data(configuration) ? Qtrue : Qfalse;
146
+ }
147
+
140
148
  static void configuration_free(void *ptr) {
141
149
  ddog_ffe_Handle_Configuration config = (ddog_ffe_Handle_Configuration)ptr;
142
150
  ddog_ffe_configuration_drop(&config);
@@ -25,7 +25,9 @@ void DDTRACE_EXPORT Init_libdatadog_api(void) {
25
25
  library_config_init(core_module);
26
26
  ddsketch_init(core_module);
27
27
  feature_flags_init(core_module);
28
- di_init(datadog_module);
28
+ #ifndef TRUFFLERUBY
29
+ di_init(datadog_module);
30
+ #endif
29
31
 
30
32
  VALUE tracing_module = rb_define_module_under(datadog_module, "Tracing");
31
33
  trace_exporter_init(tracing_module);
@@ -4,7 +4,12 @@
4
4
  #include "datadog_ruby_common.h"
5
5
  #include "otel_thread_context.h"
6
6
 
7
- #ifdef __linux__
7
+ // Relies on a :fiber_switch TracePoint that TruffleRuby does not support yet
8
+ #if defined(__linux__) && !defined(TRUFFLERUBY)
9
+ #define OTEL_THREAD_CONTEXT_SUPPORTED
10
+ #endif
11
+
12
+ #ifdef OTEL_THREAD_CONTEXT_SUPPORTED
8
13
  #include <datadog/otel-thread-ctx.h>
9
14
  extern __thread const uint8_t *otel_thread_ctx_v1;
10
15
 
@@ -32,18 +37,18 @@ static VALUE native_read(VALUE _self);
32
37
  void otel_thread_context_init(VALUE tracing_module) {
33
38
  fiber_context_slot = rb_intern("__dd_otel_fiber_context");
34
39
 
35
- VALUE otel_thread_context_module = rb_define_module_under(tracing_module, "OTelThreadContext");
40
+ VALUE otel_thread_context_class = rb_define_class_under(tracing_module, "OTelThreadContext", rb_cObject);
36
41
 
37
- rb_define_singleton_method(otel_thread_context_module, "_native_enable", native_enable, 0);
38
- rb_define_singleton_method(otel_thread_context_module, "_native_set", native_set, 3);
39
- rb_define_singleton_method(otel_thread_context_module, "_native_clear", native_clear, 0);
40
- rb_define_singleton_method(otel_thread_context_module, "_native_supported?", native_supported_p, 0);
42
+ rb_define_method(otel_thread_context_class, "_native_enable", native_enable, 0);
43
+ rb_define_method(otel_thread_context_class, "_native_set", native_set, 3);
44
+ rb_define_method(otel_thread_context_class, "_native_clear", native_clear, 0);
45
+ rb_define_method(otel_thread_context_class, "_native_supported?", native_supported_p, 0);
41
46
 
42
- VALUE testing_module = rb_define_module_under(otel_thread_context_module, "Testing");
47
+ VALUE testing_module = rb_define_module_under(otel_thread_context_class, "Testing");
43
48
  rb_define_singleton_method(testing_module, "_native_read", native_read, 0);
44
49
  }
45
50
 
46
- #ifdef __linux__
51
+ #ifdef OTEL_THREAD_CONTEXT_SUPPORTED
47
52
  static ddog_ThreadContextHandle *get_fiber_handle_for(VALUE thread) {
48
53
  VALUE obj = rb_thread_local_aref(thread, fiber_context_slot);
49
54
 
@@ -56,6 +61,22 @@ void otel_thread_context_init(VALUE tracing_module) {
56
61
  return get_fiber_handle_for(rb_thread_current());
57
62
  }
58
63
 
64
+ static void pack_span_id(VALUE id, uint8_t bytes[8]) {
65
+ // Tracing::Utils.next_id caps generated span IDs at (1 << 62) - 1, which fits
66
+ // in a Fixnum on 64-bit Ruby. Packing them directly avoids rb_integer_pack's
67
+ // generic conversion and format checks on each context update.
68
+ // https://github.com/DataDog/dd-trace-rb/blob/0b10368e0a2940c1f4d50c9739b9973b4f8bc32a/lib/datadog/tracing/utils.rb#L20-L37
69
+ if (FIXNUM_P(id)) {
70
+ uint64_t number = (uint64_t) FIX2LONG(id);
71
+ for (int i = 7; i >= 0; i--) {
72
+ bytes[i] = (uint8_t) number;
73
+ number >>= 8;
74
+ }
75
+ } else {
76
+ rb_integer_pack(id, bytes, 1, sizeof(uint64_t), 0, INTEGER_PACK_BIG_ENDIAN);
77
+ }
78
+ }
79
+
59
80
  static void store_current_fiber_handle(ddog_ThreadContextHandle *handle) {
60
81
  VALUE obj = TypedData_Wrap_Struct(rb_cObject, &otel_ctx_handle_t, handle);
61
82
  rb_thread_local_aset(rb_thread_current(), fiber_context_slot, obj);
@@ -137,7 +158,7 @@ void otel_thread_context_init(VALUE tracing_module) {
137
158
  #endif
138
159
 
139
160
  static VALUE native_enable(DDTRACE_UNUSED VALUE _self) {
140
- #ifdef __linux__
161
+ #ifdef OTEL_THREAD_CONTEXT_SUPPORTED
141
162
  if (otel_context_enabled) return Qtrue;
142
163
  otel_context_enabled = true;
143
164
 
@@ -163,7 +184,7 @@ static VALUE native_enable(DDTRACE_UNUSED VALUE _self) {
163
184
  }
164
185
 
165
186
  static VALUE native_supported_p(DDTRACE_UNUSED VALUE _self) {
166
- #ifdef __linux__
187
+ #ifdef OTEL_THREAD_CONTEXT_SUPPORTED
167
188
  return Qtrue;
168
189
  #else
169
190
  return Qfalse;
@@ -176,17 +197,17 @@ static VALUE native_set(
176
197
  DDTRACE_UNUSED VALUE span_id,
177
198
  DDTRACE_UNUSED VALUE local_root_span_id
178
199
  ) {
179
- #ifdef __linux__
200
+ #ifdef OTEL_THREAD_CONTEXT_SUPPORTED
180
201
  if (!otel_context_enabled) return Qfalse;
181
202
 
182
203
  uint8_t trace_id_bytes[16];
183
204
  uint8_t span_id_bytes[8];
184
205
  uint8_t local_root_span_id_bytes[8];
185
206
 
186
- const int BIG_ENDIAN_PACK_FLAGS = INTEGER_PACK_MSWORD_FIRST | INTEGER_PACK_BIG_ENDIAN;
187
- rb_integer_pack(trace_id, trace_id_bytes, sizeof(trace_id_bytes), 1, 0, BIG_ENDIAN_PACK_FLAGS);
188
- rb_integer_pack(span_id, span_id_bytes, sizeof(span_id_bytes), 1, 0, BIG_ENDIAN_PACK_FLAGS);
189
- rb_integer_pack(local_root_span_id, local_root_span_id_bytes, sizeof(local_root_span_id_bytes), 1, 0, BIG_ENDIAN_PACK_FLAGS);
207
+ // Note: We use 2 words of size 8, as `rb_integer_pack` has a fast path for it (vs 16 bytes of size 1)
208
+ rb_integer_pack(trace_id, trace_id_bytes, 2, sizeof(uint64_t), 0, INTEGER_PACK_BIG_ENDIAN);
209
+ pack_span_id(span_id, span_id_bytes);
210
+ pack_span_id(local_root_span_id, local_root_span_id_bytes);
190
211
 
191
212
  ddog_ThreadContextHandle *handle = get_current_fiber_handle();
192
213
 
@@ -207,7 +228,7 @@ static VALUE native_set(
207
228
  }
208
229
 
209
230
  static VALUE native_clear(DDTRACE_UNUSED VALUE _self) {
210
- #ifdef __linux__
231
+ #ifdef OTEL_THREAD_CONTEXT_SUPPORTED
211
232
  if (!otel_context_enabled) return Qfalse;
212
233
 
213
234
  ddog_ThreadContextHandle *detached = ddog_otel_thread_ctx_detach();
@@ -220,7 +241,7 @@ static VALUE native_clear(DDTRACE_UNUSED VALUE _self) {
220
241
  }
221
242
 
222
243
  static VALUE native_read(DDTRACE_UNUSED VALUE _self) {
223
- #ifdef __linux__
244
+ #ifdef OTEL_THREAD_CONTEXT_SUPPORTED
224
245
  const uint8_t *raw = otel_thread_ctx_v1;
225
246
  if (!raw) return Qnil;
226
247
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "rubygems"
4
4
  require "pathname"
5
+ require "shellwords"
5
6
 
6
7
  module Datadog
7
8
  # Contains a bunch of shared helpers that get used during building of extensions that link to libdatadog
@@ -10,7 +11,7 @@ module Datadog
10
11
  module LibdatadogExtconfHelpers
11
12
  # Used to make sure the correct gem version gets loaded, as extconf.rb does not get run with "bundle exec" and thus
12
13
  # may see multiple libdatadog versions. See https://github.com/DataDog/dd-trace-rb/pull/2531 for the horror story.
13
- LIBDATADOG_VERSION = "~> 40.0.0.2.0"
14
+ LIBDATADOG_VERSION = "~> 44.0.1.1.0"
14
15
 
15
16
  # Used as an workaround for a limitation with how dynamic linking works in environments where the datadog gem and
16
17
  # libdatadog are moved after the extension gets compiled.
@@ -116,7 +117,10 @@ module Datadog
116
117
  extconf_folder:,
117
118
  libdatadog_pkgconfig_folder: Libdatadog.pkgconfig_folder,
118
119
  gem_dir: Gem.dir,
119
- logger: Logging
120
+ logger: Logging,
121
+ target_incflags: $INCFLAGS,
122
+ target_ldflags: $LDFLAGS,
123
+ target_libs: $libs
120
124
  )
121
125
  return unless libdatadog_pkgconfig_folder
122
126
 
@@ -124,10 +128,9 @@ module Datadog
124
128
  libdir = "#{libdatadog_pkgconfig_folder}/../../lib"
125
129
  includedir = "#{libdatadog_pkgconfig_folder}/../../include"
126
130
 
127
- # Set mkmf global variables
128
- $INCFLAGS << " -I#{includedir}"
129
- $LDFLAGS << " -L#{libdir} -Wl,-rpath,#{libdir}"
130
- $libs << " -ldatadog_profiling"
131
+ target_incflags << " -I#{Shellwords.escape(includedir)}"
132
+ target_ldflags << " -L#{Shellwords.escape(libdir)} -Wl,-rpath,#{Shellwords.escape(libdir)}"
133
+ target_libs << " -ldatadog_profiling"
131
134
 
132
135
  # Add extra relative rpaths using $ORIGIN to handle environments where gems are moved after installation.
133
136
  # The excessive escaping is needed to get these special characters through Make and the shell untouched.
@@ -141,10 +144,12 @@ module Datadog
141
144
  libdatadog_pkgconfig_folder: libdatadog_pkgconfig_folder,
142
145
  ),
143
146
  ]
144
- extra_relative_rpaths.each { |folder| $LDFLAGS << " -Wl,-rpath,$$$\\\\{ORIGIN\\}/#{folder}" }
147
+ extra_relative_rpaths.each { |folder| target_ldflags << " -Wl,-rpath,$$$\\\\{ORIGIN\\}/#{Shellwords.escape(folder)}" }
145
148
 
146
- logger.message("linking with libdatadog (include=#{includedir}, lib=#{libdir})\n")
147
- logger.message("[datadog] $LDFLAGS were set to: #{$LDFLAGS.inspect}\n")
149
+ # includedir/libdir/ldflags might have a `%` in there and `logger.message` bottoms out on `printf` so we need to
150
+ # use the `message("%s", ...)` format otherwise the logger will complain
151
+ logger.message("%s", "linking with libdatadog (include=#{includedir}, lib=#{libdir})\n")
152
+ logger.message("%s", "[datadog] $LDFLAGS were set to: #{target_ldflags.inspect}\n")
148
153
 
149
154
  true
150
155
  end
@@ -1,37 +1,46 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "api_client"
3
+ require "json"
4
+
5
+ require_relative "ext"
6
+ require_relative "http_client"
4
7
  require_relative "evaluation"
5
8
  require_relative "evaluation/request"
9
+ require_relative "evaluation/response"
10
+ require_relative "evaluation/client"
6
11
  require_relative "evaluation/result"
12
+ require_relative "evaluation/outcome"
7
13
  require_relative "evaluation/no_op_result"
8
14
  require_relative "evaluation/message"
9
15
  require_relative "evaluation/tool_call"
10
16
  require_relative "evaluation/content_part"
11
- require_relative "evaluation/content_builder"
12
- require_relative "ext"
17
+ require_relative "evaluation/message_builder"
18
+ require_relative "redaction"
19
+ require_relative "redaction/result"
20
+ require_relative "redaction/replacements"
21
+ require_relative "metrics/telemetry"
13
22
 
14
23
  module Datadog
15
24
  module AIGuard
16
25
  # Component for API Guard product
17
26
  class Component
18
- attr_reader :api_client, :logger, :telemetry
27
+ attr_reader :http_client, :logger, :telemetry
19
28
 
20
29
  def self.build(settings, logger:, telemetry:)
21
30
  return unless settings.respond_to?(:ai_guard) && settings.ai_guard.enabled
22
31
 
23
- api_client = APIClient.new(
32
+ http_client = HTTPClient.new(
24
33
  endpoint: settings.ai_guard.endpoint,
25
34
  api_key: settings.api_key,
26
35
  application_key: settings.ai_guard.app_key,
27
36
  timeout: settings.ai_guard.timeout_ms / 1_000
28
37
  )
29
38
 
30
- new(api_client, logger: logger, telemetry: telemetry)
39
+ new(http_client, logger: logger, telemetry: telemetry)
31
40
  end
32
41
 
33
- def initialize(api_client, logger:, telemetry:)
34
- @api_client = api_client
42
+ def initialize(http_client, logger:, telemetry:)
43
+ @http_client = http_client
35
44
  @logger = logger
36
45
  @telemetry = telemetry
37
46
  end
@@ -6,6 +6,8 @@ module Datadog
6
6
  # This module contains constants for AI Guard component
7
7
  module Ext
8
8
  ENV_AI_GUARD_ENABLED = "DD_AI_GUARD_ENABLED"
9
+ ENV_AI_GUARD_REDACTION_ENABLED = "DD_AI_GUARD_REDACTION_ENABLED"
10
+
9
11
  ENV_AI_GUARD_ENDPOINT = "DD_AI_GUARD_ENDPOINT"
10
12
  ENV_AI_GUARD_TIMEOUT = "DD_AI_GUARD_TIMEOUT"
11
13
  ENV_AI_GUARD_MAX_CONTENT_SIZE = "DD_AI_GUARD_MAX_CONTENT_SIZE"
@@ -34,6 +34,19 @@ module Datadog
34
34
  o.default false
35
35
  end
36
36
 
37
+ # Redact sensitive data from evaluated messages.
38
+ #
39
+ # When enabled, values flagged by AI Guard are replaced before the messages are
40
+ # stored in the span or returned to the caller. Disable to skip redaction entirely.
41
+ #
42
+ # @default `DD_AI_GUARD_REDACTION_ENABLED`, otherwise `true`
43
+ # @return [Boolean]
44
+ option :redaction_enabled do |o|
45
+ o.type :bool
46
+ o.env Ext::ENV_AI_GUARD_REDACTION_ENABLED
47
+ o.default true
48
+ end
49
+
37
50
  define_method(:instrument) do |integration_name|
38
51
  return unless enabled # steep:ignore
39
52
 
@@ -4,74 +4,67 @@ module Datadog
4
4
  module AIGuard
5
5
  module Contrib
6
6
  module RubyLLM
7
- # module that gets prepended to RubyLLM::Chat
7
+ # Protects tool calls before RubyLLM executes them locally
8
+ #
9
+ # @api private
8
10
  module ChatInstrumentation
9
- class << self
10
- def evaluate!(messages)
11
- ai_guard_messages = messages.flat_map do |message|
12
- if message.tool_call?
13
- message.tool_calls.map do |tool_call_id, tool_call|
14
- AIGuard.assistant(id: tool_call_id, tool_name: tool_call.name, arguments: tool_call.arguments.to_s)
15
- end
16
- elsif message.tool_result?
17
- build_ai_guard_tool(message)
18
- else
19
- build_ai_guard_message(message)
20
- end
21
- end
11
+ private
22
12
 
23
- AIGuard.evaluate(*ai_guard_messages)
24
- end
25
-
26
- private
13
+ def provider_completion(usage_recorder:, stream_tracker: nil, &block)
14
+ @_datadog_ai_guard_evaluate = true
15
+ super
16
+ ensure
17
+ @_datadog_ai_guard_evaluate = false
18
+ end
27
19
 
28
- def build_ai_guard_message(message)
29
- content = message.content
20
+ def preprocessed_messages(list = messages)
21
+ return super unless @_datadog_ai_guard_evaluate
30
22
 
31
- case content
32
- when ::RubyLLM::Content
33
- AIGuard.message(role: message.role) do |m|
34
- m.text(content.text.to_s) if content.text
23
+ adapter = MessageAdapter.new(list)
35
24
 
36
- # Calling attachment.for_llm triggers lazy loading of file contents.
37
- # The result is memoized, so providers won't re-read.
38
- content.attachments.each do |attachment|
39
- case attachment.type
40
- when :image
41
- m.image_url(attachment.for_llm)
42
- when :text
43
- m.text(attachment.for_llm)
44
- end
45
- # Skip :pdf, :audio, :video, :unknown — not supported by AIGuard
46
- end
47
- end
48
- else
49
- AIGuard.message(role: message.role, content: content)
50
- end
25
+ begin
26
+ converted_messages = adapter.to_ai_guard
27
+ rescue JSON::JSONError
28
+ Metrics::Telemetry.report_error
29
+ return super
30
+ rescue => e
31
+ AIGuard.telemetry&.report(e, description: "AI Guard: Failed to convert RubyLLM messages")
32
+ return super
51
33
  end
52
34
 
53
- def build_ai_guard_tool(message)
54
- content = message.content
55
- # Tools can return Content or Content::Raw objects (e.g. with attachments),
56
- # but AIGuard.tool expects a String. Extract text when content is a Content object.
57
- case content
58
- when ::RubyLLM::Content
59
- content = content.text.to_s
60
- end
61
- AIGuard.tool(tool_call_id: message.tool_call_id, content: content)
35
+ evaluation = AIGuard.evaluate(*converted_messages)
36
+
37
+ begin
38
+ redacted_messages = adapter.apply_redactions(evaluation.messages)
39
+ rescue JSON::JSONError => e
40
+ AIGuard.telemetry&.report(e, description: "AI Guard: Failed to apply RubyLLM redaction")
41
+ return super
62
42
  end
43
+
44
+ super(redacted_messages)
63
45
  end
64
46
 
65
- def complete(&block)
66
- Datadog::AIGuard::Contrib::RubyLLM::ChatInstrumentation.evaluate!(messages)
47
+ def execute_pending_tool_calls(response)
48
+ response_index = messages.index { |message| message == response }
49
+ return super unless response_index
67
50
 
68
- super
69
- end
51
+ adapter = MessageAdapter.new(messages)
52
+ begin
53
+ converted_messages = adapter.to_ai_guard
54
+ rescue JSON::JSONError
55
+ Metrics::Telemetry.report_error
56
+ return super
57
+ end
70
58
 
71
- def handle_tool_calls(response, &block)
72
- Datadog::AIGuard::Contrib::RubyLLM::ChatInstrumentation.evaluate!(messages)
59
+ evaluation = AIGuard.evaluate(*converted_messages)
60
+ begin
61
+ redacted_messages = adapter.apply_redactions(evaluation.messages)
62
+ rescue JSON::JSONError => e
63
+ AIGuard.telemetry&.report(e, description: "AI Guard: Failed to apply RubyLLM redaction")
64
+ return super
65
+ end
73
66
 
74
- super
67
+ super(redacted_messages[response_index])
75
68
  end
76
69
  end
77
70
  end
@@ -11,7 +11,7 @@ module Datadog
11
11
  class Integration
12
12
  include Datadog::AIGuard::Contrib::Integration
13
13
 
14
- MINIMUM_VERSION = Gem::Version.new("1.0.0")
14
+ MINIMUM_VERSION = Gem::Version.new("2.0.0")
15
15
 
16
16
  register_as :ruby_llm, auto_patch: false
17
17
 
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "redaction_change_set"
4
+
5
+ module Datadog
6
+ module AIGuard
7
+ module Contrib
8
+ module RubyLLM
9
+ # RubyLLM messages paired with their AI Guard representation
10
+ #
11
+ # @api private
12
+ class MessageAdapter
13
+ def initialize(messages)
14
+ @ruby_llm_messages = messages
15
+ end
16
+
17
+ def to_ai_guard
18
+ @ai_guard_messages ||= @ruby_llm_messages.map { |message| build_ai_guard_message(message) }
19
+ end
20
+
21
+ def apply_redactions(redacted_messages)
22
+ ai_guard_messages = to_ai_guard
23
+ return @ruby_llm_messages if redacted_messages == ai_guard_messages
24
+
25
+ @ruby_llm_messages.each_with_index.map do |message, index|
26
+ original_message = ai_guard_messages[index]
27
+ redacted_message = redacted_messages[index]
28
+ next message if redacted_message == original_message
29
+
30
+ RedactionChangeSet.new(original_message, redacted_message).apply_to(message)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def build_ai_guard_message(message)
37
+ builder = Evaluation::MessageBuilder.new
38
+
39
+ # NOTE: Hash block arguments are misinterpreted by Steep after `nil.to_h`
40
+ # steep:ignore:start
41
+ message.tool_calls.to_h.each do |id, tool_call|
42
+ builder.tool_call(name: tool_call.name, id: id, arguments: tool_call.arguments)
43
+ end
44
+ # steep:ignore:end
45
+
46
+ if message.attachments.empty?
47
+ content = message.content
48
+ else
49
+ builder.text(message.content) if message.content && !message.content.empty?
50
+
51
+ message.attachments.each do |attachment|
52
+ next if attachment.provider_file?
53
+
54
+ case attachment.type
55
+ when :text
56
+ builder.text(attachment.content)
57
+ when :image
58
+ # NOTE: Local images are Base64-encoded here by `#for_llm`
59
+ # and again when RubyLLM builds the provider request
60
+ url = attachment.url? ? attachment.source.to_s : attachment.for_llm
61
+ builder.image_url(url)
62
+ end
63
+ end
64
+
65
+ content = builder.content
66
+ end
67
+
68
+ Evaluation::Message.new(
69
+ role: message.role, content: content, tool_calls: builder.tool_calls, tool_call_id: message.tool_call_id
70
+ )
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "message_adapter"
3
4
  require_relative "chat_instrumentation"
4
5
 
5
6
  module Datadog