Skip to content

Commit 3fe3518

Browse files
adeshkumar1claude
andcommitted
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>
1 parent 1f76c5c commit 3fe3518

3 files changed

Lines changed: 90 additions & 5 deletions

File tree

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

Lines changed: 9 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)
@@ -51,6 +52,7 @@ typedef struct {
5152
const char* service_type;
5253
const char* environment;
5354
const char* version;
55+
int64_t start_time_ns; // UNIX-epoch nanoseconds; 0 = use current time
5456
} dd_span_options_t;
5557

5658
// Error codes returned by the C binding.
@@ -179,6 +181,13 @@ DD_TRACE_C_API dd_span_t* dd_span_create_child(dd_span_t* span_handle,
179181
// @param span_handle Span handle
180182
DD_TRACE_C_API void dd_span_finish(dd_span_t* span_handle);
181183

184+
// Finish a span using an explicit end time. No-op if span_handle is NULL.
185+
//
186+
// @param span_handle Span handle
187+
// @param end_time_ns Wall-clock end time (UNIX-epoch ns); 0 = current time
188+
DD_TRACE_C_API void dd_span_finish_with_time(dd_span_t* span_handle,
189+
int64_t end_time_ns);
190+
182191
// Get the trace ID as a zero-padded hex string.
183192
//
184193
// @param span_handle Span handle

binding/c/src/tracer.cpp

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

47+
// tick is derived from the current system/steady offset so that duration
48+
// (end.tick - start.tick) stays correct across FFI-supplied timestamps.
49+
dd::TimePoint wall_ns_to_timepoint(int64_t wall_ns) {
50+
const auto wall = std::chrono::system_clock::time_point(
51+
std::chrono::round<std::chrono::system_clock::duration>(
52+
std::chrono::nanoseconds(wall_ns)));
53+
const auto now_wall = std::chrono::system_clock::now();
54+
const auto now_tick = std::chrono::steady_clock::now();
55+
return {wall,
56+
now_tick - std::chrono::duration_cast<dd::Duration>(now_wall - wall)};
57+
}
58+
4759
dd::SpanConfig make_span_config(dd_span_options_t options) {
4860
dd::SpanConfig span_config;
4961
if (options.name != nullptr) {
@@ -64,6 +76,9 @@ dd::SpanConfig make_span_config(dd_span_options_t options) {
6476
if (options.version != nullptr) {
6577
span_config.version = options.version;
6678
}
79+
if (options.start_time_ns != 0) {
80+
span_config.start = wall_ns_to_timepoint(options.start_time_ns);
81+
}
6782
return span_config;
6883
}
6984

@@ -249,12 +264,34 @@ dd_span_t *dd_span_create_child(dd_span_t *span_handle,
249264
}
250265
}
251266

252-
void dd_span_finish(dd_span_t *span_handle) {
267+
void dd_span_finish_with_time(dd_span_t *span_handle, int64_t end_time_ns) {
253268
if (span_handle == nullptr) {
254269
return;
255270
}
256-
reinterpret_cast<dd::Span *>(span_handle)
257-
->set_end_time(std::chrono::steady_clock::now());
271+
auto *span = reinterpret_cast<dd::Span *>(span_handle);
272+
const auto start = span->start_time();
273+
// Explicit path anchors to start.wall so NTP adjustments during the
274+
// span's lifetime don't skew duration; implicit path uses steady now.
275+
// Both clamp to start.tick so a negative duration never ships
276+
// (serialization casts duration to uint64_t).
277+
std::chrono::steady_clock::time_point end_tick;
278+
if (end_time_ns == 0) {
279+
end_tick = std::chrono::steady_clock::now();
280+
} else {
281+
const auto end_wall = std::chrono::system_clock::time_point(
282+
std::chrono::round<std::chrono::system_clock::duration>(
283+
std::chrono::nanoseconds(end_time_ns)));
284+
end_tick =
285+
start.tick +
286+
(end_wall > start.wall
287+
? std::chrono::duration_cast<dd::Duration>(end_wall - start.wall)
288+
: dd::Duration::zero());
289+
}
290+
span->set_end_time(end_tick > start.tick ? end_tick : start.tick);
291+
}
292+
293+
void dd_span_finish(dd_span_t *span_handle) {
294+
dd_span_finish_with_time(span_handle, 0);
258295
}
259296

260297
int dd_span_get_trace_id(dd_span_t *span_handle, char *buffer,

binding/c/test/test_c_binding.cpp

Lines changed: 41 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_finish_with_time(span, end_ns);
104108
dd_span_free(span);
105109

106110
const auto &sd = ctx.collector->first_span();
@@ -109,6 +113,40 @@ 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("finish clamps negative durations to zero", "[c_binding]") {
126+
auto ctx = make_tracer();
127+
128+
// Explicit end before explicit start.
129+
const int64_t past_ns = 1'700'000'000'000'000'000LL;
130+
auto *s1 = dd_tracer_create_span(
131+
ctx.tracer, {.name = "explicit", .start_time_ns = past_ns});
132+
dd_span_finish_with_time(s1, past_ns - 1'000'000LL);
133+
dd_span_free(s1);
134+
135+
// Sentinel finish (steady_clock::now()) with a start projected into
136+
// the future — exercises the implicit-path clamp.
137+
const int64_t future_ns =
138+
std::chrono::duration_cast<std::chrono::nanoseconds>(
139+
(std::chrono::system_clock::now() + std::chrono::hours(1))
140+
.time_since_epoch())
141+
.count();
142+
auto *s2 = dd_tracer_create_span(
143+
ctx.tracer, {.name = "implicit", .start_time_ns = future_ns});
144+
dd_span_finish(s2);
145+
dd_span_free(s2);
146+
147+
for (const auto &chunk : ctx.collector->chunks)
148+
for (const auto &span : chunk)
149+
CHECK(span->duration >= std::chrono::nanoseconds(0));
112150
}
113151

114152
TEST_CASE("create span with resource", "[c_binding]") {
@@ -253,6 +291,7 @@ TEST_CASE("null arguments do not crash", "[c_binding]") {
253291
dd_span_set_error_message(nullptr, "msg");
254292
dd_span_inject(nullptr, test_header_writer);
255293
dd_span_finish(nullptr);
294+
dd_span_finish_with_time(nullptr, 0);
256295
dd_span_set_resource(nullptr, "res");
257296
dd_span_set_service(nullptr, "svc");
258297
}

0 commit comments

Comments
 (0)