datadog 2.34.0 → 2.36.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 (168) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +114 -6
  3. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +68 -31
  4. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +1 -1
  5. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +1 -1
  6. data/ext/datadog_profiling_native_extension/collectors_stack.c +37 -18
  7. data/ext/datadog_profiling_native_extension/collectors_stack.h +8 -2
  8. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +435 -314
  9. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +9 -7
  10. data/ext/datadog_profiling_native_extension/datadog_ruby_common.c +7 -8
  11. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +0 -12
  12. data/ext/datadog_profiling_native_extension/extconf.rb +2 -2
  13. data/ext/datadog_profiling_native_extension/gvl_profiling_helper.c +4 -43
  14. data/ext/datadog_profiling_native_extension/gvl_profiling_helper.h +15 -47
  15. data/ext/datadog_profiling_native_extension/heap_recorder.c +44 -26
  16. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +14 -35
  17. data/ext/datadog_profiling_native_extension/profiling.c +41 -4
  18. data/ext/datadog_profiling_native_extension/ruby_helpers.c +33 -34
  19. data/ext/datadog_profiling_native_extension/stack_recorder.c +30 -14
  20. data/ext/datadog_profiling_native_extension/stack_recorder.h +1 -0
  21. data/ext/datadog_profiling_native_extension/unsafe_api_calls_check.h +4 -2
  22. data/ext/libdatadog_api/datadog_ruby_common.c +7 -8
  23. data/ext/libdatadog_api/datadog_ruby_common.h +0 -12
  24. data/ext/libdatadog_extconf_helpers.rb +1 -1
  25. data/lib/datadog/ai_guard/configuration.rb +1 -0
  26. data/lib/datadog/ai_guard/contrib/rack/request_middleware.rb +53 -39
  27. data/lib/datadog/ai_guard/evaluation.rb +6 -1
  28. data/lib/datadog/ai_guard/ext.rb +12 -1
  29. data/lib/datadog/appsec/api_security/route_extractor.rb +9 -0
  30. data/lib/datadog/appsec/component.rb +1 -1
  31. data/lib/datadog/appsec/configuration.rb +7 -0
  32. data/lib/datadog/appsec/contrib/aws_lambda/waf_addresses.rb +37 -4
  33. data/lib/datadog/appsec/contrib/graphql/gateway/multiplex.rb +64 -19
  34. data/lib/datadog/appsec/contrib/graphql/integration.rb +1 -0
  35. data/lib/datadog/appsec/contrib/rack/buffered_input.rb +83 -0
  36. data/lib/datadog/appsec/contrib/rack/gateway/request.rb +41 -3
  37. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +20 -7
  38. data/lib/datadog/appsec/contrib/rack/input_peeker.rb +92 -0
  39. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +9 -40
  40. data/lib/datadog/appsec/contrib/rails/gateway/request.rb +33 -0
  41. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +17 -1
  42. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +20 -3
  43. data/lib/datadog/appsec/default_header_tags.rb +52 -0
  44. data/lib/datadog/core/configuration/components.rb +1 -0
  45. data/lib/datadog/core/configuration/settings.rb +11 -8
  46. data/lib/datadog/core/configuration/supported_configurations.rb +2 -0
  47. data/lib/datadog/core/environment/process.rb +4 -2
  48. data/lib/datadog/core/remote/component.rb +1 -1
  49. data/lib/datadog/core/telemetry/event/app_started.rb +0 -21
  50. data/lib/datadog/core/utils/at_fork_monkey_patch.rb +1 -1
  51. data/lib/datadog/core/utils/{array.rb → enumerable_compat.rb} +2 -2
  52. data/lib/datadog/core/utils/forking.rb +3 -1
  53. data/lib/datadog/core/utils/spawn_monkey_patch.rb +3 -1
  54. data/lib/datadog/core.rb +3 -0
  55. data/lib/datadog/di/base.rb +4 -1
  56. data/lib/datadog/di/component.rb +1 -1
  57. data/lib/datadog/error_tracking/collector.rb +2 -1
  58. data/lib/datadog/error_tracking/component.rb +2 -2
  59. data/lib/datadog/kit/tracing/method_tracer.rb +4 -1
  60. data/lib/datadog/opentelemetry/sdk/propagator.rb +9 -3
  61. data/lib/datadog/opentelemetry/sdk/span_processor.rb +4 -1
  62. data/lib/datadog/profiling/collectors/thread_context.rb +1 -4
  63. data/lib/datadog/profiling/component.rb +15 -25
  64. data/lib/datadog/profiling/ext/dir_monkey_patches.rb +6 -5
  65. data/lib/datadog/profiling/stack_recorder.rb +0 -4
  66. data/lib/datadog/ruby_version.rb +25 -0
  67. data/lib/datadog/symbol_database/component.rb +306 -98
  68. data/lib/datadog/symbol_database/extractor.rb +226 -87
  69. data/lib/datadog/tracing/configuration/ext.rb +13 -0
  70. data/lib/datadog/tracing/configuration/settings.rb +17 -0
  71. data/lib/datadog/tracing/contrib/action_cable/events/broadcast.rb +4 -1
  72. data/lib/datadog/tracing/contrib/action_cable/events/perform_action.rb +4 -1
  73. data/lib/datadog/tracing/contrib/action_cable/events/transmit.rb +4 -1
  74. data/lib/datadog/tracing/contrib/action_cable/instrumentation.rb +4 -1
  75. data/lib/datadog/tracing/contrib/action_mailer/event.rb +4 -1
  76. data/lib/datadog/tracing/contrib/action_pack/action_controller/instrumentation.rb +1 -0
  77. data/lib/datadog/tracing/contrib/action_view/events/render_partial.rb +4 -1
  78. data/lib/datadog/tracing/contrib/action_view/events/render_template.rb +4 -1
  79. data/lib/datadog/tracing/contrib/active_job/events/discard.rb +4 -1
  80. data/lib/datadog/tracing/contrib/active_job/events/enqueue.rb +4 -1
  81. data/lib/datadog/tracing/contrib/active_job/events/enqueue_at.rb +4 -1
  82. data/lib/datadog/tracing/contrib/active_job/events/enqueue_retry.rb +4 -1
  83. data/lib/datadog/tracing/contrib/active_job/events/perform.rb +4 -1
  84. data/lib/datadog/tracing/contrib/active_job/events/retry_stopped.rb +4 -1
  85. data/lib/datadog/tracing/contrib/active_record/events/sql.rb +1 -0
  86. data/lib/datadog/tracing/contrib/active_support/cache/events/cache.rb +1 -0
  87. data/lib/datadog/tracing/contrib/active_support/cache/instrumentation.rb +1 -0
  88. data/lib/datadog/tracing/contrib/aws/instrumentation.rb +1 -0
  89. data/lib/datadog/tracing/contrib/configuration/resolver.rb +7 -0
  90. data/lib/datadog/tracing/contrib/dalli/instrumentation.rb +1 -0
  91. data/lib/datadog/tracing/contrib/delayed_job/plugin.rb +2 -0
  92. data/lib/datadog/tracing/contrib/delayed_job/server_internal_tracer/worker.rb +1 -0
  93. data/lib/datadog/tracing/contrib/elasticsearch/patcher.rb +1 -0
  94. data/lib/datadog/tracing/contrib/elasticsearch/quantize.rb +2 -2
  95. data/lib/datadog/tracing/contrib/ethon/easy_patch.rb +1 -0
  96. data/lib/datadog/tracing/contrib/ethon/multi_patch.rb +1 -0
  97. data/lib/datadog/tracing/contrib/excon/middleware.rb +1 -0
  98. data/lib/datadog/tracing/contrib/ext.rb +3 -0
  99. data/lib/datadog/tracing/contrib/faraday/middleware.rb +1 -0
  100. data/lib/datadog/tracing/contrib/grape/endpoint.rb +3 -0
  101. data/lib/datadog/tracing/contrib/graphql/unified_trace.rb +1 -0
  102. data/lib/datadog/tracing/contrib/grpc/datadog_interceptor/client.rb +1 -0
  103. data/lib/datadog/tracing/contrib/grpc/datadog_interceptor/server.rb +1 -0
  104. data/lib/datadog/tracing/contrib/grpc/distributed/propagation.rb +2 -0
  105. data/lib/datadog/tracing/contrib/grpc.rb +1 -0
  106. data/lib/datadog/tracing/contrib/hanami/action_tracer.rb +1 -0
  107. data/lib/datadog/tracing/contrib/hanami/renderer_policy_tracing.rb +1 -0
  108. data/lib/datadog/tracing/contrib/hanami/router_tracing.rb +1 -0
  109. data/lib/datadog/tracing/contrib/http/distributed/propagation.rb +2 -0
  110. data/lib/datadog/tracing/contrib/http/instrumentation.rb +1 -0
  111. data/lib/datadog/tracing/contrib/http.rb +1 -0
  112. data/lib/datadog/tracing/contrib/httpclient/instrumentation.rb +1 -0
  113. data/lib/datadog/tracing/contrib/httprb/instrumentation.rb +1 -0
  114. data/lib/datadog/tracing/contrib/kafka/event.rb +1 -0
  115. data/lib/datadog/tracing/contrib/karafka/distributed/propagation.rb +2 -0
  116. data/lib/datadog/tracing/contrib/karafka.rb +1 -0
  117. data/lib/datadog/tracing/contrib/mongodb/parsers.rb +5 -5
  118. data/lib/datadog/tracing/contrib/mongodb/subscribers.rb +2 -0
  119. data/lib/datadog/tracing/contrib/mysql2/instrumentation.rb +1 -0
  120. data/lib/datadog/tracing/contrib/opensearch/patcher.rb +1 -0
  121. data/lib/datadog/tracing/contrib/opensearch/quantize.rb +2 -2
  122. data/lib/datadog/tracing/contrib/pg/instrumentation.rb +1 -0
  123. data/lib/datadog/tracing/contrib/presto/instrumentation.rb +3 -0
  124. data/lib/datadog/tracing/contrib/propagation/sql_comment/ext.rb +3 -0
  125. data/lib/datadog/tracing/contrib/propagation/sql_comment/mode.rb +2 -2
  126. data/lib/datadog/tracing/contrib/que/tracer.rb +1 -0
  127. data/lib/datadog/tracing/contrib/racecar/event.rb +1 -0
  128. data/lib/datadog/tracing/contrib/rack/header_tagging.rb +23 -0
  129. data/lib/datadog/tracing/contrib/rack/middlewares.rb +4 -1
  130. data/lib/datadog/tracing/contrib/rack/route_inference.rb +3 -1
  131. data/lib/datadog/tracing/contrib/rack/trace_proxy_middleware.rb +2 -0
  132. data/lib/datadog/tracing/contrib/rails/runner.rb +2 -0
  133. data/lib/datadog/tracing/contrib/rake/instrumentation.rb +4 -2
  134. data/lib/datadog/tracing/contrib/redis/trace_middleware.rb +2 -0
  135. data/lib/datadog/tracing/contrib/resque/resque_job.rb +1 -0
  136. data/lib/datadog/tracing/contrib/rest_client/request_patch.rb +1 -0
  137. data/lib/datadog/tracing/contrib/roda/ext.rb +1 -0
  138. data/lib/datadog/tracing/contrib/roda/instrumentation.rb +4 -1
  139. data/lib/datadog/tracing/contrib/sequel/database.rb +1 -0
  140. data/lib/datadog/tracing/contrib/sequel/dataset.rb +1 -0
  141. data/lib/datadog/tracing/contrib/shoryuken/tracer.rb +1 -0
  142. data/lib/datadog/tracing/contrib/sidekiq/client_tracer.rb +1 -0
  143. data/lib/datadog/tracing/contrib/sidekiq/distributed/propagation.rb +2 -0
  144. data/lib/datadog/tracing/contrib/sidekiq/server_internal_tracer/heartbeat.rb +2 -0
  145. data/lib/datadog/tracing/contrib/sidekiq/server_internal_tracer/job_fetch.rb +1 -0
  146. data/lib/datadog/tracing/contrib/sidekiq/server_internal_tracer/redis_info.rb +1 -0
  147. data/lib/datadog/tracing/contrib/sidekiq/server_internal_tracer/scheduled_poller.rb +2 -0
  148. data/lib/datadog/tracing/contrib/sidekiq/server_internal_tracer/stop.rb +1 -0
  149. data/lib/datadog/tracing/contrib/sidekiq/server_tracer.rb +3 -2
  150. data/lib/datadog/tracing/contrib/sidekiq.rb +1 -0
  151. data/lib/datadog/tracing/contrib/sinatra/tracer.rb +1 -0
  152. data/lib/datadog/tracing/contrib/sinatra/tracer_middleware.rb +1 -0
  153. data/lib/datadog/tracing/contrib/sneakers/tracer.rb +1 -0
  154. data/lib/datadog/tracing/contrib/sucker_punch/instrumentation.rb +1 -0
  155. data/lib/datadog/tracing/contrib/trilogy/instrumentation.rb +1 -0
  156. data/lib/datadog/tracing/contrib/utils/quantization/{hash.rb → hash_formatter.rb} +1 -1
  157. data/lib/datadog/tracing/contrib/waterdrop/distributed/propagation.rb +2 -0
  158. data/lib/datadog/tracing/contrib/waterdrop.rb +1 -0
  159. data/lib/datadog/tracing/distributed/propagation.rb +33 -1
  160. data/lib/datadog/tracing/distributed/trace_context.rb +11 -2
  161. data/lib/datadog/tracing/metadata/ext.rb +7 -0
  162. data/lib/datadog/tracing/trace_digest.rb +7 -0
  163. data/lib/datadog/tracing/trace_operation.rb +4 -1
  164. data/lib/datadog/tracing/tracer.rb +4 -0
  165. data/lib/datadog/tracing/transport/traces.rb +2 -2
  166. data/lib/datadog/version.rb +1 -1
  167. data/lib/datadog.rb +4 -1
  168. metadata +13 -11
