datadog 2.40.0 → 2.41.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 (117) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +85 -58
  3. data/ext/datadog_profiling_native_extension/collectors_stack.c +2 -2
  4. data/ext/datadog_profiling_native_extension/native_extension_helpers.rb +3 -3
  5. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +26 -12
  6. data/ext/libdatadog_api/trace_exporter.c +409 -71
  7. data/ext/libdatadog_extconf_helpers.rb +1 -1
  8. data/lib/datadog/ai_guard/api_client.rb +1 -1
  9. data/lib/datadog/ai_guard/evaluation/request.rb +8 -8
  10. data/lib/datadog/ai_guard/evaluation.rb +10 -3
  11. data/lib/datadog/ai_guard/ext.rb +1 -1
  12. data/lib/datadog/appsec/actions_handler/serializable_backtrace.rb +21 -42
  13. data/lib/datadog/appsec/api_security/endpoint_collection/grape_route_serializer.rb +1 -1
  14. data/lib/datadog/appsec/api_security/endpoint_collection/rails_route_serializer.rb +1 -1
  15. data/lib/datadog/appsec/api_security/endpoint_collection/sinatra_route_serializer.rb +1 -1
  16. data/lib/datadog/appsec/configuration.rb +2 -2
  17. data/lib/datadog/appsec/context.rb +1 -1
  18. data/lib/datadog/appsec/contrib/active_record/instrumentation.rb +1 -1
  19. data/lib/datadog/appsec/contrib/aws_lambda/waf_addresses.rb +2 -2
  20. data/lib/datadog/appsec/contrib/devise/configuration.rb +1 -1
  21. data/lib/datadog/appsec/contrib/excon/ssrf_detection_middleware.rb +2 -2
  22. data/lib/datadog/appsec/contrib/faraday/ssrf_detection_middleware.rb +2 -2
  23. data/lib/datadog/appsec/contrib/graphql/gateway/watcher.rb +1 -1
  24. data/lib/datadog/appsec/contrib/rack/ext.rb +2 -2
  25. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +2 -2
  26. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +2 -2
  27. data/lib/datadog/appsec/contrib/rest_client/request_ssrf_detection_patch.rb +2 -2
  28. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +2 -2
  29. data/lib/datadog/appsec/processor/rule_loader.rb +12 -12
  30. data/lib/datadog/appsec/response.rb +1 -1
  31. data/lib/datadog/appsec/route_normalizer/route_pattern.rb +3 -3
  32. data/lib/datadog/appsec/security_engine/engine.rb +3 -3
  33. data/lib/datadog/core/configuration/components.rb +2 -2
  34. data/lib/datadog/core/configuration/option_definition.rb +1 -1
  35. data/lib/datadog/core/configuration/options.rb +1 -1
  36. data/lib/datadog/core/diagnostics/environment_logger.rb +36 -1
  37. data/lib/datadog/core/metrics/helpers.rb +1 -1
  38. data/lib/datadog/core/metrics/options.rb +2 -2
  39. data/lib/datadog/core/process_discovery.rb +1 -1
  40. data/lib/datadog/core/remote/client.rb +1 -1
  41. data/lib/datadog/core/remote/configuration/repository.rb +2 -2
  42. data/lib/datadog/core/telemetry/configuration_value.rb +38 -0
  43. data/lib/datadog/core/telemetry/event/app_client_configuration_change.rb +3 -1
  44. data/lib/datadog/core/telemetry/event/app_endpoints_loaded.rb +1 -1
  45. data/lib/datadog/core/telemetry/event/app_started.rb +5 -24
  46. data/lib/datadog/core/telemetry/event/generate_metrics.rb +1 -1
  47. data/lib/datadog/core/telemetry/event/log.rb +2 -2
  48. data/lib/datadog/core/telemetry/metric.rb +2 -2
  49. data/lib/datadog/core/telemetry/request.rb +2 -2
  50. data/lib/datadog/core/transport/http.rb +1 -1
  51. data/lib/datadog/core/transport/transport.rb +1 -1
  52. data/lib/datadog/core/utils/at_fork_monkey_patch.rb +127 -43
  53. data/lib/datadog/data_streams/processor.rb +14 -14
  54. data/lib/datadog/di/el/compiler.rb +23 -0
  55. data/lib/datadog/di/el/expression.rb +8 -5
  56. data/lib/datadog/di/instrumenter.rb +5 -4
  57. data/lib/datadog/di/probe_builder.rb +5 -4
  58. data/lib/datadog/di/probe_notification_builder.rb +2 -2
  59. data/lib/datadog/di/serializer.rb +29 -4
  60. data/lib/datadog/error_tracking/component.rb +1 -1
  61. data/lib/datadog/kit/appsec/events/v2.rb +2 -2
  62. data/lib/datadog/open_feature/exposures/batch_builder.rb +1 -1
  63. data/lib/datadog/open_feature/exposures/event.rb +5 -5
  64. data/lib/datadog/profiling/collectors/info.rb +1 -1
  65. data/lib/datadog/profiling/tasks/exec.rb +1 -1
  66. data/lib/datadog/symbol_database/extractor.rb +6 -6
  67. data/lib/datadog/tracing/configuration/dynamic/option.rb +4 -0
  68. data/lib/datadog/tracing/configuration/dynamic.rb +5 -3
  69. data/lib/datadog/tracing/contrib/action_cable/events.rb +1 -1
  70. data/lib/datadog/tracing/contrib/action_mailer/events.rb +1 -1
  71. data/lib/datadog/tracing/contrib/action_pack/action_controller/instrumentation.rb +2 -2
  72. data/lib/datadog/tracing/contrib/action_view/events.rb +1 -1
  73. data/lib/datadog/tracing/contrib/active_model_serializers/events.rb +1 -1
  74. data/lib/datadog/tracing/contrib/active_record/configuration/resolver.rb +1 -1
  75. data/lib/datadog/tracing/contrib/active_record/events.rb +1 -1
  76. data/lib/datadog/tracing/contrib/active_support/cache/events/cache.rb +2 -2
  77. data/lib/datadog/tracing/contrib/aws/ext.rb +1 -1
  78. data/lib/datadog/tracing/contrib/aws/services.rb +1 -1
  79. data/lib/datadog/tracing/contrib/elasticsearch/quantize.rb +1 -1
  80. data/lib/datadog/tracing/contrib/ext.rb +3 -3
  81. data/lib/datadog/tracing/contrib/grpc/datadog_interceptor/client.rb +1 -1
  82. data/lib/datadog/tracing/contrib/grpc/datadog_interceptor/server.rb +1 -1
  83. data/lib/datadog/tracing/contrib/grpc/distributed/propagation.rb +1 -1
  84. data/lib/datadog/tracing/contrib/kafka/events.rb +1 -1
  85. data/lib/datadog/tracing/contrib/karafka/distributed/propagation.rb +1 -1
  86. data/lib/datadog/tracing/contrib/mongodb/parsers.rb +1 -1
  87. data/lib/datadog/tracing/contrib/opensearch/ext.rb +1 -1
  88. data/lib/datadog/tracing/contrib/opensearch/quantize.rb +1 -1
  89. data/lib/datadog/tracing/contrib/patchable.rb +1 -1
  90. data/lib/datadog/tracing/contrib/patcher.rb +1 -1
  91. data/lib/datadog/tracing/contrib/que/tracer.rb +1 -1
  92. data/lib/datadog/tracing/contrib/racecar/events.rb +1 -1
  93. data/lib/datadog/tracing/contrib/rack/configuration/settings.rb +1 -1
  94. data/lib/datadog/tracing/contrib/rack/header_tagging.rb +1 -1
  95. data/lib/datadog/tracing/contrib/rack/patcher.rb +1 -1
  96. data/lib/datadog/tracing/contrib/rack/trace_proxy_middleware.rb +1 -1
  97. data/lib/datadog/tracing/contrib/redis/configuration/resolver.rb +1 -1
  98. data/lib/datadog/tracing/contrib/redis/quantize.rb +36 -2
  99. data/lib/datadog/tracing/contrib/redis/trace_middleware.rb +9 -3
  100. data/lib/datadog/tracing/contrib/sequel/utils.rb +1 -1
  101. data/lib/datadog/tracing/contrib/sidekiq/distributed/propagation.rb +1 -1
  102. data/lib/datadog/tracing/contrib/sinatra/configuration/settings.rb +1 -1
  103. data/lib/datadog/tracing/contrib/sneakers/tracer.rb +1 -1
  104. data/lib/datadog/tracing/contrib/utils/quantization/hash_formatter.rb +1 -1
  105. data/lib/datadog/tracing/contrib/waterdrop/distributed/propagation.rb +1 -1
  106. data/lib/datadog/tracing/correlation.rb +2 -2
  107. data/lib/datadog/tracing/diagnostics/environment_logger.rb +2 -2
  108. data/lib/datadog/tracing/distributed/baggage.rb +1 -2
  109. data/lib/datadog/tracing/metadata/tagging.rb +1 -1
  110. data/lib/datadog/tracing/remote.rb +2 -2
  111. data/lib/datadog/tracing/span.rb +1 -1
  112. data/lib/datadog/tracing/span_operation.rb +1 -1
  113. data/lib/datadog/tracing/transport/io/traces.rb +1 -1
  114. data/lib/datadog/tracing/transport/native.rb +78 -54
  115. data/lib/datadog/tracing/writer.rb +1 -1
  116. data/lib/datadog/version.rb +1 -1
  117. metadata +6 -5
