ddtrace 1.20.0 → 1.23.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +130 -1
  3. data/LICENSE-3rdparty.csv +1 -1
  4. data/bin/ddprofrb +15 -0
  5. data/bin/ddtracerb +3 -1
  6. data/ext/{ddtrace_profiling_loader/ddtrace_profiling_loader.c → datadog_profiling_loader/datadog_profiling_loader.c} +2 -2
  7. data/ext/{ddtrace_profiling_loader → datadog_profiling_loader}/extconf.rb +3 -3
  8. data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/collectors_cpu_and_wall_time_worker.c +226 -61
  9. data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/collectors_discrete_dynamic_sampler.c +145 -72
  10. data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/collectors_discrete_dynamic_sampler.h +17 -5
  11. data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/collectors_thread_context.c +115 -14
  12. data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/extconf.rb +2 -2
  13. data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/heap_recorder.c +81 -4
  14. data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/heap_recorder.h +12 -1
  15. data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/http_transport.c +15 -19
  16. data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/native_extension_helpers.rb +4 -4
  17. data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/private_vm_api_access.c +14 -0
  18. data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/private_vm_api_access.h +4 -0
  19. data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/profiling.c +1 -1
  20. data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/ruby_helpers.c +10 -0
  21. data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/ruby_helpers.h +5 -0
  22. data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/stack_recorder.c +161 -62
  23. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +43 -13
  24. data/lib/datadog/appsec/event.rb +1 -1
  25. data/lib/datadog/auto_instrument.rb +3 -0
  26. data/lib/datadog/core/configuration/components.rb +7 -6
  27. data/lib/datadog/core/configuration/option.rb +8 -6
  28. data/lib/datadog/core/configuration/settings.rb +130 -63
  29. data/lib/datadog/core/configuration.rb +20 -4
  30. data/lib/datadog/core/diagnostics/environment_logger.rb +4 -3
  31. data/lib/datadog/core/environment/git.rb +25 -0
  32. data/lib/datadog/core/environment/identity.rb +18 -48
  33. data/lib/datadog/core/environment/platform.rb +7 -1
  34. data/lib/datadog/core/git/ext.rb +2 -23
  35. data/lib/datadog/core/remote/client/capabilities.rb +1 -1
  36. data/lib/datadog/core/remote/negotiation.rb +2 -2
  37. data/lib/datadog/core/remote/transport/http/config.rb +1 -1
  38. data/lib/datadog/core/remote/worker.rb +7 -4
  39. data/lib/datadog/core/telemetry/client.rb +18 -10
  40. data/lib/datadog/core/telemetry/emitter.rb +9 -13
  41. data/lib/datadog/core/telemetry/event.rb +247 -57
  42. data/lib/datadog/core/telemetry/ext.rb +1 -0
  43. data/lib/datadog/core/telemetry/heartbeat.rb +1 -3
  44. data/lib/datadog/core/telemetry/http/ext.rb +4 -1
  45. data/lib/datadog/core/telemetry/http/response.rb +4 -0
  46. data/lib/datadog/core/telemetry/http/transport.rb +9 -4
  47. data/lib/datadog/core/telemetry/request.rb +59 -0
  48. data/lib/datadog/core/transport/ext.rb +2 -0
  49. data/lib/datadog/core/utils/url.rb +25 -0
  50. data/lib/datadog/profiling/collectors/code_provenance.rb +10 -4
  51. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +31 -0
  52. data/lib/datadog/profiling/collectors/info.rb +101 -0
  53. data/lib/datadog/profiling/component.rb +34 -28
  54. data/lib/datadog/profiling/exporter.rb +23 -6
  55. data/lib/datadog/profiling/ext.rb +2 -0
  56. data/lib/datadog/profiling/flush.rb +6 -3
  57. data/lib/datadog/profiling/http_transport.rb +5 -1
  58. data/lib/datadog/profiling/load_native_extension.rb +19 -6
  59. data/lib/datadog/profiling/native_extension.rb +1 -1
  60. data/lib/datadog/profiling/stack_recorder.rb +6 -2
  61. data/lib/datadog/profiling/tag_builder.rb +5 -0
  62. data/lib/datadog/profiling/tasks/exec.rb +3 -3
  63. data/lib/datadog/profiling/tasks/help.rb +3 -3
  64. data/lib/datadog/profiling.rb +13 -2
  65. data/lib/datadog/tracing/contrib/action_mailer/events/deliver.rb +1 -1
  66. data/lib/datadog/tracing/contrib/active_record/configuration/resolver.rb +11 -4
  67. data/lib/datadog/tracing/contrib/concurrent_ruby/async_patch.rb +20 -0
  68. data/lib/datadog/tracing/contrib/concurrent_ruby/patcher.rb +11 -1
  69. data/lib/datadog/tracing/contrib/configurable.rb +1 -1
  70. data/lib/datadog/tracing/contrib/extensions.rb +6 -2
  71. data/lib/datadog/tracing/contrib/pg/instrumentation.rb +11 -4
  72. data/lib/datadog/tracing/sampling/matcher.rb +23 -3
  73. data/lib/datadog/tracing/sampling/rule.rb +7 -2
  74. data/lib/datadog/tracing/sampling/rule_sampler.rb +2 -0
  75. data/lib/datadog/tracing/trace_operation.rb +1 -2
  76. data/lib/datadog/tracing/transport/http.rb +1 -0
  77. data/lib/datadog/tracing/transport/trace_formatter.rb +31 -0
  78. data/lib/ddtrace/version.rb +1 -1
  79. metadata +58 -65
  80. data/ext/ddtrace_profiling_native_extension/pid_controller.c +0 -57
  81. data/ext/ddtrace_profiling_native_extension/pid_controller.h +0 -45
  82. data/lib/datadog/core/telemetry/collector.rb +0 -250
  83. data/lib/datadog/core/telemetry/v1/app_event.rb +0 -59
  84. data/lib/datadog/core/telemetry/v1/application.rb +0 -92
  85. data/lib/datadog/core/telemetry/v1/configuration.rb +0 -25
  86. data/lib/datadog/core/telemetry/v1/dependency.rb +0 -43
  87. data/lib/datadog/core/telemetry/v1/host.rb +0 -59
  88. data/lib/datadog/core/telemetry/v1/install_signature.rb +0 -38
  89. data/lib/datadog/core/telemetry/v1/integration.rb +0 -64
  90. data/lib/datadog/core/telemetry/v1/product.rb +0 -36
  91. data/lib/datadog/core/telemetry/v1/telemetry_request.rb +0 -106
  92. data/lib/datadog/core/telemetry/v2/app_client_configuration_change.rb +0 -41
  93. data/lib/datadog/core/telemetry/v2/request.rb +0 -29
  94. data/lib/datadog/profiling/diagnostics/environment_logger.rb +0 -39
  95. /data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/NativeExtensionDesign.md +0 -0
  96. /data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/clock_id.h +0 -0
  97. /data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/clock_id_from_pthread.c +0 -0
  98. /data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/clock_id_noop.c +0 -0
  99. /data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/collectors_dynamic_sampling_rate.c +0 -0
  100. /data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/collectors_dynamic_sampling_rate.h +0 -0
  101. /data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/collectors_gc_profiling_helper.c +0 -0
  102. /data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/collectors_gc_profiling_helper.h +0 -0
  103. /data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/collectors_idle_sampling_helper.c +0 -0
  104. /data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/collectors_idle_sampling_helper.h +0 -0
  105. /data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/collectors_stack.c +0 -0
  106. /data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/collectors_stack.h +0 -0
  107. /data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/collectors_thread_context.h +0 -0
  108. /data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/helpers.h +0 -0
  109. /data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/libdatadog_helpers.c +0 -0
  110. /data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/libdatadog_helpers.h +0 -0
  111. /data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/setup_signal_handler.c +0 -0
  112. /data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/setup_signal_handler.h +0 -0
  113. /data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/stack_recorder.h +0 -0
  114. /data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/time_helpers.c +0 -0
  115. /data/ext/{ddtrace_profiling_native_extension → datadog_profiling_native_extension}/time_helpers.h +0 -0