@@ -21,42 +21,40 @@ void ruby_helpers_init(void) {
21
21
  to_s_id = rb_intern("to_s");
22
22
  }
23
23
 
24
- // Internal helper for raising pre-formatted syserr exceptions
25
- static NORETURN(void private_raise_syserr_formatted(int syserr_errno, const char *detailed_message, const char *static_message)) {
26
- VALUE exception = rb_syserr_new(syserr_errno, detailed_message);
27
- private_raise_exception(exception, static_message);
28
- }
29
-
30
24
  // Use `raise_syserr` the macro instead, as it provides additional argument checks.
31
25
  void private_raise_syserr(int syserr_errno, const char *fmt, ...) {
32
- FORMAT_VA_ERROR_MESSAGE(detailed_message, fmt);
33
- private_raise_syserr_formatted(syserr_errno, detailed_message, fmt);
26
+ va_list args;
27
+ va_start(args, fmt);
28
+ VALUE detailed_message = rb_vsprintf(fmt, args);
29
+ va_end(args);
30
+
31
+ VALUE exception = rb_syserr_new_str(syserr_errno, detailed_message);
32
+ private_raise_exception(exception, fmt);
34
33
  }
35
34
 
36
35
  typedef struct {
37
36
  VALUE exception_class;
38
37
  int syserr_errno;
39
- char exception_message[MAX_RAISE_MESSAGE_SIZE];
40
- char telemetry_message[MAX_RAISE_MESSAGE_SIZE];
38
+ const char *format_string;
39
+ va_list va_args;
41
40
  } raise_args;