@@ -1,6 +1,16 @@
1
1
  #include <ruby.h>
2
+ #ifdef __GNUC__
3
+ #pragma GCC diagnostic push
4
+ #pragma GCC diagnostic ignored "-Wunused-parameter"
5
+ #endif
6
+ #include <ruby/encoding.h>
7
+ #ifdef __GNUC__
8
+ #pragma GCC diagnostic pop
9
+ #endif
2
10
  #include <ruby/thread.h>
11
+ #include <limits.h>
3
12
  #include <stdbool.h>
13
+ #include <string.h>
4
14
  #include <datadog/data-pipeline.h>
5
15
  #include <datadog/shared-runtime.h>
6
16
 
@@ -12,8 +22,12 @@
12
22
  * Forward declarations
13
23
  * ======================================================================== */
14
24
 
15
- /* Internal: convert a Ruby Span to a Rust ddog_TracerSpan* (caller owns) */
16
- static ddog_TracerSpan *convert_ruby_span_to_rust(VALUE span);
25
+ typedef struct {
26
+ ddog_TracerSpan *span;
27
+ } raw_span_owner;
28
+
29
+ /* Internal: convert a Ruby Span into the supplied raw Rust span owner */
30
+ static void convert_ruby_span_to_rust(VALUE span, raw_span_owner *owner);
17
31
 
18
32
  /* TracerSpan methods */
19
33
  static VALUE _native_from_span(VALUE klass, VALUE span);
@@ -51,9 +65,15 @@ static ID at_duration_id;
51
65
  static ID at_status_id;
52
66
  static ID at_meta_id;
53
67
  static ID at_metrics_id;
68
+ static ID at_metastruct_id;
54
69
 
55
70
  /* Method IDs for time / integer operations */
56
71
  static ID id_duration_method;
72
+ static ID id_to_h;
73
+ static ID id_negative_p;
74
+
75
+ /* Resolved lazily because AppSec may load after this extension. */
76
+ static VALUE serializable_backtrace_class = Qnil;
57
77
 
