Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions binding/c/include/datadog/c/tracer.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include <stddef.h>
#include <stdint.h>

#if defined(_WIN32)
#if defined(DD_TRACE_C_BUILDING)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
//
Expand Down
71 changes: 70 additions & 1 deletion binding/c/src/tracer.cpp
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
#include "datadog/c/tracer.h"

#include <datadog/collector.h>
#include <datadog/hex.h>
#include <datadog/msgpack.h>
#include <datadog/span_data.h>
#include <datadog/trace_segment.h>
#include <datadog/tracer.h>

#include <cstddef>
#include <cstring>
#include <memory>
#include <string>
#include <utility>

namespace dd = datadog::tracing;

Expand Down Expand Up @@ -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<void> send(
std::vector<std::unique_ptr<dd::SpanData>> &&spans,
const std::shared_ptr<dd::TraceSampler> & /*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<const uint8_t *>(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) {
Expand Down Expand Up @@ -125,20 +161,53 @@ 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<dd::TracerConfig *>(handle);
if (callback == nullptr) {
cfg->collector.reset();
return;
}
cfg->collector = std::make_shared<CallbackCollector>(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");
return nullptr;
}

const auto *config = reinterpret_cast<const dd::TracerConfig *>(conf_handle);
const auto validated_config = dd::finalize_config(*config);
auto validated_config = dd::finalize_config(*config);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bypass agent URL validation in callback collector mode

dd_tracer_new always runs finalize_config(*config) before handling the callback-collector path, so tracer creation still fails when DD_TRACE_AGENT_URL/DD_OPT_AGENT_URL is malformed even though traces are being diverted to the in-process callback. finalize_config validates agent config unconditionally (src/datadog/tracer_config.cpp), which makes this new callback mode unusable in callback-only deployments that intentionally do not rely on agent transport.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documented as a caveat in the header docstring — malformed DD_TRACE_AGENT_URL / DD_OPT_AGENT_URL still fails finalize_config before the callback path is taken. Fixing this properly requires either unsetenv() (process-wide side effect) or refactoring finalize_config to defer agent validation, both of which are out of scope for this PR. Callers in callback mode should simply not set a malformed URL.

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<std::shared_ptr<dd::Collector>>(
validated_config->collector)) {
validated_config->telemetry.enabled = false;
validated_config->telemetry.report_metrics = false;
validated_config->telemetry.report_logs = false;
Comment on lines +206 to +208
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Disable telemetry globally when callback collector is used

This only mutates the local tracer config, but telemetry is process-global and initialized once (src/datadog/telemetry/telemetry.cpp uses a static singleton), so if another tracer already initialized telemetry as enabled, creating a callback-configured tracer here will not actually stop telemetry network traffic. In multi-tracer processes, this violates the new API contract that callback mode performs zero network I/O.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documented as a caveat in the header docstring. Telemetry is process-global; the config setter can only affect this tracer's finalized config. In multi-tracer processes where an earlier tracer already initialized telemetry as enabled, that traffic continues. Fully silencing it from the C binding would require touching the telemetry module's singleton, which is out of scope. The typical C-binding use case (Kong) is single-tracer-per-process, where this is a non-issue.

}

try {
return reinterpret_cast<dd_tracer_t *>(new dd::Tracer{*validated_config});
} catch (...) {
Expand Down
35 changes: 35 additions & 0 deletions binding/c/test/test_c_binding.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,40 @@ TEST_CASE("tracer new with invalid config and null error", "[c_binding]") {
dd_tracer_conf_free(conf);
}

namespace {
std::vector<std::vector<uint8_t>> 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<const char *>(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);
Expand All @@ -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);
}
Loading