@@ -82,6 +82,9 @@ static ID at_id_id; // id of :@id in Ruby
82
82
  static ID at_resource_id; // id of :@resource in Ruby
83
83
  static ID at_root_span_id; // id of :@root_span in Ruby
84
84
  static ID at_type_id; // id of :@type in Ruby
85
+ static ID at_otel_values_id; // id of :@otel_values in Ruby
86
+ static ID at_parent_span_id_id; // id of :@parent_span_id in Ruby
87
+ static ID at_datadog_trace_id; // id of :@datadog_trace in Ruby
85
88
 
86
89
  // Contains state for a single ThreadContext instance
87
90
  struct thread_context_collector_state {
@@ -114,6 +117,8 @@ struct thread_context_collector_state {
114
117
  monotonic_to_system_epoch_state time_converter_state;
115
118
  // Used to identify the main thread, to give it a fallback name
116
119
  VALUE main_thread;
120
+ // Used when extracting trace identifiers from otel spans. Lazily initialized.
121
+ VALUE otel_current_span_key;
117
122
 
118
123
  struct stats {
119
124
  // Track how many garbage collection samples we've taken.
@@ -212,12 +217,20 @@ static long thread_id_for(VALUE thread);
212
217
  static VALUE _native_stats(VALUE self, VALUE collector_instance);
213
218
  static VALUE _native_gc_tracking(VALUE self, VALUE collector_instance);
214
219
  static void trace_identifiers_for(struct thread_context_collector_state *state, VALUE thread, struct trace_identifiers *trace_identifiers_result);
215
- static bool should_collect_resource(VALUE root_span_type);
220
+ static bool should_collect_resource(VALUE root_span);
216
221
  static VALUE _native_reset_after_fork(DDTRACE_UNUSED VALUE self, VALUE collector_instance);
217
222
  static VALUE thread_list(struct thread_context_collector_state *state);
218
223
  static VALUE _native_sample_allocation(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE sample_weight, VALUE new_object);
219
224
  static VALUE _native_new_empty_thread(VALUE self);
220
225
  static ddog_CharSlice ruby_value_type_to_class_name(enum ruby_value_type type);
226
+ static void ddtrace_otel_trace_identifiers_for(
227
+ struct thread_context_collector_state *state,
228
+ VALUE *active_trace,
229
+ VALUE *root_span,
230
+ VALUE *numeric_span_id,
231
+ VALUE active_span,
232
+ VALUE otel_values
233
+ );
221
234
 
222
235
  void collectors_thread_context_init(VALUE profiling_module) {
223
236
  VALUE collectors_module = rb_define_module_under(profiling_module, "Collectors");
@@ -255,6 +268,9 @@ void collectors_thread_context_init(VALUE profiling_module) {
255
268
  at_resource_id = rb_intern_const("@resource");
256
269
  at_root_span_id = rb_intern_const("@root_span");
257
270
  at_type_id = rb_intern_const("@type");
271
+ at_otel_values_id = rb_intern_const("@otel_values");
272
+ at_parent_span_id_id = rb_intern_const("@parent_span_id");
273
+ at_datadog_trace_id = rb_intern_const("@datadog_trace");
258
274
 
259
275
  gc_profiling_init();
260
276
  }
@@ -282,6 +298,7 @@ static void thread_context_collector_typed_data_mark(void *state_ptr) {
282
298
  st_foreach(state->hash_map_per_thread_context, hash_map_per_thread_context_mark, 0 /* unused */);
283
299
  rb_gc_mark(state->thread_list_buffer);
284
300
  rb_gc_mark(state->main_thread);
301
+ rb_gc_mark(state->otel_current_span_key);
285
302
  }
286
303
 
287
304
  static void thread_context_collector_typed_data_free(void *state_ptr) {
@@ -334,6 +351,7 @@ static VALUE _native_new(VALUE klass) {
334
351
  state->allocation_type_enabled = true;
335
352
  state->time_converter_state = (monotonic_to_system_epoch_state) MONOTONIC_TO_SYSTEM_EPOCH_INITIALIZER;
336
353
  state->main_thread = rb_thread_main();
354
+ state->otel_current_span_key = Qnil;
337
355
  state->gc_tracking.wall_time_at_previous_gc_ns = INVALID_TIME;
338
356
  state->gc_tracking.wall_time_at_last_flushed_gc_event_ns = 0;
339
357
 
@@ -603,11 +621,14 @@ bool thread_context_collector_on_gc_finish(VALUE self_instance) {
603
621
  // Let the caller know if it should schedule a flush or not. Returning true every time would cause a lot of overhead
604
622
  // on the application (see GC tracking introduction at the top of the file), so instead we try to accumulate a few
605
623
  // samples first.
606
- bool finished_major_gc = gc_profiling_has_major_gc_finished();
607
624
  bool over_flush_time_treshold =
608
625
  (wall_time_at_finish_ns - state->gc_tracking.wall_time_at_last_flushed_gc_event_ns) >= TIME_BETWEEN_GC_EVENTS_NS;
609
626
 
610
- return finished_major_gc || over_flush_time_treshold;
627
+ if (over_flush_time_treshold) {
628
+ return true;
629
+ } else {
630
+ return gc_profiling_has_major_gc_finished();
631
+ }
611
632
  }
612
633
 
613
634
  // This function gets called after one or more GC work steps (calls to on_gc_start/on_gc_finish).
@@ -917,6 +938,7 @@ static VALUE _native_inspect(DDTRACE_UNUSED VALUE _self, VALUE collector_instanc
917
938
  ));
918
939
  rb_str_concat(result, rb_sprintf(" main_thread=%"PRIsVALUE, state->main_thread));
919
940
  rb_str_concat(result, rb_sprintf(" gc_tracking=%"PRIsVALUE, gc_tracking_as_ruby_hash(state)));
941
+ rb_str_concat(result, rb_sprintf(" otel_current_span_key=%"PRIsVALUE, state->otel_current_span_key));
920
942
 
921
943
  return result;
922
944
  }
@@ -1104,10 +1126,19 @@ static void trace_identifiers_for(struct thread_context_collector_state *state,
1104
1126
 
1105
1127
  VALUE root_span = rb_ivar_get(active_trace, at_root_span_id /* @root_span */);
1106
1128
  VALUE active_span = rb_ivar_get(active_trace, at_active_span_id /* @active_span */);
1107
- if (root_span == Qnil || active_span == Qnil) return;
1129
+ // Note: On Ruby 3.x `rb_attr_get` is exactly the same as `rb_ivar_get`. For Ruby 2.x, the difference is that
1130
+ // `rb_ivar_get` can trigger "warning: instance variable @otel_values not initialized" if warnings are enabled and
1131
+ // opentelemetry is not in use, whereas `rb_attr_get` does the lookup without generating the warning.
1132
+ VALUE otel_values = rb_attr_get(active_trace, at_otel_values_id /* @otel_values */);
1133
+
1134
+ VALUE numeric_span_id = Qnil;
1135
+
1136
+ if (otel_values != Qnil) ddtrace_otel_trace_identifiers_for(state, &active_trace, &root_span, &numeric_span_id, active_span, otel_values);
1137
+
1138
+ if (root_span == Qnil || (active_span == Qnil && numeric_span_id == Qnil)) return;
1108
1139
 
1109
1140
  VALUE numeric_local_root_span_id = rb_ivar_get(root_span, at_id_id /* @id */);
1110
- VALUE numeric_span_id = rb_ivar_get(active_span, at_id_id /* @id */);
1141
+ if (active_span != Qnil && numeric_span_id == Qnil) numeric_span_id = rb_ivar_get(active_span, at_id_id /* @id */);
1111
1142
  if (numeric_local_root_span_id == Qnil || numeric_span_id == Qnil) return;
1112
1143
 
1113
1144
  trace_identifiers_result->local_root_span_id = NUM2ULL(numeric_local_root_span_id);
@@ -1115,10 +1146,7 @@ static void trace_identifiers_for(struct thread_context_collector_state *state,
1115
1146
 
1116
1147
  trace_identifiers_result->valid = true;
1117
1148
 
1118
- if (!state->endpoint_collection_enabled) return;
1119
-
1120
- VALUE root_span_type = rb_ivar_get(root_span, at_type_id /* @type */);
1121
- if (root_span_type == Qnil || !should_collect_resource(root_span_type)) return;
1149
+ if (!state->endpoint_collection_enabled || !should_collect_resource(root_span)) return;
1122
1150
 
1123
1151
  VALUE trace_resource = rb_ivar_get(active_trace, at_resource_id /* @resource */);
1124
1152
  if (RB_TYPE_P(trace_resource, T_STRING)) {
@@ -1129,21 +1157,32 @@ static void trace_identifiers_for(struct thread_context_collector_state *state,
1129
1157
  }
1130
1158
  }
1131
1159
 
1132
- // We only collect the resource for spans of types:
1160
+ // We opt-in to collecting the resource for spans of types:
1133
1161
  // * 'web', for web requests
1134
- // * proxy', used by the rack integration with request_queuing: true (e.g. also represents a web request)
1162
+ // * 'proxy', used by the rack integration with request_queuing: true (e.g. also represents a web request)
1163
+ // * 'worker', used for sidekiq and similar background job processors
1135
1164
  //
1136
- // NOTE: Currently we're only interested in HTTP service endpoints. Over time, this list may be expanded.
1165
+ // Over time, this list may be expanded.
1137
1166
  // Resources MUST NOT include personal identifiable information (PII); this should not be the case with
1138
1167
  // ddtrace integrations, but worth mentioning just in case :)
1139
- static bool should_collect_resource(VALUE root_span_type) {
1168
+ static bool should_collect_resource(VALUE root_span) {
1169
+ VALUE root_span_type = rb_ivar_get(root_span, at_type_id /* @type */);
1170
+ if (root_span_type == Qnil) return false;
1140
1171
  ENFORCE_TYPE(root_span_type, T_STRING);
1141
1172
 
1142
1173
  int root_span_type_length = RSTRING_LEN(root_span_type);
1143
1174
  const char *root_span_type_value = StringValuePtr(root_span_type);
1144
1175
 
1145
- return (root_span_type_length == strlen("web") && (memcmp("web", root_span_type_value, strlen("web")) == 0)) ||
1176
+ bool is_web_request =
1177
+ (root_span_type_length == strlen("web") && (memcmp("web", root_span_type_value, strlen("web")) == 0)) ||
1146
1178
  (root_span_type_length == strlen("proxy") && (memcmp("proxy", root_span_type_value, strlen("proxy")) == 0));
1179
+
1180
+ if (is_web_request) return true;
1181
+
1182
+ bool is_worker_request =
1183
+ (root_span_type_length == strlen("worker") && (memcmp("worker", root_span_type_value, strlen("worker")) == 0));
1184
+
1185
+ return is_worker_request;
1147
1186
  }
1148
1187
 
1149
1188
  // After the Ruby VM forks, this method gets called in the child process to clean up any leftover state from the parent.
@@ -1299,3 +1338,65 @@ static ddog_CharSlice ruby_value_type_to_class_name(enum ruby_value_type type) {
1299
1338
  default: return DDOG_CHARSLICE_C("(VM Internal, Missing class)");
1300
1339
  }
1301
1340
  }
1341
+
1342
+ static VALUE get_otel_current_span_key(struct thread_context_collector_state *state) {
1343
+ if (state->otel_current_span_key == Qnil) {
1344
+ VALUE datadog_module = rb_const_get(rb_cObject, rb_intern("Datadog"));
1345
+ VALUE opentelemetry_module = rb_const_get(datadog_module, rb_intern("OpenTelemetry"));
1346
+ VALUE api_module = rb_const_get(opentelemetry_module, rb_intern("API"));
1347
+ VALUE context_module = rb_const_get(api_module, rb_intern_const("Context"));
1348
+ VALUE current_span_key = rb_const_get(context_module, rb_intern_const("CURRENT_SPAN_KEY"));
1349
+
1350
+ if (current_span_key == Qnil) {
1351
+ rb_raise(rb_eRuntimeError, "Unexpected: Missing Datadog::OpenTelemetry::API::Context::CURRENT_SPAN_KEY");
1352
+ }
1353
+
1354
+ state->otel_current_span_key = current_span_key;
1355
+ }
1356
+
1357
+ return state->otel_current_span_key;
1358
+ }
1359
+
1360
+ // This method gets used when ddtrace is being used indirectly via the otel APIs. Information gets stored slightly
1361
+ // differently, and this codepath handles it.
1362
+ static void ddtrace_otel_trace_identifiers_for(
1363
+ struct thread_context_collector_state *state,
1364
+ VALUE *active_trace,
1365
+ VALUE *root_span,
1366
+ VALUE *numeric_span_id,
1367
+ VALUE active_span,
1368
+ VALUE otel_values
1369
+ ) {
1370
+ VALUE resolved_numeric_span_id =
1371
+ active_span == Qnil ?
1372
+ // For traces started from otel spans, the span id will be empty, and the @parent_span_id has the right value
1373
+ rb_ivar_get(*active_trace, at_parent_span_id_id /* @parent_span_id */) :
1374
+ // Regular span created by ddtrace
1375
+ rb_ivar_get(active_span, at_id_id /* @id */);
1376
+
1377
+ if (resolved_numeric_span_id == Qnil) return;
1378
+
1379
+ VALUE otel_current_span_key = get_otel_current_span_key(state);
1380
+ VALUE current_trace = *active_trace;
1381
+
1382
+ // ddtrace uses a different structure when spans are created from otel, where each otel span will have a unique ddtrace
1383
+ // trace and span representing it. Each ddtrace trace is then connected to the previous otel span, forming a linked
1384
+ // list. The local root span is going to be the trace/span we find at the end of this linked list.
1385
+ while (otel_values != Qnil) {
1386
+ VALUE otel_span = rb_hash_lookup(otel_values, otel_current_span_key);
1387
+ if (otel_span == Qnil) break;
1388
+ VALUE next_trace = rb_ivar_get(otel_span, at_datadog_trace_id);
1389
+ if (next_trace == Qnil) break;
1390
+
1391
+ current_trace = next_trace;
1392
+ otel_values = rb_ivar_get(current_trace, at_otel_values_id /* @otel_values */);
1393
+ }
1394
+
1395
+ // We found the last trace in the linked list. This contains the local root span
1396
+ VALUE resolved_root_span = rb_ivar_get(current_trace, at_root_span_id /* @root_span */);
1397
+ if (resolved_root_span == Qnil) return;
1398
+
1399
+ *root_span = resolved_root_span;
1400
+ *active_trace = current_trace;
1401
+ *numeric_span_id = resolved_numeric_span_id;
1402
+ }
@@ -100,7 +100,7 @@ add_compiler_flag '-Wno-declaration-after-statement'
100
100
  add_compiler_flag '-Werror-implicit-function-declaration'
101
101
 
102
102
  # The native extension is not intended to expose any symbols/functions for other native libraries to use;
103
- # the sole exception being `Init_ddtrace_profiling_native_extension` which needs to be visible for Ruby to call it when
103
+ # the sole exception being `Init_datadog_profiling_native_extension` which needs to be visible for Ruby to call it when
104
104
  # it `dlopen`s the library.
105
105
  #
106
106
  # By setting this compiler flag, we tell it to assume that everything is private unless explicitly stated.
@@ -237,7 +237,7 @@ Logging.message("[ddtrace] After pkg-config $LDFLAGS were set to: #{$LDFLAGS.ins
237
237
  # This makes it easier for development (avoids "oops I forgot to rebuild when I switched my Ruby") and ensures that
238
238
  # the wrong library is never loaded.
239
239
  # When requiring, we need to use the exact same string, including the version and the platform.
240
- EXTENSION_NAME = "ddtrace_profiling_native_extension.#{RUBY_VERSION}_#{RUBY_PLATFORM}".freeze
240
+ EXTENSION_NAME = "datadog_profiling_native_extension.#{RUBY_VERSION}_#{RUBY_PLATFORM}".freeze
241
241
 
242
242
  if Datadog::Profiling::NativeExtensionHelpers::CAN_USE_MJIT_HEADER
243
243
  mjit_header_file_name = "rb_mjit_min_header-#{RUBY_VERSION}.h"
@@ -10,6 +10,13 @@
10
10
  #define CAN_APPLY_GC_FORCE_RECYCLE_BUG_WORKAROUND
11
11
  #endif
12
12
 
13
+ // Minimum age (in GC generations) of heap objects we want to include in heap
14
+ // recorder iterations. Object with age 0 represent objects that have yet to undergo
15
+ // a GC and, thus, may just be noise/trash at instant of iteration and are usually not
16
+ // relevant for heap profiles as the great majority should be trivially reclaimed
17
+ // during the next GC.
18
+ #define ITERATION_MIN_AGE 1
19
+
13
20
  // A compact representation of a stacktrace frame for a heap allocation.
14
21
  typedef struct {
15
22
  char *name;
@@ -137,6 +144,11 @@ struct heap_recorder {
137
144
  // mutation of the data so iteration can occur without acquiring a lock.
138
145
  // NOTE: Contrary to object_records, this table has no ownership of its data.
139
146
  st_table *object_records_snapshot;
147
+ // The GC gen/epoch/count in which we prepared the current iteration.
148
+ //
149
+ // This enables us to calculate the age of iterated objects in the above snapshot by
150
+ // comparing it against an object's alloc_gen.
151
+ size_t iteration_gen;
140
152
 
141
153
  // Data for a heap recording that was started but not yet ended
142
154
  recording active_recording;
@@ -146,6 +158,13 @@ struct heap_recorder {
146
158
 
147
159
  // Sampling state
148
160
  uint num_recordings_skipped;
161
+
162
+ struct stats_last_update {
163
+ size_t objects_alive;
164
+ size_t objects_dead;
165
+ size_t objects_skipped;
166
+ size_t objects_frozen;
167
+ } stats_last_update;
149
168
  };
150
169
  static heap_record* get_or_create_heap_record(heap_recorder*, ddog_prof_Slice_Location);
151
170
  static void cleanup_heap_record_if_unused(heap_recorder*, heap_record*);
@@ -353,11 +372,16 @@ void heap_recorder_prepare_iteration(heap_recorder *heap_recorder) {
353
372
  return;
354
373
  }
355
374
 
375
+ heap_recorder->iteration_gen = rb_gc_count();
376
+
356
377
  if (heap_recorder->object_records_snapshot != NULL) {
357
378
  // we could trivially handle this but we raise to highlight and catch unexpected usages.
358
379
  rb_raise(rb_eRuntimeError, "New heap recorder iteration prepared without the previous one having been finished.");
359
380
  }
360
381
 
382
+ // Reset last update stats, we'll be building them from scratch during the st_foreach call below
383
+ heap_recorder->stats_last_update = (struct stats_last_update) {};
384
+
361
385
  st_foreach(heap_recorder->object_records, st_object_record_update, (st_data_t) heap_recorder);
362
386
 
363
387
  heap_recorder->object_records_snapshot = st_copy(heap_recorder->object_records);
@@ -413,6 +437,22 @@ bool heap_recorder_for_each_live_object(
413
437
  return true;
414
438
  }
415
439
 
440
+ VALUE heap_recorder_state_snapshot(heap_recorder *heap_recorder) {
441
+ VALUE arguments[] = {
442
+ ID2SYM(rb_intern("num_object_records")), /* => */ LONG2NUM(heap_recorder->object_records->num_entries),
443
+ ID2SYM(rb_intern("num_heap_records")), /* => */ LONG2NUM(heap_recorder->heap_records->num_entries),
444
+
445
+ // Stats as of last update
446
+ ID2SYM(rb_intern("last_update_objects_alive")), /* => */ LONG2NUM(heap_recorder->stats_last_update.objects_alive),
447
+ ID2SYM(rb_intern("last_update_objects_dead")), /* => */ LONG2NUM(heap_recorder->stats_last_update.objects_dead),
448
+ ID2SYM(rb_intern("last_update_objects_skipped")), /* => */ LONG2NUM(heap_recorder->stats_last_update.objects_skipped),
449
+ ID2SYM(rb_intern("last_update_objects_frozen")), /* => */ LONG2NUM(heap_recorder->stats_last_update.objects_frozen),
450
+ };
451
+ VALUE hash = rb_hash_new();
452
+ for (long unsigned int i = 0; i < VALUE_COUNT(arguments); i += 2) rb_hash_aset(hash, arguments[i], arguments[i+1]);
453
+ return hash;
454
+ }
455
+
416
456
  void heap_recorder_testonly_assert_hash_matches(ddog_prof_Slice_Location locations) {
417
457
  heap_stack *stack = heap_stack_new(locations);
418
458
  heap_record_key stack_based_key = (heap_record_key) {
@@ -459,6 +499,13 @@ static int st_object_record_entry_free(DDTRACE_UNUSED st_data_t key, st_data_t v
459
499
  return ST_DELETE;
460
500
  }
461
501
 
502
+ // Check to see if an object should not be included in a heap recorder iteration.
503
+ // This centralizes the checking logic to ensure it's equally applied between
504
+ // preparation and iteration codepaths.
505
+ static inline bool should_exclude_from_iteration(object_record *obj_record) {
506
+ return obj_record->object_data.gen_age < ITERATION_MIN_AGE;
507
+ }
508
+
462
509
  static int st_object_record_update(st_data_t key, st_data_t value, st_data_t extra_arg) {
463
510
  long obj_id = (long) key;
464
511
  object_record *record = (object_record*) value;
@@ -466,9 +513,24 @@ static int st_object_record_update(st_data_t key, st_data_t value, st_data_t ext
466
513
 
467
514
  VALUE ref;
468
515
 
516
+ size_t iteration_gen = recorder->iteration_gen;
517
+ size_t alloc_gen = record->object_data.alloc_gen;
518
+ // Guard against potential overflows given unsigned types here.
519
+ record->object_data.gen_age = alloc_gen < iteration_gen ? iteration_gen - alloc_gen : 0;
520
+
521
+ if (should_exclude_from_iteration(record)) {
522
+ // If an object won't be included in the current iteration, there's
523
+ // no point checking for liveness or updating its size, so exit early.
524
+ // NOTE: This means that there should be an equivalent check during actual
525
+ // iteration otherwise we'd iterate/expose stale object data.
526
+ recorder->stats_last_update.objects_skipped++;
527
+ return ST_CONTINUE;
528
+ }
529
+
469
530
  if (!ruby_ref_from_id(LONG2NUM(obj_id), &ref)) {
470
531
  // Id no longer associated with a valid ref. Need to delete this object record!
471
532
  on_committed_object_record_cleanup(recorder, record);
533
+ recorder->stats_last_update.objects_dead++;
472
534
  return ST_DELETE;
473
535
  }
474
536
 
@@ -503,6 +565,7 @@ static int st_object_record_update(st_data_t key, st_data_t value, st_data_t ext
503
565
  RB_FL_SET(ref, RUBY_FL_SEEN_OBJ_ID);
504
566
 
505
567
  on_committed_object_record_cleanup(recorder, record);
568
+ recorder->stats_last_update.objects_dead++;
506
569
  return ST_DELETE;
507
570
  }
508
571
 
@@ -516,6 +579,11 @@ static int st_object_record_update(st_data_t key, st_data_t value, st_data_t ext
516
579
  record->object_data.is_frozen = RB_OBJ_FROZEN(ref);
517
580
  }
518
581
 
582
+ recorder->stats_last_update.objects_alive++;
583
+ if (record->object_data.is_frozen) {
584
+ recorder->stats_last_update.objects_frozen++;
585
+ }
586
+
519
587
  return ST_CONTINUE;
520
588
  }
521
589
 
@@ -525,8 +593,16 @@ static int st_object_records_iterate(DDTRACE_UNUSED st_data_t key, st_data_t val
525
593
  const heap_stack *stack = record->heap_record->stack;
526
594
  iteration_context *context = (iteration_context*) extra;
527
595
 
528
- ddog_prof_Location *locations = context->heap_recorder->reusable_locations;
596
+ const heap_recorder *recorder = context->heap_recorder;
597
+
598
+ if (should_exclude_from_iteration(record)) {
599
+ // Skip objects that should not be included in iteration
600
+ // NOTE: This matches the short-circuiting condition in st_object_record_update
601
+ // and prevents iteration over stale objects.
602
+ return ST_CONTINUE;
603
+ }
529
604
 
605
+ ddog_prof_Location *locations = recorder->reusable_locations;
530
606
  for (uint16_t i = 0; i < stack->frames_len; i++) {
531
607
  const heap_frame *frame = &stack->frames[i];
532
608
  ddog_prof_Location *location = &locations[i];
@@ -725,9 +801,10 @@ void object_record_free(object_record *record) {
725
801
 
726
802
  VALUE object_record_inspect(object_record *record) {
727
803
  heap_frame top_frame = record->heap_record->stack->frames[0];
728
- VALUE inspect = rb_sprintf("obj_id=%ld weight=%d size=%zu location=%s:%d alloc_gen=%zu ",
729
- record->obj_id, record->object_data.weight, record->object_data.size, top_frame.filename,
730
- (int) top_frame.line, record->object_data.alloc_gen);
804
+ live_object_data object_data = record->object_data;
805
+ VALUE inspect = rb_sprintf("obj_id=%ld weight=%d size=%zu location=%s:%d alloc_gen=%zu gen_age=%zu frozen=%d ",
806
+ record->obj_id, object_data.weight, object_data.size, top_frame.filename,
807
+ (int) top_frame.line, object_data.alloc_gen, object_data.gen_age, object_data.is_frozen);
731
808
 
732
809
  const char *class = record->object_data.class;
733
810
  if (class != NULL) {
@@ -27,7 +27,9 @@ typedef struct live_object_data {
27
27
  // could be seen as being representative of 50 objects.
28
28
  unsigned int weight;
29
29
 
30
- // Size of this object on last flush/update.
30
+ // Size of this object in memory.
31
+ // NOTE: This only gets updated during heap_recorder_prepare_iteration and only
32
+ // for those objects that meet the minimum iteration age requirements.
31
33
  size_t size;
32
34
 
33
35
  // The class of the object that we're tracking.
@@ -39,6 +41,10 @@ typedef struct live_object_data {
39
41
  // This enables us to calculate the age of this object in terms of GC executions.
40
42
  size_t alloc_gen;
41
43
 
44
+ // The age of this object in terms of GC generations.
45
+ // NOTE: This only gets updated during heap_recorder_prepare_iteration
46
+ size_t gen_age;
47
+
42
48
  // Whether this object was previously seen as being frozen. If this is the case,
43
49
  // we'll skip any further size updates since frozen objects are supposed to be
44
50
  // immutable.
@@ -144,6 +150,11 @@ bool heap_recorder_for_each_live_object(
144
150
  bool (*for_each_callback)(heap_recorder_iteration_data data, void* extra_arg),
145
151
  void *for_each_callback_extra_arg);
146
152
 
153
+ // Return a Ruby hash containing a snapshot of this recorder's interesting state at calling time.
154
+ // WARN: This allocates in the Ruby VM and therefore should not be called without the
155
+ // VM lock or during GC.
156
+ VALUE heap_recorder_state_snapshot(heap_recorder *heap_recorder);
157
+
147
158
  // v--- TEST-ONLY APIs ---v
148
159
 
149
160
  // Assert internal hashing logic is valid for the provided locations and its
@@ -30,7 +30,7 @@ inline static ddog_ByteSlice byte_slice_from_ruby_string(VALUE string);
30
30
  static VALUE _native_validate_exporter(VALUE self, VALUE exporter_configuration);
31
31
  static ddog_prof_Exporter_NewResult create_exporter(VALUE exporter_configuration, VALUE tags_as_array);
32
32
  static VALUE handle_exporter_failure(ddog_prof_Exporter_NewResult exporter_result);
33
- static ddog_Endpoint endpoint_from(VALUE exporter_configuration);
33
+ static ddog_prof_Endpoint endpoint_from(VALUE exporter_configuration);
34
34
  static ddog_Vec_Tag convert_tags(VALUE tags_as_array);
35
35
  static void safely_log_failure_to_process_tag(ddog_Vec_Tag tags, VALUE err_details);
36
36
  static VALUE _native_do_export(
@@ -46,17 +46,17 @@ static VALUE _native_do_export(
46
46
  VALUE code_provenance_file_name,
47
47
  VALUE code_provenance_data,
48
48
  VALUE tags_as_array,
49
- VALUE internal_metadata_json
49
+ VALUE internal_metadata_json,
50
+ VALUE info_json
50
51
  );
51
52
  static void *call_exporter_without_gvl(void *call_args);
52
53
  static void interrupt_exporter_call(void *cancel_token);
53
- static VALUE ddtrace_version(void);
54
54
 
55
55
  void http_transport_init(VALUE profiling_module) {
56
56
  VALUE http_transport_class = rb_define_class_under(profiling_module, "HttpTransport", rb_cObject);
57
57
 
58
58
  rb_define_singleton_method(http_transport_class, "_native_validate_exporter", _native_validate_exporter, 1);
59
- rb_define_singleton_method(http_transport_class, "_native_do_export", _native_do_export, 12);
59
+ rb_define_singleton_method(http_transport_class, "_native_do_export", _native_do_export, 13);
60
60
 
61
61
  ok_symbol = ID2SYM(rb_intern_const("ok"));
62
62
  error_symbol = ID2SYM(rb_intern_const("error"));
@@ -94,7 +94,7 @@ static ddog_prof_Exporter_NewResult create_exporter(VALUE exporter_configuration
94
94
 
95
95
  // This needs to be called BEFORE convert_tags since it can raise an exception and thus cause the ddog_Vec_Tag
96
96
  // to be leaked.
97
- ddog_Endpoint endpoint = endpoint_from(exporter_configuration);
97
+ ddog_prof_Endpoint endpoint = endpoint_from(exporter_configuration);
98
98
 
99
99
  ddog_Vec_Tag tags = convert_tags(tags_as_array);
100
100
 
@@ -116,7 +116,7 @@ static VALUE handle_exporter_failure(ddog_prof_Exporter_NewResult exporter_resul
116
116
  rb_ary_new_from_args(2, error_symbol, get_error_details_and_drop(&exporter_result.err));
117
117
  }
118
118
 
119
- static ddog_Endpoint endpoint_from(VALUE exporter_configuration) {
119
+ static ddog_prof_Endpoint endpoint_from(VALUE exporter_configuration) {
120
120
  ENFORCE_TYPE(exporter_configuration, T_ARRAY);
121
121
 
122
122
  ID working_mode = SYM2ID(rb_ary_entry(exporter_configuration, 0)); // SYM2ID verifies its input so we can do this safely
@@ -131,12 +131,12 @@ static ddog_Endpoint endpoint_from(VALUE exporter_configuration) {
131
131
  ENFORCE_TYPE(site, T_STRING);
132
132
  ENFORCE_TYPE(api_key, T_STRING);
133
133
 
134
- return ddog_Endpoint_agentless(char_slice_from_ruby_string(site), char_slice_from_ruby_string(api_key));
134
+ return ddog_prof_Endpoint_agentless(char_slice_from_ruby_string(site), char_slice_from_ruby_string(api_key));
135
135
  } else { // agent_id
136
136
  VALUE base_url = rb_ary_entry(exporter_configuration, 1);
137
137
  ENFORCE_TYPE(base_url, T_STRING);
138
138
 
139
- return ddog_Endpoint_agent(char_slice_from_ruby_string(base_url));
139
+ return ddog_prof_Endpoint_agent(char_slice_from_ruby_string(base_url));
140
140
  }
141
141
  }
142
142
 
@@ -208,6 +208,7 @@ static VALUE perform_export(
208
208
  ddog_prof_Exporter_Slice_File files_to_export_unmodified,
209
209
  ddog_Vec_Tag *additional_tags,
210
210
  ddog_CharSlice internal_metadata,
211
+ ddog_CharSlice info,
211
212
  uint64_t timeout_milliseconds
212
213
  ) {
213
214
  ddog_prof_ProfiledEndpointsStats *endpoints_stats = NULL; // Not in use yet
@@ -220,6 +221,7 @@ static VALUE perform_export(
220
221
  additional_tags,
221
222
  endpoints_stats,
222
223
  &internal_metadata,
224
+ &info,
223
225
  timeout_milliseconds
224
226
  );
225
227
 
@@ -290,7 +292,8 @@ static VALUE _native_do_export(
290
292
  VALUE code_provenance_file_name,
291
293
  VALUE code_provenance_data,
292
294
  VALUE tags_as_array,
293
- VALUE internal_metadata_json
295
+ VALUE internal_metadata_json,
296
+ VALUE info_json
294
297
  ) {
295
298
  ENFORCE_TYPE(upload_timeout_milliseconds, T_FIXNUM);
296
299
  ENFORCE_TYPE(start_timespec_seconds, T_FIXNUM);
@@ -301,6 +304,7 @@ static VALUE _native_do_export(
301
304
  ENFORCE_TYPE(pprof_data, T_STRING);
302
305
  ENFORCE_TYPE(code_provenance_file_name, T_STRING);
303
306
  ENFORCE_TYPE(internal_metadata_json, T_STRING);
307
+ ENFORCE_TYPE(info_json, T_STRING);
304
308
 
305
309
  // Code provenance can be disabled and in that case will be set to nil
306
310
  bool have_code_provenance = !NIL_P(code_provenance_data);
@@ -335,6 +339,7 @@ static VALUE _native_do_export(
335
339
 
336
340
  ddog_Vec_Tag *null_additional_tags = NULL;
337
341
  ddog_CharSlice internal_metadata = char_slice_from_ruby_string(internal_metadata_json);
342
+ ddog_CharSlice info = char_slice_from_ruby_string(info_json);
338
343
 
339
344
  ddog_prof_Exporter_NewResult exporter_result = create_exporter(exporter_configuration, tags_as_array);
340
345
  // Note: Do not add anything that can raise exceptions after this line, as otherwise the exporter memory will leak
@@ -350,6 +355,7 @@ static VALUE _native_do_export(
350
355
  files_to_export_unmodified,
351
356
  null_additional_tags,
352
357
  internal_metadata,
358
+ info,
353
359
  timeout_milliseconds
354
360
  );
355
361
  }
@@ -367,13 +373,3 @@ static void *call_exporter_without_gvl(void *call_args) {
367
373
  static void interrupt_exporter_call(void *cancel_token) {
368
374
  ddog_CancellationToken_cancel((ddog_CancellationToken *) cancel_token);
369
375
  }
370
-
371
- static VALUE ddtrace_version(void) {
372
- VALUE ddtrace_module = rb_const_get(rb_cObject, rb_intern("DDTrace"));
373
- ENFORCE_TYPE(ddtrace_module, T_MODULE);
374
- VALUE version_module = rb_const_get(ddtrace_module, rb_intern("VERSION"));
375
- ENFORCE_TYPE(version_module, T_MODULE);
376
- VALUE version_string = rb_const_get(version_module, rb_intern("STRING"));
377
- ENFORCE_TYPE(version_string, T_STRING);
378
- return version_string;
379
- }
@@ -15,7 +15,7 @@ module Datadog
15
15
  # The MJIT header was introduced on 2.6 and removed on 3.3; for other Rubies we rely on debase-ruby_core_source
16
16
  CAN_USE_MJIT_HEADER = RUBY_VERSION.start_with?('2.6', '2.7', '3.0.', '3.1.', '3.2.')
17
17
 
18
- LIBDATADOG_VERSION = '~> 5.0.0.1.0'
18
+ LIBDATADOG_VERSION = '~> 7.0.0.1.0'
19
19
 
20
20
  def self.fail_install_if_missing_extension?
21
21
  ENV[ENV_FAIL_INSTALL_IF_MISSING_EXTENSION].to_s.strip.downcase == 'true'
@@ -29,7 +29,7 @@ module Datadog
29
29
  # native extension), we need to add a "runpath" -- a list of folders to search for libdatadog.
30
30
  #
31
31
  # This runpath gets hardcoded at native library linking time. You can look at it using the `readelf` tool in
32
- # Linux: e.g. `readelf -d ddtrace_profiling_native_extension.2.7.3_x86_64-linux.so`.
32
+ # Linux: e.g. `readelf -d datadog_profiling_native_extension.2.7.3_x86_64-linux.so`.
33
33
  #
34
34
  # In older versions of ddtrace, we only set as runpath an absolute path to libdatadog.
35
35
  # (This gets set automatically by the call
@@ -305,8 +305,8 @@ module Datadog
305
305
  no_binaries_for_current_platform = explain_issue(
306
306
  'the `libdatadog` gem installed on your system is missing binaries for your',
307
307
  'platform variant.',
308
- "(Your platform: `#{Gem::Platform.local}`)",
309
- '(Available binaries: ',
308
+ "(Your platform: `#{Libdatadog.current_platform}`)",
309
+ '(Available binaries:',
310
310
  "`#{Libdatadog.available_binaries.join('`, `')}`)",
311
311
  suggested: CONTACT_SUPPORT,
312
312
  )
@@ -876,3 +876,17 @@ static inline int ddtrace_imemo_type(VALUE imemo) {
876
876
  return NULL;
877
877
  }
878
878
  #endif
879
+
880
+ // This is used to workaround a VM bug. See "handle_sampling_signal" in "collectors_cpu_and_wall_time_worker" for details.
881
+ #ifdef NO_POSTPONED_TRIGGER
882
+ void *objspace_ptr_for_gc_finalize_deferred_workaround(void) {
883
+ rb_vm_t *vm =
884
+ #ifndef NO_GET_VM // TODO: Inline GET_VM below once we drop support in dd-trace-rb 2.x for < Ruby 2.5
885
+ GET_VM();
886
+ #else
887
+ thread_struct_from_object(rb_thread_current())->vm;
888
+ #endif
889
+
890
+ return vm->objspace;
891
+ }
892
+ #endif
@@ -55,3 +55,7 @@ void self_test_mn_enabled(void);
55
55
 
56
56
  // Provides more specific information on what kind an imemo is
57
57
  const char *imemo_kind(VALUE imemo);
58
+
59
+ #ifdef NO_POSTPONED_TRIGGER
60
+ void *objspace_ptr_for_gc_finalize_deferred_workaround(void);
61
+ #endif
@@ -38,7 +38,7 @@ static VALUE _native_enforce_success(DDTRACE_UNUSED VALUE _self, VALUE syserr_er
38
38
  static void *trigger_enforce_success(void *trigger_args);
39
39
  static VALUE _native_malloc_stats(DDTRACE_UNUSED VALUE _self);
40
40
 
41
- void DDTRACE_EXPORT Init_ddtrace_profiling_native_extension(void) {
41
+ void DDTRACE_EXPORT Init_datadog_profiling_native_extension(void) {
42
42
  VALUE datadog_module = rb_define_module("Datadog");
43
43
  VALUE profiling_module = rb_define_module_under(datadog_module, "Profiling");
44
44
  VALUE native_extension_module = rb_define_module_under(profiling_module, "NativeExtension");
@@ -255,3 +255,13 @@ VALUE ruby_safe_inspect(VALUE obj) {
255
255
  return rb_str_new_cstr("(Not inspectable)");
256
256
  }
257
257
  }
258
+
259
+ VALUE ddtrace_version(void) {
260
+ VALUE ddtrace_module = rb_const_get(rb_cObject, rb_intern("DDTrace"));
261
+ ENFORCE_TYPE(ddtrace_module, T_MODULE);
262
+ VALUE version_module = rb_const_get(ddtrace_module, rb_intern("VERSION"));
263
+ ENFORCE_TYPE(version_module, T_MODULE);
264
+ VALUE version_string = rb_const_get(version_module, rb_intern("STRING"));
265
+ ENFORCE_TYPE(version_string, T_STRING);
266
+ return version_string;
267
+ }