42
41
 
42
+ // Called via rb_thread_call_with_gvl from private_grab_gvl_and_raise.
43
+ // Formats the message with rb_vsprintf (which requires the GVL) and raises.
43
44
  static void *trigger_raise(void *raise_arguments) {
44
45
  raise_args *args = (raise_args *) raise_arguments;
45
46
 
47
+ VALUE detailed_message = rb_vsprintf(args->format_string, args->va_args);
48
+
49
+ VALUE exception;
46
50
  if (args->syserr_errno) {
47
- private_raise_syserr_formatted(
48
- args->syserr_errno,
49
- args->exception_message,
50
- args->telemetry_message
51
- );
51
+ exception = rb_syserr_new_str(args->syserr_errno, detailed_message);
52
52
  } else {
53
- private_raise_error_formatted(
54
- args->exception_class,
55
- args->exception_message,
56
- args->telemetry_message
57
- );
53
+ exception = rb_exc_new_str(args->exception_class, detailed_message);
58
54
  }
59
55
 
56
+ private_raise_exception(exception, args->format_string);
57
+
60
58
  return NULL;
61
59
  }
62
60
 
@@ -71,11 +69,17 @@ void private_grab_gvl_and_raise(VALUE exception_class, int syserr_errno, const c
71
69
  args.syserr_errno = 0;
72
70
  }
73
71
 
74
- FORMAT_VA_ERROR_MESSAGE(formatted_exception_message, format_string);
75
- snprintf(args.exception_message, MAX_RAISE_MESSAGE_SIZE, "%s", formatted_exception_message);
76
- snprintf(args.telemetry_message, MAX_RAISE_MESSAGE_SIZE, "%s", format_string);
72
+ args.format_string = format_string;
73
+ va_start(args.va_args, format_string);
77
74
 
