Skip to content

Commit ba2ee22

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 ba2ee22

3 files changed

Lines changed: 77 additions & 6 deletions

File tree

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

Lines changed: 11 additions & 1 deletion
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,14 +44,16 @@ typedef enum {
4344
DD_OPT_INTEGRATION_VERSION = 5
4445
} dd_tracer_option;
4546

46-
// Options for creating a span. Unset fields default to NULL.
47+
// Options for creating a span. String fields default to NULL; integer
48+
// fields default to 0.
4749
typedef struct {
4850
const char* name;
4951
const char* resource;
5052
const char* service;
5153
const char* service_type;
5254
const char* environment;
5355
const char* version;
56+
int64_t start_time_ns; // UNIX-epoch nanoseconds; 0 = use current time
5457
} dd_span_options_t;
5558

5659
// Error codes returned by the C binding.
@@ -179,6 +182,13 @@ DD_TRACE_C_API dd_span_t* dd_span_create_child(dd_span_t* span_handle,
179182
// @param span_handle Span handle
180183
DD_TRACE_C_API void dd_span_finish(dd_span_t* span_handle);
181184

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

binding/c/src/tracer.cpp

Lines changed: 37 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, now_tick - std::chrono::duration_cast<dd::Duration>(
56+
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,31 @@ 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+
if (end_time_ns == 0) {
273+
span->set_end_time(std::chrono::steady_clock::now());
274+
return;
275+
}
276+
// Anchor end.tick to the span's stored start so NTP adjustments during
277+
// the span's lifetime don't skew the reported duration, and clamp to
278+
// start on negative deltas (serialization casts duration to uint64_t).
279+
const auto start = span->start_time();
280+
const auto end_wall = std::chrono::system_clock::time_point(
281+
std::chrono::round<std::chrono::system_clock::duration>(
282+
std::chrono::nanoseconds(end_time_ns)));
283+
const auto duration = end_wall > start.wall
284+
? std::chrono::duration_cast<dd::Duration>(
285+
end_wall - start.wall)
286+
: dd::Duration::zero();
287+
span->set_end_time(start.tick + duration);
288+
}
289+
290+
void dd_span_finish(dd_span_t *span_handle) {
291+
dd_span_finish_with_time(span_handle, 0);
258292
}
259293

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

binding/c/test/test_c_binding.cpp

Lines changed: 29 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,28 @@ 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 with end before start clamps to zero duration",
126+
"[c_binding]") {
127+
auto ctx = make_tracer();
128+
129+
const int64_t start_ns = 1'700'000'000'000'000'000LL;
130+
auto *span = dd_tracer_create_span(
131+
ctx.tracer, {.name = "reversed", .start_time_ns = start_ns});
132+
REQUIRE(span != nullptr);
133+
134+
dd_span_finish_with_time(span, start_ns - 1'000'000LL);
135+
dd_span_free(span);
136+
137+
CHECK(ctx.collector->first_span().duration >= std::chrono::nanoseconds(0));
112138
}
113139

114140
TEST_CASE("create span with resource", "[c_binding]") {
@@ -253,6 +279,7 @@ TEST_CASE("null arguments do not crash", "[c_binding]") {
253279
dd_span_set_error_message(nullptr, "msg");
254280
dd_span_inject(nullptr, test_header_writer);
255281
dd_span_finish(nullptr);
282+
dd_span_finish_with_time(nullptr, 0);
256283
dd_span_set_resource(nullptr, "res");
257284
dd_span_set_service(nullptr, "svc");
258285
}

0 commit comments

Comments
 (0)