58
78
  /* Response class (loaded from Ruby) */
59
79
  static VALUE response_class = Qnil;
@@ -101,7 +121,7 @@ static void tracer_span_dfree(void *ptr) {
101
121
  */
102
122
  typedef struct {
103
123
  ddog_TraceExporter *exporter;
104
- const ddog_SharedRuntime *runtime;
124
+ const ddog_ForkSafeRuntime *runtime;
105
125
  } trace_exporter_t;
106
126
 
107
127
  static const rb_data_type_t trace_exporter_typed_data = {
@@ -305,31 +325,320 @@ static int metrics_iter_cb(VALUE key, VALUE value, VALUE arg) {
305
325
  return ST_CONTINUE;
306
326
  }
307
327
 
308
- /*
309
- * Emit a warning while a raw ddog_TracerSpan is held.
310
- *
311
- * log_warning calls into Ruby (Datadog.logger.warn), which can raise or be
312
- * interrupted. A non-local exit here would leak rust_span (it is not yet
313
- * owned by any chunk), so run the call under rb_protect and, on a pending
314
- * jump, free rust_span before re-raising with rb_jump_tag.
315
- */
316
- static void log_warning_owning_span(ddog_TracerSpan *rust_span, VALUE message) {
328
+ typedef struct {
329
+ ddog_TracerValueToken *tokens;
330
+ size_t len;
331
+ size_t capacity;
332
+ st_table *active;
333
+ uint8_t **scratch;
334
+ size_t scratch_len;
335
+ size_t scratch_capacity;
336
+ } structured_value_ctx;
337
+
338
+ static ddog_TracerValueToken *append_structured_value_token(
339
+ structured_value_ctx *ctx, uint8_t kind) {
340
+ if (ctx->len == ctx->capacity) {
341
+ size_t capacity = ctx->capacity == 0 ? 16 : ctx->capacity * 2;
342
+ if (capacity < ctx->capacity || capacity > SIZE_MAX / sizeof(*ctx->tokens)) {
343
+ rb_raise(rb_eNoMemError, "meta_struct token buffer is too large");
344
+ }
345
+ REALLOC_N(ctx->tokens, ddog_TracerValueToken, capacity);
346
+ ctx->capacity = capacity;
347
+ }
348
+
349
+ ddog_TracerValueToken *token = &ctx->tokens[ctx->len++];
350
+ *token = (ddog_TracerValueToken){0};
351
+ token->kind = kind;
352
+ return token;
353
+ }
354
+
355
+ static const uint8_t *copy_structured_value_bytes(
356
+ structured_value_ctx *ctx, VALUE string, size_t len) {
357
+ if (len == 0) return (const uint8_t *)"";
358
+
359
+ if (ctx->scratch_len == ctx->scratch_capacity) {
360
+ size_t capacity = ctx->scratch_capacity == 0 ? 8 : ctx->scratch_capacity * 2;
361
+ if (capacity < ctx->scratch_capacity || capacity > SIZE_MAX / sizeof(*ctx->scratch)) {
362
+ rb_raise(rb_eNoMemError, "meta_struct scratch buffer is too large");
363
+ }
364
+ REALLOC_N(ctx->scratch, uint8_t *, capacity);
365
+ ctx->scratch_capacity = capacity;
366
+ }
367
+
368
+ uint8_t *copy = ruby_xmalloc(len);
369
+ ctx->scratch[ctx->scratch_len++] = copy;
370
+ /* ruby_xmalloc may trigger GC, so retrieve the Ruby buffer only after it
371
+ * returns and do not call Ruby again before the copy completes. */
372
+ memcpy(copy, RSTRING_PTR(string), len);
373
+ return copy;
374
+ }
375
+
376
+ static void free_structured_value_ctx(structured_value_ctx *ctx) {
377
+ if (ctx->active != NULL) st_free_table(ctx->active);
378
+ for (size_t i = 0; i < ctx->scratch_len; i++) {
379
+ ruby_xfree(ctx->scratch[i]);
380
+ }
381
+ ruby_xfree(ctx->scratch);
382
+ ruby_xfree(ctx->tokens);
383
+ }
384
+
385
+ static VALUE resolve_serializable_backtrace_class(void) {
386
+ if (serializable_backtrace_class != Qnil) return serializable_backtrace_class;
387
+
388
+ ID datadog_id = rb_intern("Datadog");
389
+ ID appsec_id = rb_intern("AppSec");
390
+ ID actions_handler_id = rb_intern("ActionsHandler");
391
+ ID backtrace_id = rb_intern("SerializableBacktrace");
392
+ if (!rb_const_defined(rb_cObject, datadog_id)) return Qnil;
393
+ VALUE datadog = rb_const_get(rb_cObject, datadog_id);
394
+ if (!rb_const_defined(datadog, appsec_id)) return Qnil;
395
+ VALUE appsec = rb_const_get(datadog, appsec_id);
396
+ if (!rb_const_defined(appsec, actions_handler_id)) return Qnil;
397
+ VALUE actions_handler = rb_const_get(appsec, actions_handler_id);
398
+ if (!rb_const_defined(actions_handler, backtrace_id)) return Qnil;
399
+
400
+ serializable_backtrace_class = rb_const_get(actions_handler, backtrace_id);
401
+ rb_global_variable(&serializable_backtrace_class);
402
+ return serializable_backtrace_class;
403
+ }
404
+
405
+ static void append_structured_value(VALUE value, unsigned int depth,
406
+ structured_value_ctx *ctx);
407
+
408
+ typedef struct {
409
+ structured_value_ctx *ctx;
410
+ unsigned int depth;
411
+ } structured_hash_ctx;
412
+
413
+ static int append_structured_hash_entry(VALUE key, VALUE value, VALUE arg) {
414
+ structured_hash_ctx *hash_ctx = (structured_hash_ctx *)arg;
415
+ append_structured_value(key, hash_ctx->depth, hash_ctx->ctx);
416
+ append_structured_value(value, hash_ctx->depth, hash_ctx->ctx);
417
+ return ST_CONTINUE;
418
+ }
419
+
420
+ static VALUE utf8_string(VALUE value) {
421
+ VALUE string = RB_TYPE_P(value, T_SYMBOL) ? rb_sym2str(value) : value;
422
+ string = rb_str_export_to_enc(string, rb_utf8_encoding());
423
+ if (rb_enc_str_coderange(string) == ENC_CODERANGE_BROKEN) {
424
+ rb_raise(rb_eEncodingError, "meta_struct string is not valid UTF-8");
425
+ }
426
+ return string;
427
+ }
428
+
429
+ static void append_structured_value(VALUE value, unsigned int depth,
430
+ structured_value_ctx *ctx) {
431
+ if (value == Qnil) {
432
+ append_structured_value_token(ctx, DDOG_TRACER_VALUE_NIL);
433
+ return;
434
+ }
435
+ if (value == Qtrue || value == Qfalse) {
436
+ ddog_TracerValueToken *token =
437
+ append_structured_value_token(ctx, DDOG_TRACER_VALUE_BOOL);
438
+ token->bool_value = value == Qtrue ? 1 : 0;
439
+ return;
440
+ }
441
+ if (RB_TYPE_P(value, T_FIXNUM) || RB_TYPE_P(value, T_BIGNUM)) {
442
+ if (RTEST(rb_funcall(value, id_negative_p, 0))) {
443
+ ddog_TracerValueToken *token =
444
+ append_structured_value_token(ctx, DDOG_TRACER_VALUE_I64);
445
+ token->i64_value = NUM2LL(value);
446
+ } else {
447
+ ddog_TracerValueToken *token =
448
+ append_structured_value_token(ctx, DDOG_TRACER_VALUE_U64);
449
+ token->u64_value = NUM2ULL(value);
450
+ }
451
+ return;
452
+ }
453
+ if (RB_TYPE_P(value, T_FLOAT)) {
454
+ ddog_TracerValueToken *token =
455
+ append_structured_value_token(ctx, DDOG_TRACER_VALUE_F64);
456
+ token->f64_value = RFLOAT_VALUE(value);
457
+ return;
458
+ }
459
+ if (RB_TYPE_P(value, T_STRING) || RB_TYPE_P(value, T_SYMBOL)) {
460
+ bool binary = RB_TYPE_P(value, T_STRING) &&
461
+ rb_enc_get_index(value) == rb_ascii8bit_encindex();
462
+ VALUE string = binary ? value : utf8_string(value);
463
+ size_t len = (size_t)RSTRING_LEN(string);
464
+ ddog_TracerValueToken *token = append_structured_value_token(
465
+ ctx, binary ? DDOG_TRACER_VALUE_BINARY : DDOG_TRACER_VALUE_STRING);
466
+ token->bytes = (ddog_ByteSlice){
467
+ .ptr = copy_structured_value_bytes(ctx, string, len),
468
+ .len = len,
469
+ };
470
+ return;
471
+ }
472
+
473
+ VALUE backtrace_class = resolve_serializable_backtrace_class();
474
+ if (backtrace_class != Qnil && rb_obj_is_kind_of(value, backtrace_class)) {
475
+ append_structured_value(rb_funcall(value, id_to_h, 0), depth, ctx);
476
+ return;
477
+ }
478
+
479
+ if (!RB_TYPE_P(value, T_ARRAY) && !RB_TYPE_P(value, T_HASH)) {
480
+ rb_raise(rb_eTypeError, "unsupported meta_struct value type: %s",
481
+ rb_obj_classname(value));
482
+ }
483
+ if (depth >= 64) {
484
+ rb_raise(rb_eArgError, "meta_struct value exceeds maximum depth of 64");
485
+ }
486
+ if (st_lookup(ctx->active, (st_data_t)value, NULL)) {
487
+ rb_raise(rb_eArgError, "meta_struct value contains a cycle");
488
+ }
489
+ st_insert(ctx->active, (st_data_t)value, 1);
490
+
491
+ size_t length = RB_TYPE_P(value, T_ARRAY) ? (size_t)RARRAY_LEN(value) : RHASH_SIZE(value);
492
+ if (length > UINT32_MAX) {
493
+ rb_raise(rb_eArgError, "meta_struct container exceeds u32::MAX entries");
494
+ }
495
+ ddog_TracerValueToken *token = append_structured_value_token(
496
+ ctx, RB_TYPE_P(value, T_ARRAY) ? DDOG_TRACER_VALUE_ARRAY : DDOG_TRACER_VALUE_MAP);
497
+ token->child_count = (uint32_t)length;
498
+
499
+ if (RB_TYPE_P(value, T_ARRAY)) {
500
+ for (size_t i = 0; i < length; i++) {
501
+ append_structured_value(rb_ary_entry(value, (long)i), depth + 1, ctx);
502
+ }
503
+ } else {
504
+ structured_hash_ctx hash_ctx = {.ctx = ctx, .depth = depth + 1};
505
+ rb_hash_foreach(value, append_structured_hash_entry, (VALUE)&hash_ctx);
506
+ }
507
+ st_delete(ctx->active, (st_data_t *)&value, NULL);
508
+ }
509
+
510
+ typedef struct {
511
+ const uint8_t *key;
512
+ size_t key_len;
513
+ structured_value_ctx value;
514
+ } prepared_metastruct_entry;
515
+
516
+ typedef struct {
517
+ prepared_metastruct_entry *entries;
518
+ size_t len;
519
+ size_t capacity;
520
+ } prepared_metastruct;
521
+
522
+ static void free_prepared_metastruct(prepared_metastruct *prepared) {
523
+ for (size_t i = 0; i < prepared->len; i++) {
524
+ free_structured_value_ctx(&prepared->entries[i].value);
525
+ }
526
+ ruby_xfree(prepared->entries);
527
+ *prepared = (prepared_metastruct){0};
528
+ }
529
+
530
+ static prepared_metastruct_entry *append_prepared_metastruct_entry(
531
+ prepared_metastruct *prepared) {
532
+ if (prepared->len == prepared->capacity) {
533
+ size_t capacity = prepared->capacity == 0 ? 4 : prepared->capacity * 2;
534
+ if (capacity < prepared->capacity || capacity > SIZE_MAX / sizeof(*prepared->entries)) {
535
+ rb_raise(rb_eNoMemError, "meta_struct entry buffer is too large");
536
+ }
537
+ REALLOC_N(prepared->entries, prepared_metastruct_entry, capacity);
538
+ prepared->capacity = capacity;
539
+ }
540
+
541
+ prepared_metastruct_entry *entry = &prepared->entries[prepared->len++];
542
+ *entry = (prepared_metastruct_entry){0};
543
+ entry->value.active = st_init_numtable();
544
+ return entry;
545
+ }
546
+
547
+ typedef struct {
548
+ prepared_metastruct *prepared;
549
+ long skipped;
550
+ } metastruct_prepare_ctx;
551
+
552
+ static int prepare_metastruct_iter_cb(VALUE key, VALUE value, VALUE arg) {
553
+ metastruct_prepare_ctx *ctx = (metastruct_prepare_ctx *)arg;
554
+
555
+ /* The agent meta_struct contract requires string keys. Accept symbols as a
556
+ * Ruby convenience, but do not encode other key types differently from the
557
+ * native tracer implementations. */
558
+ if (RB_TYPE_P(key, T_SYMBOL)) {
559
+ key = rb_sym2str(key);
560
+ } else if (!RB_TYPE_P(key, T_STRING)) {
561
+ ctx->skipped++;
562
+ return ST_CONTINUE;
563
+ }
564
+
565
+ key = utf8_string(key);
566
+ prepared_metastruct_entry *entry =
567
+ append_prepared_metastruct_entry(ctx->prepared);
568
+ entry->key_len = (size_t)RSTRING_LEN(key);
569
+ entry->key = copy_structured_value_bytes(
570
+ &entry->value, key, entry->key_len);
571
+ append_structured_value(value, 0, &entry->value);
572
+
573
+ return ST_CONTINUE;
574
+ }
575
+
576
+ typedef struct {
577
+ VALUE span;
578
+ prepared_metastruct *prepared;
579
+ } prepare_metastruct_call;
580
+
581
+ static VALUE prepare_metastruct_body(VALUE arg) {
582
+ prepare_metastruct_call *call = (prepare_metastruct_call *)arg;
583
+ VALUE metastruct = rb_ivar_get(call->span, at_metastruct_id);
584
+ if (metastruct == Qnil) return Qnil;
585
+
586
+ VALUE values = rb_funcall(metastruct, id_to_h, 0);
587
+ Check_Type(values, T_HASH);
588
+ if (RHASH_SIZE(values) == 0) return Qnil;
589
+
590
+ metastruct_prepare_ctx ctx = {.prepared = call->prepared, .skipped = 0};
591
+ rb_hash_foreach(values, prepare_metastruct_iter_cb, (VALUE)&ctx);
592
+ if (ctx.skipped > 0) {
593
+ log_warning(rb_sprintf(
594
+ "Native trace exporter: skipped %ld meta_struct entries with non-string keys",
595
+ ctx.skipped));
596
+ }
597
+ return Qnil;
598
+ }
599
+
600
+ /* Prepare stable token storage before allocating the Rust span. Any Ruby
601
+ * callback or non-local exit is contained here, where deterministic cleanup
602
+ * does not need to account for Rust span ownership. */
603
+ static void prepare_metastruct(VALUE span, prepared_metastruct *prepared) {
604
+ prepare_metastruct_call call = {.span = span, .prepared = prepared};
317
605
  int state = 0;
318
- rb_protect(log_warning, message, &state);
606
+ rb_protect(prepare_metastruct_body, (VALUE)&call, &state);
319
607
  if (state) {
320
- ddog_tracer_span_free(rust_span);
608
+ free_prepared_metastruct(prepared);
321
609
  rb_jump_tag(state);
322
610
  }
323
611
  }
324
612
 
613
+ static void set_prepared_metastruct(
614
+ ddog_TracerSpan *span, prepared_metastruct *prepared) {
615
+ for (size_t i = 0; i < prepared->len; i++) {
616
+ prepared_metastruct_entry *entry = &prepared->entries[i];
617
+ ddog_Slice_TracerValueToken tokens = {
618
+ .ptr = entry->value.tokens,
619
+ .len = entry->value.len,
620
+ };
621
+ ddog_CharSlice key = {
622
+ .ptr = (const char *)entry->key,
623
+ .len = entry->key_len,
624
+ };
625
+ ddog_TraceExporterError *err =
626
+ ddog_tracer_span_set_meta_struct(span, key, tokens);
627
+ if (err != NULL) {
628
+ free_prepared_metastruct(prepared);
629
+ check_exporter_error("Failed to set span meta_struct", err);
630
+ }
631
+ }
632
+ free_prepared_metastruct(prepared);
633
+ }
325
634
  /* ========================================================================
326
- * Internal: convert a Ruby Span -> ddog_TracerSpan*
635
+ * Internal: convert a Ruby Span into a raw_span_owner
327
636
  *
328
- * The returned pointer is Rust-heap-allocated. Ownership is transferred
329
- * to the caller (either wrap it in TypedData or push it into trace chunks).
637
+ * The caller keeps its ensure handler active until the span is either wrapped
638
+ * in TypedData or consumed by a trace chunk.
330
639
  * ======================================================================== */
331
640
 
332
- static ddog_TracerSpan *convert_ruby_span_to_rust(VALUE span) {
641
+ static void convert_ruby_span_to_rust(VALUE span, raw_span_owner *owner) {
333
642
  /* 1. Read Ruby ivars */
334
643
  VALUE rb_name = rb_ivar_get(span, at_name_id);
335
644
  VALUE rb_service = rb_ivar_get(span, at_service_id);
@@ -340,12 +649,12 @@ static ddog_TracerSpan *convert_ruby_span_to_rust(VALUE span) {
340
649
  VALUE rb_trace_id = rb_ivar_get(span, at_trace_id_id);
341
650
  VALUE rb_status = rb_ivar_get(span, at_status_id);
342
651
 
343
- /* 2. Convert scalars */
344
- ddog_CharSlice name_s = char_slice_from_ruby_string(rb_name);
345
- ddog_CharSlice service_s = nullable_char_slice(rb_service);
346
- ddog_CharSlice resource_s = nullable_char_slice(rb_resource);
347
- ddog_CharSlice type_s = nullable_char_slice(rb_type);
652
+ ENFORCE_TYPE(rb_name, T_STRING);
653
+ if (rb_service != Qnil) ENFORCE_TYPE(rb_service, T_STRING);
654
+ if (rb_resource != Qnil) ENFORCE_TYPE(rb_resource, T_STRING);
655
+ if (rb_type != Qnil) ENFORCE_TYPE(rb_type, T_STRING);
348
656
 
657
+ /* 2. Convert scalars that may call Ruby. */
349
658
  uint64_t span_id = NUM2ULL(rb_span_id);
350
659
  uint64_t parent_id = NUM2ULL(rb_parent_id);
351
660
  int32_t error_val = NUM2INT(rb_status);
@@ -369,7 +678,18 @@ static ddog_TracerSpan *convert_ruby_span_to_rust(VALUE span) {
369
678
  if (dur != Qnil) duration_ns = (int64_t)(NUM2DBL(dur) * 1e9);
370
679
  }
371
680
 
372
- /* 3. Create Rust span */
681
+ /* Structured-value normalisation can call Ruby code. Snapshot it before
682
+ * borrowing scalar string pointers or allocating the Rust span. */
683
+ prepared_metastruct metastruct = {0};
684
+ prepare_metastruct(span, &metastruct);
685
+
686
+ /* No Ruby calls may occur between borrowing these pointers and span_new. */
687
+ ddog_CharSlice name_s = char_slice_from_ruby_string(rb_name);
688
+ ddog_CharSlice service_s = nullable_char_slice(rb_service);
689
+ ddog_CharSlice resource_s = nullable_char_slice(rb_resource);
690
+ ddog_CharSlice type_s = nullable_char_slice(rb_type);
691
+
692
+ /* 3. Create Rust span and immediately consume the stable prepared values. */
373
693
  ddog_TracerSpanFields fields = {
374
694
  .service = service_s,
375
695
  .name = name_s,
@@ -384,22 +704,20 @@ static ddog_TracerSpan *convert_ruby_span_to_rust(VALUE span) {
384
704
  .error = error_val,
385
705
  };
386
706
 
387
- ddog_TracerSpan *rust_span = NULL;
388
- ddog_TraceExporterError *err = ddog_tracer_span_new(&rust_span, &fields);
707
+ ddog_TraceExporterError *err = ddog_tracer_span_new(&owner->span, &fields);
708
+ if (err != NULL) free_prepared_metastruct(&metastruct);
389
709
  check_exporter_error("Failed to create TracerSpan", err);
710
+ set_prepared_metastruct(owner->span, &metastruct);
390
711
 
391
712
  /* 4. Populate meta and metrics */
392
- hash_iter_ctx ctx = {.span = rust_span, .error = NULL, .skipped = 0};
713
+ hash_iter_ctx ctx = {.span = owner->span, .error = NULL, .skipped = 0};
393
714
 
394
715
  VALUE rb_meta = rb_ivar_get(span, at_meta_id);
395
716
  if (RB_TYPE_P(rb_meta, T_HASH) && RHASH_SIZE(rb_meta) > 0) {
396
717
  rb_hash_foreach(rb_meta, meta_iter_cb, (VALUE)&ctx);
397
- if (ctx.error != NULL) {
398
- ddog_tracer_span_free(rust_span);
399
- check_exporter_error("Failed to set span meta", ctx.error);
400
- }
718
+ check_exporter_error("Failed to set span meta", ctx.error);
401
719
  if (ctx.skipped > 0) {
402
- log_warning_owning_span(rust_span, rb_sprintf(
720
+ log_warning(rb_sprintf(
403
721
  "Native trace exporter: skipped %ld non-string meta entries",
404
722
  ctx.skipped));
405
723
  ctx.skipped = 0;
@@ -409,28 +727,49 @@ static ddog_TracerSpan *convert_ruby_span_to_rust(VALUE span) {
409
727
  VALUE rb_metrics = rb_ivar_get(span, at_metrics_id);
410
728
  if (RB_TYPE_P(rb_metrics, T_HASH) && RHASH_SIZE(rb_metrics) > 0) {
411
729
  rb_hash_foreach(rb_metrics, metrics_iter_cb, (VALUE)&ctx);
412
- if (ctx.error != NULL) {
413
- ddog_tracer_span_free(rust_span);
414
- check_exporter_error("Failed to set span metric", ctx.error);
415
- }
730
+ check_exporter_error("Failed to set span metric", ctx.error);
416
731
  if (ctx.skipped > 0) {
417
- log_warning_owning_span(rust_span, rb_sprintf(
732
+ log_warning(rb_sprintf(
418
733
  "Native trace exporter: skipped %ld non-numeric metrics entries",
419
734
  ctx.skipped));
420
735
  }
421
736
  }
422
737
 
423
- return rust_span;
738
+ }
739
+
740
+ static VALUE free_raw_span(VALUE arg) {
741
+ raw_span_owner *owner = (raw_span_owner *)arg;
742
+ if (owner->span != NULL) {
743
+ ddog_tracer_span_free(owner->span);
744
+ owner->span = NULL;
745
+ }
746
+ return Qnil;
424
747
  }
425
748
 
426
749
  /* ========================================================================
427
750
  * TracerSpan._native_from_span
428
751
  * ======================================================================== */
429
752
 
753
+ typedef struct {
754
+ VALUE span;
755
+ raw_span_owner owner;
756
+ } wrap_span_ctx;
757
+
758
+ static VALUE convert_and_wrap_span(VALUE arg) {
759
+ wrap_span_ctx *ctx = (wrap_span_ctx *)arg;
760
+ convert_ruby_span_to_rust(ctx->span, &ctx->owner);
761
+
762
+ VALUE wrapped = TypedData_Wrap_Struct(
763
+ tracer_span_class, &tracer_span_typed_data, ctx->owner.span);
764
+ ctx->owner.span = NULL;
765
+ return wrapped;
766
+ }
767
+
430
768
  static VALUE _native_from_span(DDTRACE_UNUSED VALUE klass, VALUE span) {
431
- ddog_TracerSpan *rust_span = convert_ruby_span_to_rust(span);
432
- return TypedData_Wrap_Struct(tracer_span_class, &tracer_span_typed_data,
433
- rust_span);
769
+ wrap_span_ctx ctx = {.span = span, .owner = {.span = NULL}};
770
+ return rb_ensure(
771
+ convert_and_wrap_span, (VALUE)&ctx,
772
+ free_raw_span, (VALUE)&ctx.owner);
434
773
  }
435
774
 
436
775
  /* ========================================================================
@@ -516,17 +855,27 @@ static VALUE _native_exporter_new(
516
855
  if (rb_service != Qnil) ENFORCE_TYPE(rb_service, T_STRING);
517
856
  if (rb_version != Qnil) ENFORCE_TYPE(rb_version, T_STRING);
518
857
 
519
- /* Phase 2: create config (cleanup on error) */
858
+ /* Phase 2: configure before creating the separately-owned runtime. */
520
859
  ddog_TraceExporterConfig *config = NULL;
521
860
  ddog_trace_exporter_config_new(&config);
522
861
 
862
+ set_config_field(config, ddog_trace_exporter_config_set_url, rb_url, "url");
863
+ set_config_field(config, ddog_trace_exporter_config_set_tracer_version, rb_tracer_version, "tracer_version");
864
+ set_config_field(config, ddog_trace_exporter_config_set_language, rb_language, "language");
865
+ set_config_field(config, ddog_trace_exporter_config_set_lang_version, rb_language_version, "language_version");
866
+ set_config_field(config, ddog_trace_exporter_config_set_lang_interpreter, rb_language_interpreter, "language_interpreter");
867
+ set_config_field(config, ddog_trace_exporter_config_set_hostname, rb_hostname, "hostname");
868
+ set_config_field(config, ddog_trace_exporter_config_set_env, rb_env, "env");
869
+ set_config_field(config, ddog_trace_exporter_config_set_service, rb_service, "service");
870
+ set_config_field(config, ddog_trace_exporter_config_set_version, rb_version, "version");
871
+
523
872
  /*
524
873
  * Create a SharedRuntime and attach it to the config before building the
525
874
  * exporter. The exporter holds a clone of the runtime's Arc; we keep our
526
875
  * own handle in the wrapper to drive fork-safety hooks and to free it when
527
876
  * the exporter is collected.
528
877
  */
529
- const ddog_SharedRuntime *runtime = NULL;
878
+ const ddog_ForkSafeRuntime *runtime = NULL;
530
879
  ddog_SharedRuntimeFFIError *rt_err = ddog_shared_runtime_new(&runtime);
531
880
  if (rt_err != NULL) {
532
881
  ddog_trace_exporter_config_free(config);
@@ -534,13 +883,13 @@ static VALUE _native_exporter_new(
534
883
  }
535
884
 
536
885
  /*
537
- * Const asymmetry: ddog_shared_runtime_new yields a `const ddog_SharedRuntime *`
886
+ * Const asymmetry: ddog_shared_runtime_new yields a `const ddog_ForkSafeRuntime *`
538
887
  * but the setter takes a non-const pointer. The setter only clones the Arc,
539
888
  * so casting away const here is safe.
540
889
  */
541
890
  {
542
891
  ddog_TraceExporterError *attach_err = ddog_trace_exporter_config_set_shared_runtime(
543
- config, (ddog_SharedRuntime *)runtime);
892
+ config, (ddog_ForkSafeRuntime *)runtime);
544
893
  if (attach_err != NULL) {
545
894
  ddog_trace_exporter_config_free(config);
546
895
  ddog_shared_runtime_free(runtime);
@@ -548,16 +897,6 @@ static VALUE _native_exporter_new(
548
897
  }
549
898
  }
550
899
 
551
- set_config_field(config, ddog_trace_exporter_config_set_url, rb_url, "url");
552
- set_config_field(config, ddog_trace_exporter_config_set_tracer_version, rb_tracer_version, "tracer_version");
553
- set_config_field(config, ddog_trace_exporter_config_set_language, rb_language, "language");
554
- set_config_field(config, ddog_trace_exporter_config_set_lang_version, rb_language_version, "language_version");
555
- set_config_field(config, ddog_trace_exporter_config_set_lang_interpreter, rb_language_interpreter, "language_interpreter");
556
- set_config_field(config, ddog_trace_exporter_config_set_hostname, rb_hostname, "hostname");
557
- set_config_field(config, ddog_trace_exporter_config_set_env, rb_env, "env");
558
- set_config_field(config, ddog_trace_exporter_config_set_service, rb_service, "service");
559
- set_config_field(config, ddog_trace_exporter_config_set_version, rb_version, "version");
560
-
561
900
  /* Phase 3: build the exporter from the config */
562
901
  ddog_TraceExporter *exporter = NULL;
563
902
  ddog_TraceExporterError *err = ddog_trace_exporter_new(&exporter, config);
@@ -703,6 +1042,7 @@ typedef struct {
703
1042
  const ddog_TraceExporter *exporter;
704
1043
  VALUE traces;
705
1044
  long trace_count;
1045
+ raw_span_owner span_owner;
706
1046
  ddog_TracerTraceChunks *chunks; /* NULL after send consumes it */
707
1047
  } send_traces_ctx;
708
1048
 
@@ -727,20 +1067,13 @@ static VALUE build_and_send_traces(VALUE arg) {
727
1067
  ddog_tracer_trace_chunks_begin_chunk(ctx->chunks, (size_t)span_count);
728
1068
  check_exporter_error("Failed to begin trace chunk", begin_err);
729
1069
  for (long j = 0; j < span_count; j++) {
730
- VALUE rb_span = rb_ary_entry(chunk_spans, j);
731
-
732
- /* convert_ruby_span_to_rust may raise (type errors, etc.).
733
- * rb_ensure guarantees chunks is freed in that case. */
734
- ddog_TracerSpan *rust_span = convert_ruby_span_to_rust(rb_span);
735
-
736
- /* push_span consumes rust_span (ownership transferred to chunks).
737
- * The error path should be unreachable: push only fails if no chunk was
738
- * started (we always call begin_chunk above) or if the handle is NULL.
739
- * Raise rather than swallow, so that if a future libdatadog change makes
740
- * it reachable we find out loudly instead of silently sending a
741
- * truncated chunk. rb_ensure frees chunks on the raise. */
1070
+ convert_ruby_span_to_rust(
1071
+ rb_ary_entry(chunk_spans, j), &ctx->span_owner);
1072
+
742
1073
  ddog_TraceExporterError *push_err =
743
- ddog_tracer_trace_chunks_push_span(ctx->chunks, rust_span);
1074
+ ddog_tracer_trace_chunks_push_span(ctx->chunks, ctx->span_owner.span);
1075
+ /* push_span consumes the span on every path. */
1076
+ ctx->span_owner.span = NULL;
744
1077
  check_exporter_error("Failed to push span into trace chunk", push_err);
745
1078
  }
746
1079
  }
@@ -834,11 +1167,12 @@ static VALUE build_and_send_traces(VALUE arg) {
834
1167
  }
835
1168
 
836
1169
  /*
837
- * Ensure: free chunks if they haven't been consumed by the send yet.
1170
+ * Ensure: free the current raw span and any chunks not consumed by the send.
838
1171
  * This runs whether build_and_send_traces returned normally or raised.
839
1172
  */
840
- static VALUE free_chunks_if_needed(VALUE arg) {
1173
+ static VALUE free_send_resources(VALUE arg) {
841
1174
  send_traces_ctx *ctx = (send_traces_ctx *)arg;
1175
+ free_raw_span((VALUE)&ctx->span_owner);
842
1176
  if (ctx->chunks != NULL) {
843
1177
  ddog_tracer_trace_chunks_free(ctx->chunks);
844
1178
  ctx->chunks = NULL;
@@ -877,12 +1211,13 @@ static VALUE _native_send_traces(VALUE self, VALUE traces) {
877
1211
  .exporter = wrapper->exporter,
878
1212
  .traces = traces,
879
1213
  .trace_count = trace_count,
1214
+ .span_owner = {.span = NULL},
880
1215
  .chunks = chunks,
881
1216
  };
882
1217
 
883
1218
  return rb_ensure(
884
1219
  build_and_send_traces, (VALUE)&ctx,
885
- free_chunks_if_needed, (VALUE)&ctx);
1220
+ free_send_resources, (VALUE)&ctx);
886
1221
  }
887
1222
 
888
1223
  /* ========================================================================
@@ -957,9 +1292,12 @@ void trace_exporter_init(VALUE tracing_module) {
957
1292
  at_status_id = rb_intern("@status");
958
1293
  at_meta_id = rb_intern("@meta");
959
1294
  at_metrics_id = rb_intern("@metrics");
1295
+ at_metastruct_id = rb_intern("@metastruct");
960
1296
 
961
1297
  /* Methods */
962
1298
  id_duration_method = rb_intern("duration");
1299
+ id_to_h = rb_intern("to_h");
1300
+ id_negative_p = rb_intern("negative?");
963
1301
 
964
1302
  /* Response.new */
965
1303
  id_new = rb_intern("new");
@@ -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 = "~> 36.0.0.1.0"
13
+ LIBDATADOG_VERSION = "~> 40.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.
@@ -32,7 +32,7 @@ module Datadog
32
32
  "DD-AI-GUARD-VERSION": Datadog::VERSION::STRING,
33
33
  "DD-AI-GUARD-SOURCE": "SDK",
34
34
  "DD-AI-GUARD-LANGUAGE": "ruby",
35
- "content-type": "application/json"
35
+ "content-type": "application/json",
36
36
  }.freeze
37
37
  end
38
38