78
75
  if (is_current_thread_holding_the_gvl()) {
76
+ VALUE detailed_message = rb_vsprintf(format_string, args.va_args);
77
+ va_end(args.va_args);
78
+
79
+ VALUE wrapped_message = rb_sprintf(
80
+ "grab_gvl_and_raise called by thread holding the global VM lock: %"PRIsVALUE,
81
+ detailed_message
82
+ );
79
83
  char telemetry_message[MAX_RAISE_MESSAGE_SIZE];
80
84
  snprintf(
81
85
  telemetry_message,
@@ -83,20 +87,14 @@ void private_grab_gvl_and_raise(VALUE exception_class, int syserr_errno, const c
83
87
  "grab_gvl_and_raise called by thread holding the global VM lock: %s",
84
88
  format_string
85
89
  );
86
- char exception_message[MAX_RAISE_MESSAGE_SIZE];
87
- snprintf(
88
- exception_message,
89
- MAX_RAISE_MESSAGE_SIZE,
90
- "grab_gvl_and_raise called by thread holding the global VM lock: %s",
91
- args.exception_message
92
- );
93
- VALUE exception = rb_exc_new_cstr(rb_eRuntimeError, exception_message);
90
+ VALUE exception = rb_exc_new_str(rb_eRuntimeError, wrapped_message);
94
91
  private_raise_exception(exception, telemetry_message);
95
92
  }
96
93
 
97
94
  rb_thread_call_with_gvl(trigger_raise, &args);
98
95
 
99
- rb_bug("[ddtrace] Unexpected: Reached the end of grab_gvl_and_raise while raising '%s'\n", args.exception_message);
96
+ va_end(args.va_args);
97
+ rb_bug("[ddtrace] Unexpected: Reached the end of grab_gvl_and_raise while raising '%s'\n", format_string);
100
98
  }
101
99
 
