diff --git a/binding/c/include/datadog/c/tracer.h b/binding/c/include/datadog/c/tracer.h index 655d334d..b67ff442 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) @@ -34,6 +35,16 @@ typedef const char* (*dd_context_read_callback)(const char* key); // @param value Header value to set typedef void (*dd_context_write_callback)(const char* key, const char* value); +// Callback invoked with the msgpack-encoded trace payload that would +// otherwise be POSTed to /v0.4/traces. The bytes are identical to the +// HTTP body the tracer would have sent. `user_data` is passed through +// from dd_tracer_conf_set_collector_callback. +// +// Called synchronously on the thread finishing a span; if it blocks, +// all span-finishing on that thread stalls. +typedef void (*dd_trace_msgpack_callback)(const uint8_t* data, size_t size, + void* user_data); + typedef enum { DD_OPT_SERVICE_NAME = 0, DD_OPT_ENV = 1, @@ -90,6 +101,28 @@ DD_TRACE_C_API void dd_tracer_conf_set(dd_conf_t* handle, dd_tracer_option option, const void* value); +// Install (or clear, by passing NULL) a custom collector callback. When a +// callback is set, finished trace chunks are handed to it instead of being +// POSTed to the Datadog Agent; telemetry and remote-configuration polling +// are also disabled for the resulting tracer. +// +// Caveats: +// - Telemetry is process-global; if another tracer has already initialized +// telemetry as enabled in this process, that traffic continues. +// - DD_TRACE_AGENT_URL, if set to a malformed URL, still fails tracer +// creation even in callback mode (finalize_config validates the URL +// before the callback path is taken). +// +// `user_data` must remain valid for the lifetime of any tracer built from +// this handle (the callback may fire during tracer shutdown). No-op if +// handle is NULL. +// +// @param handle Configuration handle +// @param callback Callback to receive msgpack payloads, or NULL to clear +// @param user_data Opaque pointer passed verbatim to the callback +DD_TRACE_C_API void dd_tracer_conf_set_collector_callback( + dd_conf_t* handle, dd_trace_msgpack_callback callback, void* user_data); + // Creates a tracer instance. The configuration handle may be freed with // dd_tracer_conf_free after this call returns. // diff --git a/binding/c/src/tracer.cpp b/binding/c/src/tracer.cpp index 2a81a40f..660479b3 100644 --- a/binding/c/src/tracer.cpp +++ b/binding/c/src/tracer.cpp @@ -1,12 +1,17 @@ #include "datadog/c/tracer.h" +#include #include +#include +#include #include #include #include #include +#include #include +#include namespace dd = datadog::tracing; @@ -44,6 +49,37 @@ class ContextWriter : public dd::DictWriter { } }; +// Collector that forwards finished trace chunks to a C callback. The payload +// matches the /v0.4/traces HTTP body: a 1-element outer array containing +// the chunk, produced by the same msgpack encoder the DatadogAgent uses. +class CallbackCollector : public dd::Collector { + dd_trace_msgpack_callback callback_; + void *user_data_; + + public: + CallbackCollector(dd_trace_msgpack_callback callback, void *user_data) + : callback_(callback), user_data_(user_data) {} + + dd::Expected send( + std::vector> &&spans, + const std::shared_ptr & /*response_handler*/) override { + std::string buffer; + if (auto rc = dd::msgpack::pack_array(buffer, std::size_t{1}); !rc) { + return rc; + } + if (auto rc = dd::msgpack_encode(buffer, spans); !rc) { + return rc; + } + callback_(reinterpret_cast(buffer.data()), buffer.size(), + user_data_); + return {}; + } + + std::string config() const override { + return "{\"type\":\"datadog::binding::c::CallbackCollector\"}"; + } +}; + dd::SpanConfig make_span_config(dd_span_options_t options) { dd::SpanConfig span_config; if (options.name != nullptr) { @@ -125,6 +161,27 @@ void dd_tracer_conf_set(dd_conf_t *handle, dd_tracer_option option, } } +void dd_tracer_conf_set_collector_callback(dd_conf_t *handle, + dd_trace_msgpack_callback callback, + void *user_data) { + if (handle == nullptr) { + return; + } + auto *cfg = reinterpret_cast(handle); + if (callback == nullptr) { + cfg->collector.reset(); + return; + } + cfg->collector = std::make_shared(callback, user_data); + // Telemetry is silenced unconditionally in dd_tracer_new after + // finalize_config, because env vars like DD_INSTRUMENTATION_TELEMETRY_ENABLED + // outrank user-config fields set here. Remote-config polling is implicitly + // disabled: when a custom collector is present, the Tracer skips DatadogAgent + // construction entirely, so no RC loop starts. The agent URL is left alone: + // callback mode never contacts it, but clobbering it here would silently + // lose a user-set URL across a set/clear callback cycle. +} + dd_tracer_t *dd_tracer_new(const dd_conf_t *conf_handle, dd_error_t *error) { if (conf_handle == nullptr) { set_error(error, DD_ERROR_NULL_ARGUMENT, "conf_handle is NULL"); @@ -132,13 +189,25 @@ dd_tracer_t *dd_tracer_new(const dd_conf_t *conf_handle, dd_error_t *error) { } const auto *config = reinterpret_cast(conf_handle); - const auto validated_config = dd::finalize_config(*config); + auto validated_config = dd::finalize_config(*config); if (!validated_config) { set_error(error, DD_ERROR_INVALID_CONFIG, validated_config.error().message.c_str()); return nullptr; } + // When a custom collector is installed (only possible via + // dd_tracer_conf_set_collector_callback), force telemetry off. We must do + // this after finalize_config because env vars like + // DD_INSTRUMENTATION_TELEMETRY_ENABLED outrank user-config fields and would + // otherwise keep telemetry network traffic alive despite the callback. + if (std::holds_alternative>( + validated_config->collector)) { + validated_config->telemetry.enabled = false; + validated_config->telemetry.report_metrics = false; + validated_config->telemetry.report_logs = false; + } + try { return reinterpret_cast(new dd::Tracer{*validated_config}); } catch (...) { diff --git a/binding/c/test/test_c_binding.cpp b/binding/c/test/test_c_binding.cpp index 7b93516b..be75bdaa 100644 --- a/binding/c/test/test_c_binding.cpp +++ b/binding/c/test/test_c_binding.cpp @@ -235,6 +235,40 @@ TEST_CASE("tracer new with invalid config and null error", "[c_binding]") { dd_tracer_conf_free(conf); } +namespace { +std::vector> g_callback_payloads; +void callback_sink(const uint8_t *data, size_t size, void * /*user_data*/) { + g_callback_payloads.emplace_back(data, data + size); +} +} // namespace + +TEST_CASE("custom collector callback receives trace payload", "[c_binding]") { + g_callback_payloads.clear(); + + auto *conf = dd_tracer_conf_new(); + dd_tracer_conf_set(conf, DD_OPT_SERVICE_NAME, "callback-service"); + dd_tracer_conf_set_collector_callback(conf, callback_sink, nullptr); + + auto *tracer = dd_tracer_new(conf, nullptr); + REQUIRE(tracer != nullptr); + dd_tracer_conf_free(conf); + + auto *span = dd_tracer_create_span(tracer, {.name = "callback.test"}); + REQUIRE(span != nullptr); + dd_span_finish(span); + dd_span_free(span); + dd_tracer_free(tracer); + + REQUIRE(g_callback_payloads.size() == 1); + CHECK(!g_callback_payloads[0].empty()); + // Spot-check: short strings are embedded as raw ASCII bytes in msgpack. + const std::string_view blob( + reinterpret_cast(g_callback_payloads[0].data()), + g_callback_payloads[0].size()); + CHECK(blob.find("callback-service") != std::string_view::npos); + CHECK(blob.find("callback.test") != std::string_view::npos); +} + TEST_CASE("null arguments do not crash", "[c_binding]") { CHECK(dd_tracer_new(nullptr, nullptr) == nullptr); CHECK(dd_tracer_create_span(nullptr, {.name = "x"}) == nullptr); @@ -255,4 +289,5 @@ TEST_CASE("null arguments do not crash", "[c_binding]") { dd_span_finish(nullptr); dd_span_set_resource(nullptr, "res"); dd_span_set_service(nullptr, "svc"); + dd_tracer_conf_set_collector_callback(nullptr, callback_sink, nullptr); }