datadog 2.23.0 → 2.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +132 -2
- data/ext/datadog_profiling_native_extension/clock_id_from_pthread.c +2 -1
- data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +100 -29
- data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +2 -2
- data/ext/datadog_profiling_native_extension/collectors_gc_profiling_helper.c +3 -2
- data/ext/datadog_profiling_native_extension/collectors_stack.c +23 -10
- data/ext/datadog_profiling_native_extension/collectors_thread_context.c +16 -12
- data/ext/datadog_profiling_native_extension/crashtracking_runtime_stacks.c +239 -0
- data/ext/datadog_profiling_native_extension/datadog_ruby_common.c +48 -1
- data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +41 -0
- data/ext/datadog_profiling_native_extension/encoded_profile.c +2 -1
- data/ext/datadog_profiling_native_extension/extconf.rb +4 -1
- data/ext/datadog_profiling_native_extension/heap_recorder.c +24 -24
- data/ext/datadog_profiling_native_extension/http_transport.c +10 -4
- data/ext/datadog_profiling_native_extension/libdatadog_helpers.c +3 -22
- data/ext/datadog_profiling_native_extension/libdatadog_helpers.h +0 -5
- data/ext/datadog_profiling_native_extension/private_vm_api_access.c +21 -8
- data/ext/datadog_profiling_native_extension/private_vm_api_access.h +4 -0
- data/ext/datadog_profiling_native_extension/profiling.c +22 -15
- data/ext/datadog_profiling_native_extension/ruby_helpers.c +55 -44
- data/ext/datadog_profiling_native_extension/ruby_helpers.h +17 -5
- data/ext/datadog_profiling_native_extension/setup_signal_handler.c +8 -2
- data/ext/datadog_profiling_native_extension/setup_signal_handler.h +3 -0
- data/ext/datadog_profiling_native_extension/stack_recorder.c +16 -16
- data/ext/datadog_profiling_native_extension/unsafe_api_calls_check.c +2 -1
- data/ext/datadog_profiling_native_extension/unsafe_api_calls_check.h +5 -2
- data/ext/libdatadog_api/crashtracker.c +5 -8
- data/ext/libdatadog_api/datadog_ruby_common.c +48 -1
- data/ext/libdatadog_api/datadog_ruby_common.h +41 -0
- data/ext/libdatadog_api/ddsketch.c +4 -8
- data/ext/libdatadog_api/feature_flags.c +5 -5
- data/ext/libdatadog_api/helpers.h +27 -0
- data/ext/libdatadog_api/init.c +4 -0
- data/ext/libdatadog_extconf_helpers.rb +1 -1
- data/lib/datadog/ai_guard/api_client.rb +82 -0
- data/lib/datadog/ai_guard/component.rb +42 -0
- data/lib/datadog/ai_guard/configuration/ext.rb +17 -0
- data/lib/datadog/ai_guard/configuration/settings.rb +110 -0
- data/lib/datadog/ai_guard/configuration.rb +11 -0
- data/lib/datadog/ai_guard/contrib/integration.rb +37 -0
- data/lib/datadog/ai_guard/contrib/ruby_llm/chat_instrumentation.rb +42 -0
- data/lib/datadog/ai_guard/contrib/ruby_llm/integration.rb +41 -0
- data/lib/datadog/ai_guard/contrib/ruby_llm/patcher.rb +30 -0
- data/lib/datadog/ai_guard/evaluation/message.rb +25 -0
- data/lib/datadog/ai_guard/evaluation/no_op_result.rb +34 -0
- data/lib/datadog/ai_guard/evaluation/request.rb +81 -0
- data/lib/datadog/ai_guard/evaluation/result.rb +43 -0
- data/lib/datadog/ai_guard/evaluation/tool_call.rb +18 -0
- data/lib/datadog/ai_guard/evaluation.rb +72 -0
- data/lib/datadog/ai_guard/ext.rb +16 -0
- data/lib/datadog/ai_guard.rb +155 -0
- data/lib/datadog/appsec/api_security/endpoint_collection/rails_collector.rb +8 -1
- data/lib/datadog/appsec/api_security/endpoint_collection/rails_route_serializer.rb +9 -2
- data/lib/datadog/appsec/component.rb +1 -1
- data/lib/datadog/appsec/context.rb +5 -4
- data/lib/datadog/appsec/contrib/active_record/integration.rb +1 -1
- data/lib/datadog/appsec/contrib/active_record/patcher.rb +1 -1
- data/lib/datadog/appsec/contrib/excon/integration.rb +1 -1
- data/lib/datadog/appsec/contrib/excon/patcher.rb +1 -1
- data/lib/datadog/appsec/contrib/excon/ssrf_detection_middleware.rb +47 -12
- data/lib/datadog/appsec/contrib/faraday/ssrf_detection_middleware.rb +32 -15
- data/lib/datadog/appsec/contrib/rails/patcher.rb +10 -2
- data/lib/datadog/appsec/contrib/rest_client/integration.rb +1 -1
- data/lib/datadog/appsec/contrib/rest_client/patcher.rb +1 -1
- data/lib/datadog/appsec/contrib/rest_client/request_ssrf_detection_patch.rb +50 -14
- data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +5 -4
- data/lib/datadog/appsec/contrib/sinatra/integration.rb +1 -1
- data/lib/datadog/appsec/contrib/sinatra/patcher.rb +4 -4
- data/lib/datadog/appsec/contrib/sinatra/patches/json_patch.rb +1 -1
- data/lib/datadog/appsec/ext.rb +2 -0
- data/lib/datadog/appsec/metrics/collector.rb +8 -3
- data/lib/datadog/appsec/metrics/exporter.rb +7 -0
- data/lib/datadog/appsec/metrics/telemetry.rb +7 -2
- data/lib/datadog/appsec/metrics.rb +5 -5
- data/lib/datadog/appsec/remote.rb +7 -14
- data/lib/datadog/appsec/security_engine/engine.rb +3 -3
- data/lib/datadog/appsec/security_engine/result.rb +2 -1
- data/lib/datadog/appsec/security_engine/runner.rb +2 -2
- data/lib/datadog/appsec/utils/http/media_type.rb +37 -23
- data/lib/datadog/appsec.rb +7 -1
- data/lib/datadog/core/configuration/components.rb +7 -0
- data/lib/datadog/core/configuration/config_helper.rb +1 -1
- data/lib/datadog/core/configuration/deprecations.rb +2 -2
- data/lib/datadog/core/configuration/option_definition.rb +4 -2
- data/lib/datadog/core/configuration/options.rb +8 -5
- data/lib/datadog/core/configuration/settings.rb +31 -3
- data/lib/datadog/core/configuration/supported_configurations.rb +10 -1
- data/lib/datadog/core/crashtracking/tag_builder.rb +6 -0
- data/lib/datadog/core/environment/cgroup.rb +52 -25
- data/lib/datadog/core/environment/container.rb +140 -46
- data/lib/datadog/core/environment/ext.rb +1 -0
- data/lib/datadog/core/environment/process.rb +9 -1
- data/lib/datadog/core/error.rb +6 -6
- data/lib/datadog/core/knuth_sampler.rb +57 -0
- data/lib/datadog/core/pin.rb +4 -0
- data/lib/datadog/core/rate_limiter.rb +9 -1
- data/lib/datadog/core/remote/client.rb +14 -6
- data/lib/datadog/core/remote/component.rb +6 -4
- data/lib/datadog/core/remote/configuration/content.rb +15 -2
- data/lib/datadog/core/remote/configuration/digest.rb +14 -7
- data/lib/datadog/core/remote/configuration/repository.rb +1 -1
- data/lib/datadog/core/remote/configuration/target.rb +13 -6
- data/lib/datadog/core/remote/transport/config.rb +3 -16
- data/lib/datadog/core/remote/transport/http/config.rb +4 -44
- data/lib/datadog/core/remote/transport/http/negotiation.rb +0 -39
- data/lib/datadog/core/remote/transport/http.rb +13 -24
- data/lib/datadog/core/remote/transport/negotiation.rb +7 -16
- data/lib/datadog/core/runtime/metrics.rb +11 -1
- data/lib/datadog/core/semaphore.rb +1 -4
- data/lib/datadog/core/telemetry/component.rb +52 -13
- data/lib/datadog/core/telemetry/event/app_started.rb +36 -1
- data/lib/datadog/core/telemetry/event/synth_app_client_configuration_change.rb +27 -4
- data/lib/datadog/core/telemetry/logger.rb +2 -0
- data/lib/datadog/core/telemetry/logging.rb +20 -2
- data/lib/datadog/core/telemetry/metrics_manager.rb +9 -0
- data/lib/datadog/core/telemetry/request.rb +17 -3
- data/lib/datadog/core/telemetry/transport/http/telemetry.rb +2 -32
- data/lib/datadog/core/telemetry/transport/http.rb +21 -16
- data/lib/datadog/core/telemetry/transport/telemetry.rb +3 -10
- data/lib/datadog/core/telemetry/worker.rb +88 -32
- data/lib/datadog/core/transport/ext.rb +2 -0
- data/lib/datadog/core/transport/http/api/endpoint.rb +9 -4
- data/lib/datadog/core/transport/http/api/instance.rb +4 -21
- data/lib/datadog/core/transport/http/builder.rb +9 -5
- data/lib/datadog/core/transport/http/client.rb +19 -8
- data/lib/datadog/core/transport/http.rb +22 -19
- data/lib/datadog/core/transport/response.rb +12 -1
- data/lib/datadog/core/transport/transport.rb +90 -0
- data/lib/datadog/core/utils/only_once_successful.rb +2 -0
- data/lib/datadog/core/utils/safe_dup.rb +2 -2
- data/lib/datadog/core/utils/sequence.rb +2 -0
- data/lib/datadog/core/utils/time.rb +1 -1
- data/lib/datadog/core/workers/async.rb +10 -1
- data/lib/datadog/core/workers/interval_loop.rb +44 -3
- data/lib/datadog/core/workers/polling.rb +2 -0
- data/lib/datadog/core/workers/queue.rb +100 -1
- data/lib/datadog/data_streams/processor.rb +1 -1
- data/lib/datadog/data_streams/transport/http/stats.rb +1 -36
- data/lib/datadog/data_streams/transport/http.rb +5 -6
- data/lib/datadog/data_streams/transport/stats.rb +3 -17
- data/lib/datadog/di/boot.rb +4 -2
- data/lib/datadog/di/configuration/settings.rb +22 -0
- data/lib/datadog/di/contrib/active_record.rb +30 -5
- data/lib/datadog/di/el/compiler.rb +8 -4
- data/lib/datadog/di/error.rb +5 -0
- data/lib/datadog/di/instrumenter.rb +26 -7
- data/lib/datadog/di/logger.rb +2 -2
- data/lib/datadog/di/probe_builder.rb +2 -1
- data/lib/datadog/di/probe_file_loader/railtie.rb +1 -1
- data/lib/datadog/di/probe_manager.rb +37 -31
- data/lib/datadog/di/probe_notification_builder.rb +15 -2
- data/lib/datadog/di/probe_notifier_worker.rb +5 -5
- data/lib/datadog/di/redactor.rb +8 -1
- data/lib/datadog/di/remote.rb +89 -84
- data/lib/datadog/di/transport/diagnostics.rb +7 -35
- data/lib/datadog/di/transport/http/diagnostics.rb +1 -31
- data/lib/datadog/di/transport/http/input.rb +1 -31
- data/lib/datadog/di/transport/http.rb +28 -17
- data/lib/datadog/di/transport/input.rb +7 -34
- data/lib/datadog/di.rb +61 -5
- data/lib/datadog/error_tracking/filters.rb +2 -2
- data/lib/datadog/kit/appsec/events/v2.rb +2 -3
- data/lib/datadog/open_feature/evaluation_engine.rb +2 -1
- data/lib/datadog/open_feature/remote.rb +3 -10
- data/lib/datadog/open_feature/transport.rb +9 -11
- data/lib/datadog/opentelemetry/api/baggage.rb +1 -1
- data/lib/datadog/opentelemetry/configuration/settings.rb +2 -2
- data/lib/datadog/opentelemetry/metrics.rb +21 -14
- data/lib/datadog/opentelemetry/sdk/metrics_exporter.rb +5 -8
- data/lib/datadog/profiling/collectors/code_provenance.rb +27 -2
- data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +3 -2
- data/lib/datadog/profiling/collectors/info.rb +5 -4
- data/lib/datadog/profiling/component.rb +25 -11
- data/lib/datadog/profiling/exporter.rb +4 -0
- data/lib/datadog/profiling/ext/dir_monkey_patches.rb +18 -0
- data/lib/datadog/profiling/ext/exec_monkey_patch.rb +32 -0
- data/lib/datadog/profiling/flush.rb +3 -0
- data/lib/datadog/profiling/http_transport.rb +4 -1
- data/lib/datadog/profiling/profiler.rb +3 -5
- data/lib/datadog/profiling/scheduler.rb +8 -7
- data/lib/datadog/profiling/tag_builder.rb +1 -0
- data/lib/datadog/tracing/contrib/extensions.rb +10 -2
- data/lib/datadog/tracing/contrib/karafka/patcher.rb +31 -32
- data/lib/datadog/tracing/contrib/status_range_matcher.rb +2 -1
- data/lib/datadog/tracing/contrib/utils/quantization/hash.rb +3 -1
- data/lib/datadog/tracing/contrib/waterdrop/patcher.rb +6 -3
- data/lib/datadog/tracing/contrib/waterdrop.rb +4 -0
- data/lib/datadog/tracing/diagnostics/environment_logger.rb +1 -1
- data/lib/datadog/tracing/distributed/baggage.rb +3 -2
- data/lib/datadog/tracing/remote.rb +1 -9
- data/lib/datadog/tracing/sampling/priority_sampler.rb +3 -1
- data/lib/datadog/tracing/sampling/rate_sampler.rb +8 -19
- data/lib/datadog/tracing/span.rb +1 -1
- data/lib/datadog/tracing/span_event.rb +2 -2
- data/lib/datadog/tracing/span_operation.rb +20 -9
- data/lib/datadog/tracing/trace_operation.rb +44 -6
- data/lib/datadog/tracing/tracer.rb +42 -16
- data/lib/datadog/tracing/transport/http/traces.rb +2 -50
- data/lib/datadog/tracing/transport/http.rb +15 -9
- data/lib/datadog/tracing/transport/io/client.rb +1 -1
- data/lib/datadog/tracing/transport/traces.rb +6 -66
- data/lib/datadog/tracing/workers/trace_writer.rb +5 -0
- data/lib/datadog/tracing/writer.rb +1 -0
- data/lib/datadog/version.rb +2 -2
- data/lib/datadog.rb +1 -0
- metadata +33 -19
- data/lib/datadog/core/remote/transport/http/api.rb +0 -53
- data/lib/datadog/core/telemetry/transport/http/api.rb +0 -43
- data/lib/datadog/core/transport/http/api/spec.rb +0 -36
- data/lib/datadog/data_streams/transport/http/api.rb +0 -33
- data/lib/datadog/data_streams/transport/http/client.rb +0 -21
- data/lib/datadog/di/transport/http/api.rb +0 -42
- data/lib/datadog/opentelemetry/api/baggage.rbs +0 -26
- data/lib/datadog/tracing/transport/http/api.rb +0 -44
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
#include "helpers.h"
|
|
9
9
|
#include "libdatadog_helpers.h"
|
|
10
10
|
#include "private_vm_api_access.h"
|
|
11
|
+
#include "ruby_helpers.h"
|
|
11
12
|
#include "stack_recorder.h"
|
|
12
13
|
#include "time_helpers.h"
|
|
13
14
|
#include "unsafe_api_calls_check.h"
|
|
@@ -292,7 +293,7 @@ static bool handle_gvl_waiting(
|
|
|
292
293
|
static VALUE _native_on_gvl_waiting(DDTRACE_UNUSED VALUE self, VALUE thread);
|
|
293
294
|
static VALUE _native_gvl_waiting_at_for(DDTRACE_UNUSED VALUE self, VALUE thread);
|
|
294
295
|
static VALUE _native_on_gvl_running(DDTRACE_UNUSED VALUE self, VALUE thread);
|
|
295
|
-
static VALUE _native_sample_after_gvl_running(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread);
|
|
296
|
+
static VALUE _native_sample_after_gvl_running(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread, VALUE allow_exception);
|
|
296
297
|
static VALUE _native_apply_delta_to_cpu_time_at_previous_sample_ns(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread, VALUE delta_ns);
|
|
297
298
|
static void otel_without_ddtrace_trace_identifiers_for(
|
|
298
299
|
thread_context_collector_state *state,
|
|
@@ -342,7 +343,7 @@ void collectors_thread_context_init(VALUE profiling_module) {
|
|
|
342
343
|
rb_define_singleton_method(testing_module, "_native_on_gvl_waiting", _native_on_gvl_waiting, 1);
|
|
343
344
|
rb_define_singleton_method(testing_module, "_native_gvl_waiting_at_for", _native_gvl_waiting_at_for, 1);
|
|
344
345
|
rb_define_singleton_method(testing_module, "_native_on_gvl_running", _native_on_gvl_running, 1);
|
|
345
|
-
rb_define_singleton_method(testing_module, "_native_sample_after_gvl_running", _native_sample_after_gvl_running,
|
|
346
|
+
rb_define_singleton_method(testing_module, "_native_sample_after_gvl_running", _native_sample_after_gvl_running, 3);
|
|
346
347
|
rb_define_singleton_method(testing_module, "_native_apply_delta_to_cpu_time_at_previous_sample_ns", _native_apply_delta_to_cpu_time_at_previous_sample_ns, 3);
|
|
347
348
|
#endif
|
|
348
349
|
|
|
@@ -518,7 +519,7 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
|
|
|
518
519
|
} else if (otel_context_enabled == ID2SYM(rb_intern("both"))) {
|
|
519
520
|
state->otel_context_enabled = OTEL_CONTEXT_ENABLED_BOTH;
|
|
520
521
|
} else {
|
|
521
|
-
|
|
522
|
+
raise_error(rb_eArgError, "Unexpected value for otel_context_enabled: %+" PRIsVALUE, otel_context_enabled);
|
|
522
523
|
}
|
|
523
524
|
|
|
524
525
|
global_waiting_for_gvl_threshold_ns = NUM2UINT(waiting_for_gvl_threshold_ns);
|
|
@@ -539,7 +540,7 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
|
|
|
539
540
|
static VALUE _native_sample(DDTRACE_UNUSED VALUE _self, VALUE collector_instance, VALUE profiler_overhead_stack_thread, VALUE allow_exception) {
|
|
540
541
|
ENFORCE_BOOLEAN(allow_exception);
|
|
541
542
|
|
|
542
|
-
if (!is_thread_alive(profiler_overhead_stack_thread))
|
|
543
|
+
if (!is_thread_alive(profiler_overhead_stack_thread)) raise_error(rb_eArgError, "Unexpected: profiler_overhead_stack_thread is not alive");
|
|
543
544
|
|
|
544
545
|
if (allow_exception == Qfalse) debug_enter_unsafe_context();
|
|
545
546
|
|
|
@@ -831,7 +832,7 @@ VALUE thread_context_collector_sample_after_gc(VALUE self_instance) {
|
|
|
831
832
|
TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
|
|
832
833
|
|
|
833
834
|
if (state->gc_tracking.wall_time_at_previous_gc_ns == INVALID_TIME) {
|
|
834
|
-
|
|
835
|
+
raise_error(rb_eRuntimeError, "BUG: Unexpected call to sample_after_gc without valid GC information available");
|
|
835
836
|
}
|
|
836
837
|
|
|
837
838
|
int max_labels_needed_for_gc = 7; // Magic number gets validated inside gc_profiling_set_metadata
|
|
@@ -998,7 +999,7 @@ static void trigger_sample_for_thread(
|
|
|
998
999
|
// @ivoanjo: I wonder if C compilers are smart enough to statically prove this check never triggers unless someone
|
|
999
1000
|
// changes the code erroneously and remove it entirely?
|
|
1000
1001
|
if (label_pos > max_label_count) {
|
|
1001
|
-
|
|
1002
|
+
raise_error(rb_eRuntimeError, "BUG: Unexpected label_pos (%d) > max_label_count (%d)", label_pos, max_label_count);
|
|
1002
1003
|
}
|
|
1003
1004
|
|
|
1004
1005
|
ddog_prof_Slice_Label slice_labels = {.ptr = labels, .len = label_pos};
|
|
@@ -1295,7 +1296,7 @@ static long update_time_since_previous_sample(long *time_at_previous_sample_ns,
|
|
|
1295
1296
|
elapsed_time_ns = 0;
|
|
1296
1297
|
} else {
|
|
1297
1298
|
// We don't expect non-wall time to go backwards, so let's flag this as a bug
|
|
1298
|
-
|
|
1299
|
+
raise_error(rb_eRuntimeError, "BUG: Unexpected negative elapsed_time_ns between samples");
|
|
1299
1300
|
}
|
|
1300
1301
|
}
|
|
1301
1302
|
|
|
@@ -1961,7 +1962,7 @@ static uint64_t otel_span_id_to_uint(VALUE otel_span_id) {
|
|
|
1961
1962
|
thread_context_collector_state *state;
|
|
1962
1963
|
TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
|
|
1963
1964
|
|
|
1964
|
-
if (!state->timeline_enabled)
|
|
1965
|
+
if (!state->timeline_enabled) raise_error(rb_eRuntimeError, "GVL profiling requires timeline to be enabled");
|
|
1965
1966
|
|
|
1966
1967
|
intptr_t gvl_waiting_at = gvl_profiling_state_thread_object_get(current_thread);
|
|
1967
1968
|
|
|
@@ -2131,10 +2132,13 @@ static uint64_t otel_span_id_to_uint(VALUE otel_span_id) {
|
|
|
2131
2132
|
return result;
|
|
2132
2133
|
}
|
|
2133
2134
|
|
|
2134
|
-
static VALUE _native_sample_after_gvl_running(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread) {
|
|
2135
|
+
static VALUE _native_sample_after_gvl_running(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread, VALUE allow_exception) {
|
|
2135
2136
|
ENFORCE_THREAD(thread);
|
|
2137
|
+
ENFORCE_BOOLEAN(allow_exception);
|
|
2138
|
+
|
|
2136
2139
|
|
|
2137
|
-
|
|
2140
|
+
|
|
2141
|
+
if (allow_exception == Qfalse) debug_enter_unsafe_context();
|
|
2138
2142
|
|
|
2139
2143
|
VALUE result = thread_context_collector_sample_after_gvl_running(
|
|
2140
2144
|
collector_instance,
|
|
@@ -2142,7 +2146,7 @@ static uint64_t otel_span_id_to_uint(VALUE otel_span_id) {
|
|
|
2142
2146
|
monotonic_wall_time_now_ns(RAISE_ON_FAILURE)
|
|
2143
2147
|
);
|
|
2144
2148
|
|
|
2145
|
-
debug_leave_unsafe_context();
|
|
2149
|
+
if (allow_exception == Qfalse) debug_leave_unsafe_context();
|
|
2146
2150
|
|
|
2147
2151
|
return result;
|
|
2148
2152
|
}
|
|
@@ -2154,7 +2158,7 @@ static uint64_t otel_span_id_to_uint(VALUE otel_span_id) {
|
|
|
2154
2158
|
TypedData_Get_Struct(collector_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
|
|
2155
2159
|
|
|
2156
2160
|
per_thread_context *thread_context = get_context_for(thread, state);
|
|
2157
|
-
if (thread_context == NULL)
|
|
2161
|
+
if (thread_context == NULL) raise_error(rb_eArgError, "Unexpected: This method cannot be used unless the per-thread context for the thread already exists");
|
|
2158
2162
|
|
|
2159
2163
|
thread_context->cpu_time_at_previous_sample_ns += NUM2LONG(delta_ns);
|
|
2160
2164
|
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// NOTE: This file is a part of the profiling native extension even though the
|
|
2
|
+
// runtime stacks feature is consumed by the crashtracker. The profiling
|
|
3
|
+
// extension already carries all the Ruby VM private header access and build
|
|
4
|
+
// plumbing required to safely poke at internal structures. Sharing that setup
|
|
5
|
+
// avoids duplicating another native extension with the same (fragile) access
|
|
6
|
+
// patterns, and keeps the overall install/build surface area smaller.
|
|
7
|
+
//
|
|
8
|
+
// This also means that this functionality is depend on the profiling native extension
|
|
9
|
+
// being available and built
|
|
10
|
+
#include "extconf.h"
|
|
11
|
+
|
|
12
|
+
#if defined(__linux__)
|
|
13
|
+
|
|
14
|
+
#include <datadog/crashtracker.h>
|
|
15
|
+
#include "datadog_ruby_common.h"
|
|
16
|
+
#include "private_vm_api_access.h"
|
|
17
|
+
#include <sys/mman.h>
|
|
18
|
+
#include <unistd.h>
|
|
19
|
+
#include <errno.h>
|
|
20
|
+
#include <string.h>
|
|
21
|
+
|
|
22
|
+
static const rb_data_type_t *crashtracker_thread_data_type = NULL;
|
|
23
|
+
|
|
24
|
+
static void ruby_runtime_stack_callback(
|
|
25
|
+
void (*emit_frame)(const ddog_crasht_RuntimeStackFrame*)
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// Use a fixed, preallocated buffer for crash-time runtime stacks to avoid
|
|
29
|
+
// heap allocation in the signal/crash path.
|
|
30
|
+
#define RUNTIME_STACK_MAX_FRAMES 512
|
|
31
|
+
static frame_info runtime_stack_buffer[RUNTIME_STACK_MAX_FRAMES];
|
|
32
|
+
|
|
33
|
+
#if defined(__x86_64__)
|
|
34
|
+
# define SYS_MINCORE 0x1B
|
|
35
|
+
#elif defined(__aarch64__)
|
|
36
|
+
# define SYS_MINCORE 0xE8
|
|
37
|
+
#endif
|
|
38
|
+
|
|
39
|
+
long syscall(long number, ...);
|
|
40
|
+
|
|
41
|
+
// align down to power of two
|
|
42
|
+
static inline uintptr_t align_down(uintptr_t x, uintptr_t align) {
|
|
43
|
+
return x & ~(align - 1u);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static uintptr_t cached_page_size = 0;
|
|
47
|
+
|
|
48
|
+
// TODO: This function is not necessarily Ruby specific. This will be moved to
|
|
49
|
+
// `libdatadog` in the future as a shared utility function.
|
|
50
|
+
static inline bool is_pointer_readable(const void *ptr, size_t size) {
|
|
51
|
+
if (!ptr || size == 0) return false;
|
|
52
|
+
|
|
53
|
+
uintptr_t page_size = cached_page_size;
|
|
54
|
+
|
|
55
|
+
const uintptr_t start = align_down((uintptr_t)ptr, page_size);
|
|
56
|
+
const uintptr_t end = ((uintptr_t)ptr + size - 1u);
|
|
57
|
+
const uintptr_t last = align_down(end, page_size);
|
|
58
|
+
|
|
59
|
+
// Number of pages spanned
|
|
60
|
+
size_t pages = 1u + (last != start);
|
|
61
|
+
if (pages > 2u) pages = 2u;
|
|
62
|
+
|
|
63
|
+
unsigned char vec[2];
|
|
64
|
+
|
|
65
|
+
int retries = 5;
|
|
66
|
+
for (;;) {
|
|
67
|
+
size_t len = pages * (size_t)page_size;
|
|
68
|
+
long rc = syscall(SYS_MINCORE, (void*)start, len, vec);
|
|
69
|
+
|
|
70
|
+
if (rc == 0) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
int e = errno;
|
|
75
|
+
if (e == ENOMEM || e == EFAULT) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (e == EAGAIN && retries-- > 0) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Unknown errno, we assume mapped to avoid cascading faults in crash path
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
static inline ddog_CharSlice char_slice_from_cstr(const char *cstr) {
|
|
89
|
+
if (cstr == NULL) {
|
|
90
|
+
return (ddog_CharSlice){.ptr = NULL, .len = 0};
|
|
91
|
+
}
|
|
92
|
+
return (ddog_CharSlice){.ptr = cstr, .len = strlen(cstr)};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
static ddog_CharSlice safe_string_value(VALUE str) {
|
|
96
|
+
if (str == Qnil) return DDOG_CHARSLICE_C("<nil>");
|
|
97
|
+
|
|
98
|
+
// Validate object and payload readability in one check
|
|
99
|
+
if (!is_pointer_readable((const void *)str, sizeof(struct RString))) return DDOG_CHARSLICE_C("<corrupted>");
|
|
100
|
+
if (!RB_TYPE_P(str, T_STRING)) return DDOG_CHARSLICE_C("<not_string>");
|
|
101
|
+
|
|
102
|
+
long len = RSTRING_LEN(str);
|
|
103
|
+
if (len < 0 || len > 1024) return DDOG_CHARSLICE_C("<length_over_limit>");
|
|
104
|
+
|
|
105
|
+
const char *ptr = RSTRING_PTR(str);
|
|
106
|
+
if (!ptr) return DDOG_CHARSLICE_C("<null>");
|
|
107
|
+
|
|
108
|
+
if (!is_pointer_readable(ptr, len > 0 ? len : 1)) return DDOG_CHARSLICE_C("<unreadable>");
|
|
109
|
+
|
|
110
|
+
return (ddog_CharSlice){.ptr = ptr, .len = (size_t)len};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
static void emit_placeholder_frame(
|
|
114
|
+
void (*emit_frame)(const ddog_crasht_RuntimeStackFrame*),
|
|
115
|
+
const char *label
|
|
116
|
+
) {
|
|
117
|
+
ddog_crasht_RuntimeStackFrame frame = {
|
|
118
|
+
.type_name = DDOG_CHARSLICE_C(""),
|
|
119
|
+
.function = char_slice_from_cstr(label),
|
|
120
|
+
.file = DDOG_CHARSLICE_C("<unknown>"),
|
|
121
|
+
.line = 0,
|
|
122
|
+
.column = 0
|
|
123
|
+
};
|
|
124
|
+
emit_frame(&frame);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Collect the crashing thread's frames via ddtrace_rb_profile_frames into a static buffer, then emit
|
|
128
|
+
// them newest-first. If corruption is detected, emit placeholder frames so the crash report still
|
|
129
|
+
// completes. We lean on the Ruby VM helpers we already use for profiling and rely on crashtracker's
|
|
130
|
+
// safety nets so a failure here should not impact customers.
|
|
131
|
+
static void ruby_runtime_stack_callback(
|
|
132
|
+
void (*emit_frame)(const ddog_crasht_RuntimeStackFrame*)
|
|
133
|
+
) {
|
|
134
|
+
// Grab the Ruby thread we crashed on; crashtracker only runs once.
|
|
135
|
+
VALUE current_thread = rb_thread_current();
|
|
136
|
+
if (current_thread == Qnil) return;
|
|
137
|
+
|
|
138
|
+
if (crashtracker_thread_data_type == NULL) return;
|
|
139
|
+
|
|
140
|
+
if (!rb_typeddata_is_kind_of(current_thread, crashtracker_thread_data_type)) {
|
|
141
|
+
emit_placeholder_frame(emit_frame, "<runtime stack not found>");
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Use the profiling helper to gather frames into our static buffer.
|
|
146
|
+
int frame_count = ddtrace_rb_profile_frames(
|
|
147
|
+
current_thread,
|
|
148
|
+
0,
|
|
149
|
+
RUNTIME_STACK_MAX_FRAMES,
|
|
150
|
+
runtime_stack_buffer
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
if (frame_count <= 0) {
|
|
154
|
+
emit_placeholder_frame(emit_frame, "<runtime stack not found>");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
for (int i = frame_count - 1; i >= 0; i--) {
|
|
159
|
+
frame_info *info = &runtime_stack_buffer[i];
|
|
160
|
+
|
|
161
|
+
if (info->is_ruby_frame) {
|
|
162
|
+
const void *iseq = (const void *)info->as.ruby_frame.iseq;
|
|
163
|
+
ddog_CharSlice function_slice = DDOG_CHARSLICE_C("<unknown>");
|
|
164
|
+
ddog_CharSlice file_slice = DDOG_CHARSLICE_C("<unknown>");
|
|
165
|
+
|
|
166
|
+
if (iseq && is_pointer_readable(iseq, sizeof_rb_iseq_t())) {
|
|
167
|
+
function_slice = safe_string_value(ddtrace_iseq_base_label(iseq));
|
|
168
|
+
file_slice = safe_string_value(ddtrace_iseq_path(iseq));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
ddog_crasht_RuntimeStackFrame frame = {
|
|
172
|
+
.type_name = DDOG_CHARSLICE_C(""),
|
|
173
|
+
.function = function_slice,
|
|
174
|
+
.file = file_slice,
|
|
175
|
+
.line = info->as.ruby_frame.line,
|
|
176
|
+
.column = 0
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
emit_frame(&frame);
|
|
180
|
+
} else {
|
|
181
|
+
ddog_CharSlice function_slice = DDOG_CHARSLICE_C("<C method>");
|
|
182
|
+
ddog_CharSlice file_slice = DDOG_CHARSLICE_C("<C extension>");
|
|
183
|
+
|
|
184
|
+
if (info->as.native_frame.method_id) {
|
|
185
|
+
const char *method_name = rb_id2name(info->as.native_frame.method_id);
|
|
186
|
+
if (is_pointer_readable(method_name, 256)) {
|
|
187
|
+
size_t method_name_len = strnlen(method_name, 256);
|
|
188
|
+
if (method_name_len > 0 && method_name_len < 256) {
|
|
189
|
+
function_slice = char_slice_from_cstr(method_name);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
ddog_crasht_RuntimeStackFrame frame = {
|
|
195
|
+
.type_name = DDOG_CHARSLICE_C(""),
|
|
196
|
+
.function = function_slice,
|
|
197
|
+
.file = file_slice,
|
|
198
|
+
.line = 0,
|
|
199
|
+
.column = 0
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
emit_frame(&frame);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (frame_count == RUNTIME_STACK_MAX_FRAMES) {
|
|
207
|
+
emit_placeholder_frame(emit_frame, "<truncated frames>");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
void crashtracking_runtime_stacks_init(void) {
|
|
212
|
+
cached_page_size = (uintptr_t)sysconf(_SC_PAGESIZE);
|
|
213
|
+
if (cached_page_size == 0 || (cached_page_size & (cached_page_size - 1u))) {
|
|
214
|
+
cached_page_size = 4096;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (crashtracker_thread_data_type == NULL) {
|
|
218
|
+
VALUE current_thread = rb_thread_current();
|
|
219
|
+
if (current_thread == Qnil) {
|
|
220
|
+
raise_error(rb_eRuntimeError, "crashtracking_runtime_stacks_init: rb_thread_current returned Qnil");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const rb_data_type_t *thread_data_type = RTYPEDDATA_TYPE(current_thread);
|
|
224
|
+
if (!thread_data_type) {
|
|
225
|
+
raise_error(rb_eRuntimeError, "crashtracking_runtime_stacks_init: RTYPEDDATA_TYPE returned NULL");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
crashtracker_thread_data_type = thread_data_type;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Register immediately so Ruby doesn't need to manage this explicitly.
|
|
232
|
+
ddog_crasht_register_runtime_frame_callback(ruby_runtime_stack_callback);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
#else
|
|
236
|
+
// Keep init symbol to satisfy linkage on non linux platforms, but do nothing
|
|
237
|
+
void crashtracking_runtime_stacks_init(void) {}
|
|
238
|
+
#endif
|
|
239
|
+
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
#include "datadog_ruby_common.h"
|
|
2
|
+
#include <stdarg.h>
|
|
2
3
|
|
|
3
4
|
// IMPORTANT: Currently this file is copy-pasted between extensions. Make sure to update all versions when doing any change!
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
static ID telemetry_message_id;
|
|
7
|
+
|
|
8
|
+
void raise_unexpected_type(VALUE value, const char *value_name, const char *type_name, const char *file, int line, const char *function_name) {
|
|
6
9
|
rb_exc_raise(
|
|
7
10
|
rb_exc_new_str(
|
|
8
11
|
rb_eTypeError,
|
|
@@ -18,6 +21,26 @@ void raise_unexpected_type(VALUE value, const char *value_name, const char *type
|
|
|
18
21
|
);
|
|
19
22
|
}
|
|
20
23
|
|
|
24
|
+
// Raises an exception with separate telemetry-safe and detailed messages.
|
|
25
|
+
// NOTE: Raising an exception always invokes Ruby code so it requires the GVL and is not compatible with "debug_enter_unsafe_context".
|
|
26
|
+
// @see debug_enter_unsafe_context
|
|
27
|
+
void private_raise_exception(VALUE exception, const char *static_message) {
|
|
28
|
+
rb_ivar_set(exception, telemetry_message_id, rb_str_new_cstr(static_message));
|
|
29
|
+
rb_exc_raise(exception);
|
|
30
|
+
}
|
|
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
|
+
// Use `raise_error` the macro instead, as it provides additional argument checks.
|
|
39
|
+
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);
|
|
42
|
+
}
|
|
43
|
+
|
|
21
44
|
VALUE datadog_gem_version(void) {
|
|
22
45
|
VALUE ddtrace_module = rb_const_get(rb_cObject, rb_intern("Datadog"));
|
|
23
46
|
ENFORCE_TYPE(ddtrace_module, T_MODULE);
|
|
@@ -78,3 +101,27 @@ ddog_Vec_Tag convert_tags(VALUE tags_as_array) {
|
|
|
78
101
|
|
|
79
102
|
return tags;
|
|
80
103
|
}
|
|
104
|
+
|
|
105
|
+
size_t read_ddogerr_string_and_drop(ddog_Error *error, char *string, size_t capacity) {
|
|
106
|
+
if (capacity == 0 || string == NULL) {
|
|
107
|
+
// short-circuit, we can't write anything
|
|
108
|
+
ddog_Error_drop(error);
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
ddog_CharSlice error_msg_slice = ddog_Error_message(error);
|
|
113
|
+
size_t error_msg_size = error_msg_slice.len;
|
|
114
|
+
// Account for extra null char for proper cstring
|
|
115
|
+
if (error_msg_size >= capacity) {
|
|
116
|
+
// Error message too big, lets truncate it to capacity - 1 to allow for extra null at end
|
|
117
|
+
error_msg_size = capacity - 1;
|
|
118
|
+
}
|
|
119
|
+
strncpy(string, error_msg_slice.ptr, error_msg_size);
|
|
120
|
+
string[error_msg_size] = '\0';
|
|
121
|
+
ddog_Error_drop(error);
|
|
122
|
+
return error_msg_size;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
void datadog_ruby_common_init(void) {
|
|
126
|
+
telemetry_message_id = rb_intern("@telemetry_message");
|
|
127
|
+
}
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
#include <ruby.h>
|
|
6
6
|
#include <datadog/common.h>
|
|
7
7
|
|
|
8
|
+
// Must be called once during initialization
|
|
9
|
+
void datadog_ruby_common_init(void);
|
|
10
|
+
|
|
8
11
|
// Used to mark symbols to be exported to the outside of the extension.
|
|
9
12
|
// Consider very carefully before tagging a function with this.
|
|
10
13
|
#define DDTRACE_EXPORT __attribute__ ((visibility ("default")))
|
|
@@ -32,6 +35,39 @@
|
|
|
32
35
|
|
|
33
36
|
NORETURN(void raise_unexpected_type(VALUE value, const char *value_name, const char *type_name, const char *file, int line, const char* function_name));
|
|
34
37
|
|
|
38
|
+
// Raises an exception of the specified class with the formatted string as its message.
|
|
39
|
+
// This macro ensures that the literal string is sent for telemetry, while the formatted
|
|
40
|
+
// message is the default `Exception#message`.
|
|
41
|
+
// *Ruby exceptions not raised through this function will not be reported via telemetry.*
|
|
42
|
+
#define raise_error(exception_class, fmt, ...) \
|
|
43
|
+
private_raise_error(exception_class, "" fmt, ##__VA_ARGS__)
|
|
44
|
+
|
|
45
|
+
NORETURN(
|
|
46
|
+
void private_raise_error(VALUE exception_class, const char *fmt, ...)
|
|
47
|
+
__attribute__ ((format (printf, 2, 3)));
|
|
48
|
+
);
|
|
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
|
+
// Raises an exception with separate telemetry-safe and detailed messages.
|
|
56
|
+
// NOTE: Raising an exception always invokes Ruby code so it requires the GVL and is not compatible with "debug_enter_unsafe_context".
|
|
57
|
+
// @see debug_enter_unsafe_context
|
|
58
|
+
NORETURN(
|
|
59
|
+
void private_raise_exception(VALUE exception, const char *static_message)
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
#define MAX_RAISE_MESSAGE_SIZE 256
|
|
63
|
+
|
|
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
|
+
|
|
35
71
|
// Helper to retrieve Datadog::VERSION::STRING
|
|
36
72
|
VALUE datadog_gem_version(void);
|
|
37
73
|
|
|
@@ -61,3 +97,8 @@ static inline VALUE get_error_details_and_drop(ddog_Error *error) {
|
|
|
61
97
|
ddog_Error_drop(error);
|
|
62
98
|
return result;
|
|
63
99
|
}
|
|
100
|
+
|
|
101
|
+
// Utility function to be able to extract an error cstring from a ddog_Error.
|
|
102
|
+
// Returns the amount of characters written to string (which are necessarily
|
|
103
|
+
// bounded by capacity - 1 since the string will be null-terminated).
|
|
104
|
+
size_t read_ddogerr_string_and_drop(ddog_Error *error, char *string, size_t capacity);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#include "encoded_profile.h"
|
|
2
2
|
#include "datadog_ruby_common.h"
|
|
3
3
|
#include "libdatadog_helpers.h"
|
|
4
|
+
#include "ruby_helpers.h"
|
|
4
5
|
|
|
5
6
|
// This class exists to wrap a ddog_prof_EncodedProfile into a Ruby object
|
|
6
7
|
// This file implements the native bits of the Datadog::Profiling::EncodedProfile class
|
|
@@ -41,7 +42,7 @@ VALUE from_ddog_prof_EncodedProfile(ddog_prof_EncodedProfile profile) {
|
|
|
41
42
|
static ddog_ByteSlice get_bytes(ddog_prof_EncodedProfile *state) {
|
|
42
43
|
ddog_prof_Result_ByteSlice raw_bytes = ddog_prof_EncodedProfile_bytes(state);
|
|
43
44
|
if (raw_bytes.tag == DDOG_PROF_RESULT_BYTE_SLICE_ERR_BYTE_SLICE) {
|
|
44
|
-
|
|
45
|
+
raise_error(rb_eRuntimeError, "Failed to get bytes from profile: %"PRIsVALUE, get_error_details_and_drop(&raw_bytes.err));
|
|
45
46
|
}
|
|
46
47
|
return raw_bytes.ok;
|
|
47
48
|
}
|
|
@@ -138,8 +138,11 @@ if have_header("dlfcn.h")
|
|
|
138
138
|
have_func("dladdr")
|
|
139
139
|
end
|
|
140
140
|
|
|
141
|
+
# On older Rubies, there was no primitive mutex and condition variable implemented in `thread_sync.rb` (internal)
|
|
142
|
+
$defs << "-DNO_PRIMITIVE_MUTEX_AND_CONDITION_VARIABLE" if RUBY_VERSION < "4"
|
|
143
|
+
|
|
141
144
|
# On Ruby 4, we can't ask the object_id from IMEMOs (https://github.com/ruby/ruby/pull/13347)
|
|
142
|
-
$defs << "-DNO_IMEMO_OBJECT_ID" unless RUBY_VERSION < "4
|
|
145
|
+
$defs << "-DNO_IMEMO_OBJECT_ID" unless RUBY_VERSION < "4"
|
|
143
146
|
|
|
144
147
|
# This symbol is exclusively visible on certain Ruby versions: 2.6 to 3.2, as well as 3.4 (but not 4.0+)
|
|
145
148
|
# It's only used to get extra information about an object when a failure happens, so it's a "very nice to have" but not
|