102
100
  void private_raise_enforce_syserr(
@@ -107,10 +105,11 @@ void private_raise_enforce_syserr(
107
105
  int line,
108
106
  const char *function_name
109
107
  ) {
108
+ const char *format = "Failure returned by '%s' at %s:%d:in `%s'";
110
109
  if (have_gvl) {
111
- rb_exc_raise(rb_syserr_new_str(syserr_errno, rb_sprintf("Failure returned by '%s' at %s:%d:in `%s'", expression, file, line, function_name)));
110
+ private_raise_exception(rb_syserr_new_str(syserr_errno, rb_sprintf(format, expression, file, line, function_name)), format);
112
111
  } else {
113
- private_grab_gvl_and_raise(Qnil, syserr_errno, "Failure returned by '%s' at %s:%d:in `%s'", expression, file, line, function_name);
112
+ private_grab_gvl_and_raise(Qnil, syserr_errno, format, expression, file, line, function_name);
114
113
  }
115
114
  }
116
115
 
@@ -9,6 +9,7 @@
9
9
  #include "time_helpers.h"
10
10
  #include "heap_recorder.h"
11
11
  #include "encoded_profile.h"
12
+ #include "collectors_thread_context.h"
12
13
 
13
14
  // Used to wrap a ddog_prof_Profile in a Ruby object and expose Ruby-level serialization APIs
14
15
  // This file implements the native bits of the Datadog::Profiling::StackRecorder class
@@ -177,6 +178,10 @@ typedef struct {
177
178
  heap_recorder *heap_recorder;
178
179
  bool heap_clean_after_gc_enabled;
179
180
 
181
+ // When set, _native_serialize will call thread_context_collector_on_serialize on this instance
182
+ // before serializing, so that threads suspended across the whole profile period still get sampled.
183
+ VALUE thread_context_collector_instance;
184
+
180
185
  pthread_mutex_t mutex_slot_one;
181
186
  profile_slot profile_slot_one;
182
187
  pthread_mutex_t mutex_slot_two;
@@ -320,6 +325,7 @@ static VALUE _native_new(VALUE klass) {
320
325
  // being leaked.
321
326
 
322
327
  state->heap_clean_after_gc_enabled = false;
328
+ state->thread_context_collector_instance = Qnil;
323
329
 
324
330
  ddog_prof_Slice_SampleType sample_types = {.ptr = all_sample_types, .len = ALL_VALUE_TYPES_COUNT};
325
331
 
@@ -391,6 +397,7 @@ static void initialize_profiles(stack_recorder_state *state, ddog_prof_Slice_Sam
391
397
  static void stack_recorder_typed_data_mark(void *state_ptr) {
392
398
  stack_recorder_state *state = (stack_recorder_state *) state_ptr;
393
399
 
400
+ rb_gc_mark(state->thread_context_collector_instance);
394
401
  heap_recorder_mark_pending_recordings(state->heap_recorder);
395
402
  }
396
403
 
@@ -420,14 +427,12 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
420
427
  VALUE heap_samples_enabled = rb_hash_fetch(options, ID2SYM(rb_intern("heap_samples_enabled")));
421
428
  VALUE heap_size_enabled = rb_hash_fetch(options, ID2SYM(rb_intern("heap_size_enabled")));
422
429
  VALUE heap_sample_every = rb_hash_fetch(options, ID2SYM(rb_intern("heap_sample_every")));
423
- VALUE timeline_enabled = rb_hash_fetch(options, ID2SYM(rb_intern("timeline_enabled")));
424
430
  VALUE heap_clean_after_gc_enabled = rb_hash_fetch(options, ID2SYM(rb_intern("heap_clean_after_gc_enabled")));
425
431
 
426
432
  ENFORCE_BOOLEAN(alloc_samples_enabled);
427
433
  ENFORCE_BOOLEAN(heap_samples_enabled);
428
434
  ENFORCE_BOOLEAN(heap_size_enabled);
429
435
  ENFORCE_TYPE(heap_sample_every, T_FIXNUM);
430
- ENFORCE_BOOLEAN(timeline_enabled);
431
436
  ENFORCE_BOOLEAN(heap_clean_after_gc_enabled);
432
437
 
433
438
  stack_recorder_state *state;
@@ -440,8 +445,7 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
440
445
  uint8_t requested_values_count = ALL_VALUE_TYPES_COUNT -
441
446
  (alloc_samples_enabled == Qtrue? 0 : 2) -
442
447
  (heap_samples_enabled == Qtrue ? 0 : 1) -
443
- (heap_size_enabled == Qtrue ? 0 : 1) -
444
- (timeline_enabled == Qtrue ? 0 : 1);
448
+ (heap_size_enabled == Qtrue ? 0 : 1);
445
449
 
446
450
  if (requested_values_count == ALL_VALUE_TYPES_COUNT) return Qtrue; // Nothing to do, this is the default
447
451
 
@@ -467,6 +471,10 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
467
471
  enabled_sample_types[next_enabled_pos] = DDOG_PROF_SAMPLE_TYPE_CPU_TIME;
468
472
  state->position_for[CPU_TIME_VALUE_ID] = next_enabled_pos++;
469
473
 
474
+ // TIMELINE is always enabled
475
+ enabled_sample_types[next_enabled_pos] = DDOG_PROF_SAMPLE_TYPE_TIMELINE;
476
+ state->position_for[TIMELINE_VALUE_ID] = next_enabled_pos++;
477
+
470
478
  if (alloc_samples_enabled == Qtrue) {
471
479
  enabled_sample_types[next_enabled_pos] = DDOG_PROF_SAMPLE_TYPE_ALLOC_SAMPLES;
472
480
  state->position_for[ALLOC_SAMPLES_VALUE_ID] = next_enabled_pos++;
@@ -500,13 +508,6 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
500
508
  state->heap_recorder = NULL;
501
509
  }
502
510
 
503
- if (timeline_enabled == Qtrue) {
504
- enabled_sample_types[next_enabled_pos] = DDOG_PROF_SAMPLE_TYPE_TIMELINE;
505
- state->position_for[TIMELINE_VALUE_ID] = next_enabled_pos++;
506
- } else {
507
- state->position_for[TIMELINE_VALUE_ID] = next_disabled_pos++;
508
- }
509
-
510
511
  ddog_prof_Profile_drop(&state->profile_slot_one.profile);
511
512
  ddog_prof_Profile_drop(&state->profile_slot_two.profile);
512
513
 
@@ -521,12 +522,16 @@ static VALUE _native_serialize(DDTRACE_UNUSED VALUE _self, VALUE recorder_instan
521
522
  TypedData_Get_Struct(recorder_instance, stack_recorder_state, &stack_recorder_typed_data, state);
522
523
 
523
524
  ddog_Timespec finish_timestamp = system_epoch_now_timespec();
524
- // Need to do this while still holding on to the Global VM Lock; see comments on method for why
525
+ // Need to do this while still holding the Global VM Lock; see comments on method for why
525
526
  serializer_set_start_timestamp_for_next_profile(state, finish_timestamp);
526
527
 
528
+ if (state->thread_context_collector_instance != Qnil) {
529
+ thread_context_collector_on_serialize(state->thread_context_collector_instance);
530
+ }
531
+
527
532
  long heap_iteration_prep_start_time_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE);
528
533
  // Prepare the iteration on heap recorder we'll be doing outside the GVL. The preparation needs to
529
- // happen while holding on to the GVL.
534
+ // happen while holding the GVL.
530
535
  // NOTE: While rare, it's possible for the GVL to be released inside this function (see comments on `heap_recorder_update`)
531
536
  // and thus don't assume this is an "atomic" step -- other threads may get some running time in the meanwhile.
532
537
  heap_recorder_prepare_iteration(state->heap_recorder);
@@ -554,7 +559,7 @@ static VALUE _native_serialize(DDTRACE_UNUSED VALUE _self, VALUE recorder_instan
554
559
  rb_thread_call_without_gvl2(call_serialize_without_gvl, &args, NULL /* No interruption function needed in this case */, NULL /* Not needed */);
555
560
  }
556
561
 
557
- // Cleanup after heap recorder iteration. This needs to happen while holding on to the GVL.
562
+ // Cleanup after heap recorder iteration. This needs to happen while holding the GVL.
558
563
  heap_recorder_finish_iteration(state->heap_recorder);
559
564
 
560
565
  // NOTE: We are focusing on the serialization time outside of the GVL in this stat here. This doesn't
@@ -639,6 +644,7 @@ void record_sample(VALUE recorder_instance, ddog_prof_Slice_Location locations,
639
644
  .values = (ddog_Slice_I64) {.ptr = metric_values, .len = state->enabled_values_count},
640
645
  .labels = labels.labels
641
646
  },
647
+ /* To disable end_timestamp_ns to get aggregated profiles in pprof, replace the below with `0` */
642
648
  labels.end_timestamp_ns
643
649
  );
644
650
 
@@ -899,6 +905,9 @@ static ddog_Timespec system_epoch_now_timespec(void) {
899
905
  //
900
906
  // Assumption: This method gets called BEFORE restarting profiling -- e.g. there are no components attempting to
901
907
  // trigger samples at the same time.
908
+ //
909
+ // Note that tests call this method directly in the same process without forking,
910
+ // and in such a case non-current Threads keep running.
902
911
  static VALUE _native_reset_after_fork(DDTRACE_UNUSED VALUE self, VALUE recorder_instance) {
903
912
  stack_recorder_state *state;
904
913
  TypedData_Get_Struct(recorder_instance, stack_recorder_state, &stack_recorder_typed_data, state);
@@ -1152,3 +1161,10 @@ static VALUE _native_finalize_pending_heap_recordings(DDTRACE_UNUSED VALUE _self
1152
1161
 
1153
1162
  return Qtrue;
1154
1163
  }
1164
+
1165
+ void recorder_install_on_serialize(VALUE recorder_instance, VALUE thread_context_collector_instance) {
1166
+ stack_recorder_state *state;
1167
+ TypedData_Get_Struct(recorder_instance, stack_recorder_state, &stack_recorder_typed_data, state);
1168
+
1169
+ state->thread_context_collector_instance = enforce_thread_context_collector_instance(thread_context_collector_instance);
1170
+ }
@@ -29,4 +29,5 @@ void record_endpoint(VALUE recorder_instance, uint64_t local_root_span_id, ddog_
29
29
  __attribute__((warn_unused_result)) bool track_object(VALUE recorder_instance, VALUE new_object, unsigned int sample_weight, ddog_CharSlice alloc_class);
30
30
  void recorder_after_sample(VALUE recorder_instance);
31
31
  void recorder_after_gc_step(VALUE recorder_instance);
32
+ void recorder_install_on_serialize(VALUE recorder_instance, VALUE thread_context_collector_instance);
32
33
  VALUE enforce_recorder_instance(VALUE object);
@@ -4,7 +4,7 @@
4
4
  //
5
5
  // Specifically, when the profiler is sampling, we're never supposed to call into Ruby code (e.g. methods
6
6
  // implemented using Ruby code) or allocate Ruby objects.
7
- // That's because those events introduce thread switch points, and really we don't the VM switching between threads
7
+ // That's because those events introduce thread switch points, and really we don't want the VM switching between threads
8
8
  // in the middle of the profiler sampling. This includes raising exceptions.
9
9
  //
10
10
  // Raising exceptions as the very last operation, to stop the profiler is ok, but comes a caveat: raising exceptions
@@ -18,10 +18,12 @@
18
18
  // in most (all?) thread switch points, Ruby will check for interrupts and run the postponed jobs.
19
19
  //
20
20
  // Thus, if we set a flag while we're sampling (inside_unsafe_context), trigger the postponed job, and then only unset
21
- // the flag after sampling, he correct thing to happen is that the postponed job should never see the flag.
21
+ // the flag after sampling, the correct thing to happen is that the postponed job should never see the flag.
22
22
  //
23
23
  // If, however, we have a bug and there's a thread switch point, our postponed job will see the flag and immediately
24
24
  // stop the Ruby VM before further damage happens (and hopefully giving us a stack trace clearly pointing to the culprit).
25
+ //
26
+ // Note that this check currently does not detect Ruby object allocations, as those do not check for interrupts.
25
27
 
26
28
  void unsafe_api_calls_check_init(void);
27
29
 
@@ -29,16 +29,15 @@ void private_raise_exception(VALUE exception, const char *static_message) {
29
29
  rb_exc_raise(exception);
30
30
  }
31
31
 
32
- // Helper for raising pre-formatted exceptions
33
- void private_raise_error_formatted(VALUE exception_class, const char *detailed_message, const char *static_message) {
34
- VALUE exception = rb_exc_new_cstr(exception_class, detailed_message);
35
- private_raise_exception(exception, static_message);
36
- }
37
-
38
32
  // Use `raise_error` the macro instead, as it provides additional argument checks.
39
33
  void private_raise_error(VALUE exception_class, const char *fmt, ...) {
40
- FORMAT_VA_ERROR_MESSAGE(detailed_message, fmt);
41
- private_raise_error_formatted(exception_class, detailed_message, fmt);
34
+ va_list args;
35
+ va_start(args, fmt);
36
+ VALUE detailed_message = rb_vsprintf(fmt, args);
37
+ va_end(args);
38
+
39
+ VALUE exception = rb_exc_new_str(exception_class, detailed_message);
40
+ private_raise_exception(exception, fmt);
42
41
  }
43
42
 
44
43
  VALUE datadog_gem_version(void) {
@@ -47,11 +47,6 @@ NORETURN(
47
47
  __attribute__ ((format (printf, 2, 3)));
48
48
  );
49
49
 
50
- // Internal helper for raising pre-formatted exceptions
51
- NORETURN(
52
- void private_raise_error_formatted(VALUE exception_class, const char *detailed_message, const char *static_message)
53
- );
54
-
55
50
  // Raises an exception with separate telemetry-safe and detailed messages.
56
51
  // NOTE: Raising an exception always invokes Ruby code so it requires the GVL and is not compatible with "debug_enter_unsafe_context".
57
52
  // @see debug_enter_unsafe_context
@@ -61,13 +56,6 @@ NORETURN(
61
56
 
62
57
  #define MAX_RAISE_MESSAGE_SIZE 256
63
58
 
64
- #define FORMAT_VA_ERROR_MESSAGE(buf, fmt) \
65
- char buf[MAX_RAISE_MESSAGE_SIZE]; \
66
- va_list buf##_args; \
67
- va_start(buf##_args, fmt); \
68
- vsnprintf(buf, MAX_RAISE_MESSAGE_SIZE, fmt, buf##_args); \
69
- va_end(buf##_args);
70
-
71
59
  // Helper to retrieve Datadog::VERSION::STRING
72
60
  VALUE datadog_gem_version(void);
73
61
 
@@ -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 = '~> 33.0.0.1.0'
13
+ LIBDATADOG_VERSION = '~> 35.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.
@@ -71,6 +71,7 @@ module Datadog
71
71
  option :app_key do |o|
72
72
  o.type :string, nilable: true
73
73
  o.env Ext::ENV_APP_KEY
74
+ o.skip_telemetry true
74
75
  end
75
76
 
76
77
  # Request timeout in milliseconds.
@@ -11,71 +11,85 @@ module Datadog
11
11
  module Rack
12
12
  # AI Guard Rack middleware.
13
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`.
14
+ # At request entry the middleware stores some request attributes for
15
+ # on the active trace under the `_dd.ai_guard.` prefix.
22
16
  #
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.
17
+ # Later when AI Guard evaluation is performed, those attributes are
18
+ # mirrored on AI Guard span.
26
19
  class RequestMiddleware
27
- NETWORK_CLIENT_IP_TAG = "network.client.ip"
28
-
29
20
  def initialize(app, opt = {})
30
21
  @app = app
31
22
  end
32
23
 
33
24
  def call(env)
25
+ trace = Datadog::Tracing.active_trace
26
+ return @app.call(env) unless trace
27
+
28
+ store_anomaly_detection_tags!(trace, env)
29
+
34
30
  @app.call(env)
35
31
  ensure
36
- tag_client_ip(env) if consume_ai_guard_executed_flag
32
+ # @type var trace: Datadog::Tracing::TraceSegment?
33
+ # Steep: https://github.com/soutaro/steep/issues/919
34
+ if trace
35
+ tag_client_ip_on_request_span!(trace) if ai_guard_executed?(trace)
36
+
37
+ clean_up_ai_guard_temp_tags!(trace)
38
+ end
37
39
  end
38
40
 
39
41
  private
40
42
 
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
43
  # 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"
44
+ def store_anomaly_detection_tags!(trace, env)
45
+ remote_ip = env["REMOTE_ADDR"]
46
+ trace.set_tag(Datadog::AIGuard::Ext::TRACE_NETWORK_CLIENT_IP_TAG, remote_ip) if remote_ip
47
+
48
+ headers = Datadog::Tracing::Contrib::Rack::Header::RequestHeaderCollection.new(env)
49
+ resolved_client_ip = Datadog::Tracing::ClientIp.extract_client_ip(headers, remote_ip)
50
+ trace.set_tag(Datadog::AIGuard::Ext::TRACE_HTTP_CLIENT_IP_TAG, resolved_client_ip) if resolved_client_ip
51
+
52
+ user_agent = env["HTTP_USER_AGENT"]
53
+ trace.set_tag(Datadog::AIGuard::Ext::TRACE_HTTP_USERAGENT_TAG, user_agent) if user_agent
54
+ rescue => e
55
+ Datadog::AIGuard.telemetry&.report(e, description: "AI Guard: failed to get request attributes")
56
+ end
57
+ # steep:ignore:end
58
+
59
+ # steep:ignore:start
60
+ def clean_up_ai_guard_temp_tags!(trace)
61
+ Ext::TRACE_ANOMALY_DETECTION_TAGS.each do |tag|
62
+ trace.clear_tag(tag)
63
+ end
54
64
 
55
- trace.clear_tag(Datadog::AIGuard::Ext::SERVICE_ENTRY_EXECUTED_TAG)
56
- true
65
+ trace.clear_tag(Datadog::AIGuard::Ext::TRACE_EXECUTED_TAG)
57
66
  end
58
67
  # steep:ignore:end
59
68
 
60
- def tag_client_ip(env)
69
+ # AI Guard's evaluation flow sets `_dd.ai_guard.executed` on the
70
+ # trace whenever an AI Guard span is created during the request.
71
+ # steep:ignore:start
72
+ def ai_guard_executed?(trace)
73
+ trace.get_tag(Datadog::AIGuard::Ext::TRACE_EXECUTED_TAG) == "1"
74
+ end
75
+ # steep:ignore:end
76
+
77
+ # steep:ignore:start
78
+ def tag_client_ip_on_request_span!(trace)
61
79
  span = Datadog::Tracing.active_span
62
80
  return unless span
63
81
 
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
- )
82
+ client_ip = trace.get_tag(Datadog::AIGuard::Ext::TRACE_HTTP_CLIENT_IP_TAG)
83
+ if client_ip && span.get_tag(Datadog::Tracing::Metadata::Ext::HTTP::TAG_CLIENT_IP).nil?
84
+ span.set_tag(Datadog::Tracing::Metadata::Ext::HTTP::TAG_CLIENT_IP, client_ip)
71
85
  end
72
86
 
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
87
+ network_client_ip = trace.get_tag(Datadog::AIGuard::Ext::TRACE_NETWORK_CLIENT_IP_TAG)
88
+ span["network.client.ip"] = network_client_ip if network_client_ip
76
89
  rescue => e
77
90
  Datadog::AIGuard.telemetry&.report(e, description: "AI Guard: failed to tag client IP on root span")
78
91
  end
92
+ # steep:ignore:end
79
93
  end
80
94
  end
81
95
  end
@@ -16,7 +16,12 @@ 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
+ trace.set_tag(Ext::TRACE_EXECUTED_TAG, "1")
20
+
21
+ Ext::TRACE_ANOMALY_DETECTION_TAGS.each do |tag|
22
+ value = trace.get_tag(tag)
23
+ span.set_tag(tag, value) if value
24
+ end
20
25
 
21
26
  if (last_message = messages.last)
22
27
  if last_message.tool_call
@@ -11,8 +11,19 @@ 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
15
  METASTRUCT_TAG = "ai_guard"
16
+
17
+ TRACE_EXECUTED_TAG = "_dd.ai_guard.executed"
18
+ TRACE_HTTP_USERAGENT_TAG = "_dd.ai_guard.http.useragent"
19
+ TRACE_HTTP_CLIENT_IP_TAG = "_dd.ai_guard.http.client_ip"
20
+ TRACE_NETWORK_CLIENT_IP_TAG = "_dd.ai_guard.network.client.ip"
21
+
22
+ TRACE_ANOMALY_DETECTION_TAGS = [
23
+ TRACE_HTTP_USERAGENT_TAG,
24
+ TRACE_HTTP_CLIENT_IP_TAG,
25
+ TRACE_NETWORK_CLIENT_IP_TAG
26
+ ].freeze
16
27
  end
17
28
  end
18
29
  end
@@ -15,6 +15,7 @@ module Datadog
15
15
  RAILS_ROUTES_KEY = 'action_dispatch.routes'
16
16
  RAILS_PATH_PARAMS_KEY = 'action_dispatch.request.path_parameters'
17
17
  RAILS_FORMAT_SUFFIX = '(.:format)'
18
+ DATADOG_RAILS_ROUTE_KEY = 'datadog.action_dispatch.route'
18
19
 
19
20
  # HACK: We rely on the fact that each contrib will modify `request.env`
20
21
  # and store information sufficient to compute the canonical
@@ -49,12 +50,20 @@ module Datadog
49
50
  # In Rails < 7.1 it also will not be set even if a route was found,
50
51
  # but in this case `action_dispatch.request.path_parameters` won't be empty.
51
52
  def self.route_pattern(request)
53
+ # NOTE: Requests from contribs like AWS Lambda don't provide a usable
54
+ # `::Rack::Request#env`, so infer the route from the path instead
55
+ unless request.respond_to?(:env)
56
+ return Tracing::Contrib::Rack::RouteInference.infer(request.path.to_s)
57
+ end
58
+
52
59
  if request.env.key?(GRAPE_ROUTE_KEY)
53
60
  pattern = request.env[GRAPE_ROUTE_KEY][:route_info]&.pattern&.origin
54
61
  "#{request.script_name}#{pattern}"
55
62
  elsif request.env.key?(SINATRA_ROUTE_KEY)
56
63
  pattern = request.env[SINATRA_ROUTE_KEY].split(SINATRA_ROUTE_SEPARATOR, 2)[1]
57
64
  "#{request.script_name}#{pattern}"
65
+ elsif request.env.key?(DATADOG_RAILS_ROUTE_KEY)
66
+ request.env[DATADOG_RAILS_ROUTE_KEY].path.spec.to_s.delete_suffix(RAILS_FORMAT_SUFFIX)
58
67
  elsif request.env.key?(RAILS_ROUTE_KEY)
59
68
  request.env[RAILS_ROUTE_KEY].path.spec.to_s.delete_suffix(RAILS_FORMAT_SUFFIX)
60
69
  elsif request.env.key?(RAILS_ROUTE_URI_PATTERN_KEY)
@@ -22,7 +22,7 @@ module Datadog
22
22
  return
23
23
  end
24
24
 
25
- if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.3') && ffi_version < Gem::Version.new('1.16.0')
25
+ if RubyVersion.is?('>= 3.3') && ffi_version < Gem::Version.new('1.16.0')
26
26
  Datadog.logger.warn(
27
27
  'AppSec is not supported in Ruby versions above 3.3.0 when using `ffi` versions older than 1.16.0, ' \
28
28
  'and will be forcibly disabled due to a memory leak in `ffi`. ' \
@@ -134,6 +134,13 @@ module Datadog
134
134
  end
135
135
  end
136
136
 
137
+ # NOTE: A value of 0 (or less) disables request body collection
138
+ option :body_parsing_size_limit do |o|
139
+ o.type :int
140
+ o.env 'DD_APPSEC_BODY_PARSING_SIZE_LIMIT' # bytes
141
+ o.default 10_485_760
142
+ end
143
+
137
144
  option :waf_debug do |o|
138
145
  o.env 'DD_APPSEC_WAF_DEBUG'
139
146
  o.default false
@@ -15,6 +15,10 @@ module Datadog
15
15
  # Extracts WAF input addresses from normalized AWS Lambda API Gateway event payloads.
16
16
  # @api private
17
17
  module WAFAddresses
18
+ BASE64_CHARS_PER_GROUP = 4
19
+ BASE64_BYTES_PER_GROUP = 3
20
+ BASE64_PADDING_BYTE = "=".ord
21
+
18
22
  module_function
19
23
 
20
24
  def from_request(payload)
@@ -27,10 +31,11 @@ module Datadog
27
31
  'server.request.uri.raw' => build_fullpath(payload),
28
32
  'server.request.headers' => headers,
29
33
  'server.request.headers.no_cookies' => headers.dup.tap { |h| h.delete('cookie') },
30
- 'http.client_ip' => extract_client_ip(payload['source_ip'], headers),
31
34
  'server.request.method' => payload['method'],
32
35
  'server.request.body' => parse_body(payload, headers),
33
- 'server.request.path_params' => payload['path_params']
36
+ 'server.request.body.byte_length' => body_byte_length(payload),
37
+ 'server.request.path_params' => payload['path_params'],
38
+ 'http.client_ip' => extract_client_ip(payload['source_ip'], headers)
34
39
  }
35
40
 
36
41
  data.compact!
@@ -44,7 +49,9 @@ module Datadog
44
49
  data = {
45
50
  'server.response.status' => payload['status_code']&.to_s,
46
51
  'server.response.headers' => headers,
47
- 'server.response.headers.no_cookies' => headers.dup.tap { |h| h.delete('set-cookie') }
52
+ 'server.response.headers.no_cookies' => headers.dup.tap { |h| h.delete('set-cookie') },
53
+ 'server.response.body' => parse_body(payload, headers),
54
+ 'server.response.body.byte_length' => body_byte_length(payload)
48
55
  }
49
56
 
50
57
  data.compact!
@@ -94,7 +101,9 @@ module Datadog
94
101
  body = payload['body']
95
102
  return unless body
96
103
 
97
- body = Core::Utils::Base64Codec.strict_decode64(body) if payload['base64_encoded']
104
+ if (byte_length = body_byte_length(payload))
105
+ return if byte_length > Datadog.configuration.appsec.body_parsing_size_limit
106
+ end
98
107
 
99
108
  content_type = headers['content-type']
100
109
  return unless content_type
@@ -102,7 +111,31 @@ module Datadog
102
111
  media_type = AppSec::Utils::HTTP::MediaType.parse(content_type)
103
112
  return unless media_type
104
113
 
114
+ body = Core::Utils::Base64Codec.strict_decode64(body) if payload['base64_encoded']
105
115
  AppSec::Utils::HTTP::Body.parse(body, media_type: media_type)
116
+ rescue ArgumentError => e
117
+ AppSec.telemetry.report(e, description: 'AppSec: Failed to decode base64 body')
118
+
119
+ nil
120
+ end
121
+
122
+ def body_byte_length(payload)
123
+ body = payload['body']
124
+
125
+ return unless body
126
+ return body.bytesize unless payload['base64_encoded']
127
+
128
+ # NOTE: Base64 packs every 3 bytes into 4 characters and pads the last
129
+ # group with up to two "=" bytes. The decoded length is therefore
130
+ # derivable from the encoded length, letting us measure the raw
131
+ # body size without allocating the decoded string.
132
+ padding = 0
133
+ if body.getbyte(-1) == BASE64_PADDING_BYTE
134
+ padding = 1
135
+ padding = 2 if body.getbyte(-2) == BASE64_PADDING_BYTE
136
+ end
137
+
138
+ body.bytesize / BASE64_CHARS_PER_GROUP * BASE64_BYTES_PER_GROUP - padding
106
139
  end
107
140
  end
108
141
  end