Skip to content

Commit 5dfaf40

Browse files
adeshkumar1claude
andauthored
feat(c-binding): accept explicit span timestamps (#309)
* feat(c-binding): accept explicit span timestamps Add `start_time_ns` to `dd_span_options_t` and introduce `dd_span_finish_with_time(end_time_ns)`. Both accept wall-clock nanoseconds since the UNIX epoch; 0 preserves today's "use current time" behaviour so existing callers are unaffected. The conversion from wall-ns to the tracer's internal `TimePoint` derives the steady_clock tick from the current system/steady offset, so duration math stays correct regardless of which endpoint is explicit and which is implicit. Motivated by FFI callers (e.g. the Kong plugin) that capture timestamps in their host runtime before crossing the FFI boundary, where using "now" inside the binding would lose the pre-FFI portion of the span and bake in call-overhead skew. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * review: rename finish_with_time → set_end_time, use -1 sentinel Per review feedback from @dmehala: 1. Rename dd_span_finish_with_time → dd_span_set_end_time to match the C++ Span::set_end_time and the existing dd_span_set_* naming family. 2. Expose DD_TRACE_CURRENT_TIME (-1) as the "use current time" sentinel for start_time_ns, so 0 (1970-01-01) stays a valid timestamp. 3. Drop the sentinel branch in dd_span_set_end_time; callers wanting the current end time use dd_span_finish, and this also removes the implicit-path clamp that was only defending against a future-start caller bug. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6d02fc0 commit 5dfaf40

3 files changed

Lines changed: 82 additions & 2 deletions

File tree

binding/c/include/datadog/c/tracer.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#pragma once
22

33
#include <stddef.h>
4+
#include <stdint.h>
45

56
#if defined(_WIN32)
67
#if defined(DD_TRACE_C_BUILDING)
@@ -43,6 +44,9 @@ typedef enum {
4344
DD_OPT_INTEGRATION_VERSION = 5
4445
} dd_tracer_option;
4546

47+
// Sentinel for start_time_ns meaning "use the current time."
48+
static const int64_t DD_TRACE_CURRENT_TIME = -1;
49+
4650
// Options for creating a span. Unset fields default to NULL.
4751
typedef struct {
4852
const char* name;
@@ -51,6 +55,7 @@ typedef struct {
5155
const char* service_type;
5256
const char* environment;
5357
const char* version;
58+
int64_t start_time_ns; // Unix-epoch nanoseconds or DD_TRACE_CURRENT_TIME.
5459
} dd_span_options_t;
5560

5661
// 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,
179184
// @param span_handle Span handle
180185
DD_TRACE_C_API void dd_span_finish(dd_span_t* span_handle);
181186

187+
// Set the end time of a span from an explicit wall-clock timestamp. The
188+
// span itself is finished when dd_span_free destroys it. To use the
189+
// current time, call dd_span_finish instead. If end_time_ns is earlier
190+
// than the span's start time, the resulting duration is clamped to zero.
191+
// No-op if span_handle is NULL.
192+
//
193+
// @param span_handle Span handle
194+
// @param end_time_ns Wall-clock end time (Unix-epoch nanoseconds).
195+
// DD_TRACE_CURRENT_TIME does not apply here.
196+
DD_TRACE_C_API void dd_span_set_end_time(dd_span_t* span_handle,
197+
int64_t end_time_ns);
198+
182199
// Get the trace ID as a zero-padded hex string.
183200
//
184201
// @param span_handle Span handle

binding/c/src/tracer.cpp

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,24 @@ class ContextWriter : public dd::DictWriter {
4444
}
4545
};
4646

47+
std::chrono::system_clock::time_point wall_ns_to_system_time(int64_t wall_ns) {
48+
return std::chrono::system_clock::time_point(
49+
std::chrono::round<std::chrono::system_clock::duration>(
50+
std::chrono::nanoseconds(wall_ns)));
51+
}
52+
53+
// Create a TimePoint from a wall-clock timestamp in nanoseconds. The tick is
54+
// derived from the wall clock using the current offset between the system and
55+
// the steady clocks. This keeps duration computed from ticks accurate when
56+
// the start time is supplied via FFI.
57+
dd::TimePoint wall_ns_to_timepoint(int64_t wall_ns) {
58+
const auto wall = wall_ns_to_system_time(wall_ns);
59+
const auto now_wall = std::chrono::system_clock::now();
60+
const auto now_tick = std::chrono::steady_clock::now();
61+
return {wall,
62+
now_tick - std::chrono::duration_cast<dd::Duration>(now_wall - wall)};
63+
}
64+
4765
dd::SpanConfig make_span_config(dd_span_options_t options) {
4866
dd::SpanConfig span_config;
4967
if (options.name != nullptr) {
@@ -64,6 +82,9 @@ dd::SpanConfig make_span_config(dd_span_options_t options) {
6482
if (options.version != nullptr) {
6583
span_config.version = options.version;
6684
}
85+
if (options.start_time_ns != DD_TRACE_CURRENT_TIME) {
86+
span_config.start = wall_ns_to_timepoint(options.start_time_ns);
87+
}
6788
return span_config;
6889
}
6990

@@ -257,6 +278,23 @@ void dd_span_finish(dd_span_t *span_handle) {
257278
->set_end_time(std::chrono::steady_clock::now());
258279
}
259280

281+
void dd_span_set_end_time(dd_span_t *span_handle, int64_t end_time_ns) {
282+
if (span_handle == nullptr) {
283+
return;
284+
}
285+
auto *span = reinterpret_cast<dd::Span *>(span_handle);
286+
// Anchor end.tick to start.tick + wall delta so NTP adjustments during the
287+
// span's lifetime don't skew duration. Clamp to start.tick on negative
288+
// deltas — serialization casts duration to uint64_t.
289+
const auto start = span->start_time();
290+
const auto end_wall = wall_ns_to_system_time(end_time_ns);
291+
const auto duration =
292+
end_wall > start.wall
293+
? std::chrono::duration_cast<dd::Duration>(end_wall - start.wall)
294+
: dd::Duration::zero();
295+
span->set_end_time(start.tick + duration);
296+
}
297+
260298
int dd_span_get_trace_id(dd_span_t *span_handle, char *buffer,
261299
size_t buffer_size) {
262300
if (span_handle == nullptr || buffer == nullptr || buffer_size == 0) {

binding/c/test/test_c_binding.cpp

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,11 @@ TEST_CASE("tracer new propagates error", "[c_binding]") {
9191
TEST_CASE("span create, tag, finish, free", "[c_binding]") {
9292
auto ctx = make_tracer();
9393

94-
auto *span = dd_tracer_create_span(ctx.tracer, {.name = "test.op"});
94+
const int64_t start_ns = 1'700'000'000'000'000'000LL;
95+
const int64_t end_ns = start_ns + 42'000'000LL;
96+
97+
auto *span = dd_tracer_create_span(
98+
ctx.tracer, {.name = "test.op", .start_time_ns = start_ns});
9599
REQUIRE(span != nullptr);
96100

97101
dd_span_set_tag(span, "http.method", "GET");
@@ -100,7 +104,7 @@ TEST_CASE("span create, tag, finish, free", "[c_binding]") {
100104
dd_span_set_error(span, 1);
101105
dd_span_set_error_message(span, "something broke");
102106

103-
dd_span_finish(span);
107+
dd_span_set_end_time(span, end_ns);
104108
dd_span_free(span);
105109

106110
const auto &sd = ctx.collector->first_span();
@@ -109,6 +113,26 @@ TEST_CASE("span create, tag, finish, free", "[c_binding]") {
109113
CHECK(sd.service == "user-service");
110114
CHECK(sd.error == true);
111115
CHECK(sd.tags.at("error.message") == "something broke");
116+
117+
CHECK(std::chrono::duration_cast<std::chrono::nanoseconds>(
118+
sd.start.wall.time_since_epoch())
119+
.count() == start_ns);
120+
CHECK(std::chrono::abs(sd.duration -
121+
std::chrono::nanoseconds(end_ns - start_ns)) <
122+
std::chrono::milliseconds(1));
123+
}
124+
125+
TEST_CASE("set_end_time before start clamps to zero duration", "[c_binding]") {
126+
auto ctx = make_tracer();
127+
128+
const int64_t past_ns = 1'700'000'000'000'000'000LL;
129+
auto *span = dd_tracer_create_span(
130+
ctx.tracer, {.name = "reversed", .start_time_ns = past_ns});
131+
REQUIRE(span != nullptr);
132+
dd_span_set_end_time(span, past_ns - 1'000'000LL);
133+
dd_span_free(span);
134+
135+
CHECK(ctx.collector->first_span().duration >= std::chrono::nanoseconds(0));
112136
}
113137

114138
TEST_CASE("create span with resource", "[c_binding]") {
@@ -253,6 +277,7 @@ TEST_CASE("null arguments do not crash", "[c_binding]") {
253277
dd_span_set_error_message(nullptr, "msg");
254278
dd_span_inject(nullptr, test_header_writer);
255279
dd_span_finish(nullptr);
280+
dd_span_set_end_time(nullptr, 0);
256281
dd_span_set_resource(nullptr, "res");
257282
dd_span_set_service(nullptr, "svc");
258283
}

0 commit comments

Comments
 (0)