diff --git a/binding/c/include/datadog/c/tracer.h b/binding/c/include/datadog/c/tracer.h index 655d334d..4c52f362 100644 --- a/binding/c/include/datadog/c/tracer.h +++ b/binding/c/include/datadog/c/tracer.h @@ -1,6 +1,7 @@ #pragma once #include +#include #if defined(_WIN32) #if defined(DD_TRACE_C_BUILDING) @@ -43,6 +44,9 @@ typedef enum { DD_OPT_INTEGRATION_VERSION = 5 } dd_tracer_option; +// Sentinel for start_time_ns meaning "use the current time." +static const int64_t DD_TRACE_CURRENT_TIME = -1; + // Options for creating a span. Unset fields default to NULL. typedef struct { const char* name; @@ -51,6 +55,7 @@ typedef struct { const char* service_type; const char* environment; const char* version; + int64_t start_time_ns; // Unix-epoch nanoseconds or DD_TRACE_CURRENT_TIME. } dd_span_options_t; // Error codes returned by the C binding. @@ -179,6 +184,18 @@ DD_TRACE_C_API dd_span_t* dd_span_create_child(dd_span_t* span_handle, // @param span_handle Span handle DD_TRACE_C_API void dd_span_finish(dd_span_t* span_handle); +// Set the end time of a span from an explicit wall-clock timestamp. The +// span itself is finished when dd_span_free destroys it. To use the +// current time, call dd_span_finish instead. If end_time_ns is earlier +// than the span's start time, the resulting duration is clamped to zero. +// No-op if span_handle is NULL. +// +// @param span_handle Span handle +// @param end_time_ns Wall-clock end time (Unix-epoch nanoseconds). +// DD_TRACE_CURRENT_TIME does not apply here. +DD_TRACE_C_API void dd_span_set_end_time(dd_span_t* span_handle, + int64_t end_time_ns); + // Get the trace ID as a zero-padded hex string. // // @param span_handle Span handle diff --git a/binding/c/src/tracer.cpp b/binding/c/src/tracer.cpp index 2a81a40f..812f7d54 100644 --- a/binding/c/src/tracer.cpp +++ b/binding/c/src/tracer.cpp @@ -44,6 +44,24 @@ class ContextWriter : public dd::DictWriter { } }; +std::chrono::system_clock::time_point wall_ns_to_system_time(int64_t wall_ns) { + return std::chrono::system_clock::time_point( + std::chrono::round( + std::chrono::nanoseconds(wall_ns))); +} + +// Create a TimePoint from a wall-clock timestamp in nanoseconds. The tick is +// derived from the wall clock using the current offset between the system and +// the steady clocks. This keeps duration computed from ticks accurate when +// the start time is supplied via FFI. +dd::TimePoint wall_ns_to_timepoint(int64_t wall_ns) { + const auto wall = wall_ns_to_system_time(wall_ns); + const auto now_wall = std::chrono::system_clock::now(); + const auto now_tick = std::chrono::steady_clock::now(); + return {wall, + now_tick - std::chrono::duration_cast(now_wall - wall)}; +} + dd::SpanConfig make_span_config(dd_span_options_t options) { dd::SpanConfig span_config; if (options.name != nullptr) { @@ -64,6 +82,9 @@ dd::SpanConfig make_span_config(dd_span_options_t options) { if (options.version != nullptr) { span_config.version = options.version; } + if (options.start_time_ns != DD_TRACE_CURRENT_TIME) { + span_config.start = wall_ns_to_timepoint(options.start_time_ns); + } return span_config; } @@ -257,6 +278,23 @@ void dd_span_finish(dd_span_t *span_handle) { ->set_end_time(std::chrono::steady_clock::now()); } +void dd_span_set_end_time(dd_span_t *span_handle, int64_t end_time_ns) { + if (span_handle == nullptr) { + return; + } + auto *span = reinterpret_cast(span_handle); + // Anchor end.tick to start.tick + wall delta so NTP adjustments during the + // span's lifetime don't skew duration. Clamp to start.tick on negative + // deltas — serialization casts duration to uint64_t. + const auto start = span->start_time(); + const auto end_wall = wall_ns_to_system_time(end_time_ns); + const auto duration = + end_wall > start.wall + ? std::chrono::duration_cast(end_wall - start.wall) + : dd::Duration::zero(); + span->set_end_time(start.tick + duration); +} + int dd_span_get_trace_id(dd_span_t *span_handle, char *buffer, size_t buffer_size) { if (span_handle == nullptr || buffer == nullptr || buffer_size == 0) { diff --git a/binding/c/test/test_c_binding.cpp b/binding/c/test/test_c_binding.cpp index 7b93516b..db102bb8 100644 --- a/binding/c/test/test_c_binding.cpp +++ b/binding/c/test/test_c_binding.cpp @@ -91,7 +91,11 @@ TEST_CASE("tracer new propagates error", "[c_binding]") { TEST_CASE("span create, tag, finish, free", "[c_binding]") { auto ctx = make_tracer(); - auto *span = dd_tracer_create_span(ctx.tracer, {.name = "test.op"}); + const int64_t start_ns = 1'700'000'000'000'000'000LL; + const int64_t end_ns = start_ns + 42'000'000LL; + + auto *span = dd_tracer_create_span( + ctx.tracer, {.name = "test.op", .start_time_ns = start_ns}); REQUIRE(span != nullptr); dd_span_set_tag(span, "http.method", "GET"); @@ -100,7 +104,7 @@ TEST_CASE("span create, tag, finish, free", "[c_binding]") { dd_span_set_error(span, 1); dd_span_set_error_message(span, "something broke"); - dd_span_finish(span); + dd_span_set_end_time(span, end_ns); dd_span_free(span); const auto &sd = ctx.collector->first_span(); @@ -109,6 +113,26 @@ TEST_CASE("span create, tag, finish, free", "[c_binding]") { CHECK(sd.service == "user-service"); CHECK(sd.error == true); CHECK(sd.tags.at("error.message") == "something broke"); + + CHECK(std::chrono::duration_cast( + sd.start.wall.time_since_epoch()) + .count() == start_ns); + CHECK(std::chrono::abs(sd.duration - + std::chrono::nanoseconds(end_ns - start_ns)) < + std::chrono::milliseconds(1)); +} + +TEST_CASE("set_end_time before start clamps to zero duration", "[c_binding]") { + auto ctx = make_tracer(); + + const int64_t past_ns = 1'700'000'000'000'000'000LL; + auto *span = dd_tracer_create_span( + ctx.tracer, {.name = "reversed", .start_time_ns = past_ns}); + REQUIRE(span != nullptr); + dd_span_set_end_time(span, past_ns - 1'000'000LL); + dd_span_free(span); + + CHECK(ctx.collector->first_span().duration >= std::chrono::nanoseconds(0)); } TEST_CASE("create span with resource", "[c_binding]") { @@ -253,6 +277,7 @@ TEST_CASE("null arguments do not crash", "[c_binding]") { dd_span_set_error_message(nullptr, "msg"); dd_span_inject(nullptr, test_header_writer); dd_span_finish(nullptr); + dd_span_set_end_time(nullptr, 0); dd_span_set_resource(nullptr, "res"); dd_span_set_service(nullptr, "svc"); }