diff --git a/CMakeLists.txt b/CMakeLists.txt index c8f49148..8cc154f1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,6 +18,7 @@ project( option(BUILD_SHARED_LIBS "Build shared libraries" ON) option(BUILD_STATIC_LIBS "Build static libraries" ON) +option(DD_TRACE_BUILD_C_BINDING "Build C binding" OFF) if (WIN32) option(DD_TRACE_STATIC_CRT "Build dd-trace-cpp with static CRT with MSVC" OFF) @@ -104,6 +105,10 @@ if (DD_TRACE_BUILD_TOOLS) add_subdirectory(tools/config-inversion) endif () +if (DD_TRACE_BUILD_C_BINDING) + add_subdirectory(binding/c) +endif () + add_library(dd-trace-cpp-objects OBJECT) add_library(dd-trace-cpp::obj ALIAS dd-trace-cpp-objects) @@ -294,11 +299,11 @@ if (BUILD_SHARED_LIBS) ) install( - TARGETS dd-trace-cpp-shared + TARGETS dd-trace-cpp-shared EXPORT dd-trace-cpp-targets LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} - RUNTIME DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} ) endif () @@ -350,7 +355,7 @@ if (BUILD_STATIC_LIBS) EXPORT dd-trace-cpp-targets LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} - RUNTIME DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} ) endif () diff --git a/bin/check-format b/bin/check-format index d8131b23..0a840785 100755 --- a/bin/check-format +++ b/bin/check-format @@ -1,4 +1,4 @@ #!/bin/sh -find include/ src/ examples/ test/ -type f \( -name '*.h' -o -name '*.cpp' \) -print0 | \ +find binding/ examples/ fuzz/ include/ src/ test/ -type f \( -name '*.h' -o -name '*.cpp' \) -print0 | \ xargs -0 clang-format-14 --style=file --dry-run -Werror diff --git a/bin/format b/bin/format index 6a5d66ac..9bcc3993 100755 --- a/bin/format +++ b/bin/format @@ -18,7 +18,7 @@ formatter=clang-format-$version formatter_options="--style=file -i $*" find_sources() { - find include/ src/ examples/ test/ fuzz/ -type f \( -name '*.h' -o -name '*.cpp' \) "$@" + find binding/ examples/ fuzz/ include/ src/ test/ -type f \( -name '*.h' -o -name '*.cpp' \) "$@" } # If the correct version of clang-format is installed, then use it and quit. diff --git a/binding/c/CMakeLists.txt b/binding/c/CMakeLists.txt new file mode 100644 index 00000000..34a87c3f --- /dev/null +++ b/binding/c/CMakeLists.txt @@ -0,0 +1,66 @@ +add_library(dd_trace_c) +add_library(dd-trace-cpp::c_binding ALIAS dd_trace_c) + +target_compile_definitions(dd_trace_c PRIVATE DD_TRACE_C_BUILDING) + +target_sources(dd_trace_c + PRIVATE + $ + src/tracer.cpp +) + +if (DD_TRACE_TRANSPORT STREQUAL "curl") + target_sources(dd_trace_c + PRIVATE + ${CMAKE_SOURCE_DIR}/src/datadog/curl.cpp + ${CMAKE_SOURCE_DIR}/src/datadog/default_http_client_curl.cpp + ) + + target_link_libraries(dd_trace_c + PRIVATE + CURL::libcurl_shared + ) +else () + target_sources(dd_trace_c + PRIVATE + ${CMAKE_SOURCE_DIR}/src/datadog/default_http_client_null.cpp + ) +endif () + +target_include_directories(dd_trace_c + PUBLIC + $ + $ + PRIVATE + ${CMAKE_SOURCE_DIR}/src +) + +target_link_libraries(dd_trace_c + PRIVATE + dd-trace-cpp::obj + dd-trace-cpp::specs +) + +install(TARGETS dd_trace_c + EXPORT dd-trace-cpp-targets + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) + +install( + DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/datadog + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} +) + +if (DD_TRACE_BUILD_TESTING) + target_sources(tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/test/test_c_binding.cpp + ) + + target_include_directories(tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/include + ) + + target_link_libraries(tests PRIVATE dd_trace_c) +endif () diff --git a/binding/c/README.md b/binding/c/README.md new file mode 100644 index 00000000..73afa75d --- /dev/null +++ b/binding/c/README.md @@ -0,0 +1,17 @@ +# C Binding for dd-trace-cpp + +A C binding interface on the C++ tracing library for +integration from C-based projects. + +## Building + +```sh +cmake -B build -DDD_TRACE_BUILD_C_BINDING=ON -DDD_TRACE_BUILD_TESTING=ON +cmake --build build -j +``` + +## Running Tests + +```sh +./build/binding/c/test_c_binding +``` diff --git a/binding/c/include/datadog/c/tracer.h b/binding/c/include/datadog/c/tracer.h new file mode 100644 index 00000000..554f6c4d --- /dev/null +++ b/binding/c/include/datadog/c/tracer.h @@ -0,0 +1,204 @@ +#pragma once + +#include + +#if defined(_WIN32) +#if defined(DD_TRACE_C_BUILDING) +#define DD_TRACE_C_API __declspec(dllexport) +#else +#define DD_TRACE_C_API __declspec(dllimport) +#endif +#elif defined(__GNUC__) || defined(__clang__) +#define DD_TRACE_C_API __attribute__((visibility("default"))) +#else +#define DD_TRACE_C_API +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +// Callback used during trace context extraction. The tracer calls this +// function for each propagation header it needs to read (e.g. "x-datadog-*"). +// +// @param key Header name to look up +// @return Header value, or NULL if the header is not present. +// The returned pointer must remain valid until +// dd_tracer_extract_or_create_span returns. +typedef const char* (*dd_context_read_callback)(const char* key); + +// Callback used during trace context injection. The tracer calls this +// function for each propagation header it needs to write. +// +// @param key Header name to set +// @param value Header value to set +typedef void (*dd_context_write_callback)(const char* key, const char* value); + +typedef enum { + DD_OPT_SERVICE_NAME = 0, + DD_OPT_ENV = 1, + DD_OPT_VERSION = 2, + DD_OPT_AGENT_URL = 3, + DD_OPT_INTEGRATION_NAME = 4, + DD_OPT_INTEGRATION_VERSION = 5 +} dd_tracer_option; + +// Options for creating a span. Unset fields default to NULL. +typedef struct { + const char* name; + const char* resource; + const char* service; +} dd_span_options_t; + +// Error details populated on failure. Caller provides the struct, +// callee fills it in. Pass NULL to ignore errors. +typedef struct { + int code; + char message[256]; +} dd_error_t; + +typedef struct dd_conf_s dd_conf_t; +typedef struct dd_tracer_s dd_tracer_t; +typedef struct dd_span_s dd_span_t; + +// Creates a tracer configuration instance. +// +// @return Configuration handle, or NULL on allocation failure +DD_TRACE_C_API dd_conf_t* dd_tracer_conf_new(void); + +// Release a tracer configuration. Safe to call with NULL. +// +// @param handle Configuration handle to release +DD_TRACE_C_API void dd_tracer_conf_free(dd_conf_t* handle); + +// Set or update a configuration field. No-op if handle is NULL. +// +// @param handle Configuration handle +// @param option Configuration option +// @param value Configuration value (interpretation depends on option) +DD_TRACE_C_API void dd_tracer_conf_set(dd_conf_t* handle, + dd_tracer_option option, void* value); + +// Creates a tracer instance. The configuration handle may be freed with +// dd_tracer_conf_free after this call returns. +// +// @param conf_handle Configuration handle (not modified) +// @param error Optional error output (may be NULL) +// +// @return Tracer handle, or NULL on error +DD_TRACE_C_API dd_tracer_t* dd_tracer_new(const dd_conf_t* conf_handle, + dd_error_t* error); + +// Release a tracer instance. Safe to call with NULL. +// +// @param tracer_handle Tracer handle to release +DD_TRACE_C_API void dd_tracer_free(dd_tracer_t* tracer_handle); + +// Create a span using a Tracer. +// +// @param tracer_handle Tracer handle +// @param options Span options (name must not be NULL) +// +// @return Span handle, or NULL on error +DD_TRACE_C_API dd_span_t* dd_tracer_create_span( + dd_tracer_t* tracer_handle, const dd_span_options_t* options); + +// Extract trace context from incoming headers, or create a new root span +// if extraction fails. Never returns an error span; on extraction failure +// a fresh root span is created. +// +// @param tracer_handle Tracer handle +// @param on_context_read Callback invoked to read propagation headers +// @param options Span options (name must not be NULL) +// +// @return Span handle, or NULL if arguments are invalid +DD_TRACE_C_API dd_span_t* dd_tracer_extract_or_create_span( + dd_tracer_t* tracer_handle, dd_context_read_callback on_context_read, + const dd_span_options_t* options); + +// Release a span instance. Safe to call with NULL. +// If the span has not been finished with dd_span_finish, it is +// automatically finished (its end time is recorded) before being freed. +// +// @param span_handle Span handle +DD_TRACE_C_API void dd_span_free(dd_span_t* span_handle); + +// Set a tag (key-value pair) on a span. No-op if any argument is NULL. +// +// @param span_handle Span handle +// @param key Tag key +// @param value Tag value +DD_TRACE_C_API void dd_span_set_tag(dd_span_t* span_handle, const char* key, + const char* value); + +// Mark a span as erroneous. No-op if span_handle is NULL. +// +// @param span_handle Span handle +// @param error_value Non-zero to mark as error, zero to clear +DD_TRACE_C_API void dd_span_set_error(dd_span_t* span_handle, int error_value); + +// Set an error message on a span. No-op if any argument is NULL. +// +// @param span_handle Span handle +// @param error_message Error message string +DD_TRACE_C_API void dd_span_set_error_message(dd_span_t* span_handle, + const char* error_message); + +// Inject trace context into outgoing headers via callback. +// No-op if any argument is NULL. +// +// @param span_handle Span handle +// @param on_context_write Callback invoked per propagation header +DD_TRACE_C_API void dd_span_inject(dd_span_t* span_handle, + dd_context_write_callback on_context_write); + +// Create a child span. Returns NULL if any required argument is NULL. +// +// @param span_handle Parent span handle +// @param options Span options (name must not be NULL) +// +// @return Child span handle, or NULL +DD_TRACE_C_API dd_span_t* dd_span_create_child( + dd_span_t* span_handle, const dd_span_options_t* options); + +// Finish a span by recording its end time. No-op if span_handle is NULL. +// After finishing, the span should be freed with dd_span_free. +// +// @param span_handle Span handle +DD_TRACE_C_API void dd_span_finish(dd_span_t* span_handle); + +// Get the trace ID as a zero-padded hex string. +// +// @param span_handle Span handle +// @param buffer Output buffer (at least 33 bytes for 128-bit IDs) +// @param buffer_size Size of the buffer +// @return Number of characters written, or -1 on error +DD_TRACE_C_API int dd_span_get_trace_id(dd_span_t* span_handle, char* buffer, + size_t buffer_size); + +// Get the span ID as a zero-padded hex string. +// +// @param span_handle Span handle +// @param buffer Output buffer (at least 17 bytes) +// @param buffer_size Size of the buffer +// @return Number of characters written (16), or -1 on error +DD_TRACE_C_API int dd_span_get_span_id(dd_span_t* span_handle, char* buffer, + size_t buffer_size); + +// Set the resource name on a span. No-op if any argument is NULL. +// +// @param span_handle Span handle +// @param resource Resource name +DD_TRACE_C_API void dd_span_set_resource(dd_span_t* span_handle, + const char* resource); + +// Set the service name on a span. No-op if any argument is NULL. +// +// @param span_handle Span handle +// @param service Service name +DD_TRACE_C_API void dd_span_set_service(dd_span_t* span_handle, + const char* service); + +#if defined(__cplusplus) +} +#endif diff --git a/binding/c/src/tracer.cpp b/binding/c/src/tracer.cpp new file mode 100644 index 00000000..99abacac --- /dev/null +++ b/binding/c/src/tracer.cpp @@ -0,0 +1,305 @@ +#include "datadog/c/tracer.h" + +#include +#include +#include + +#include +#include +#include + +namespace dd = datadog::tracing; + +namespace { + +class ContextReader : public dd::DictReader { + dd_context_read_callback read_; + + public: + explicit ContextReader(dd_context_read_callback read_callback) + : read_(read_callback) {} + + dd::Optional lookup(dd::StringView key) const override { + std::string key_str(key); + if (auto value = read_(key_str.c_str())) { + return value; + } + return dd::nullopt; + } + + void visit(const std::function + & /* visitor */) const override {} +}; + +class ContextWriter : public dd::DictWriter { + dd_context_write_callback write_; + + public: + explicit ContextWriter(dd_context_write_callback func) : write_(func) {} + + void set(dd::StringView key, dd::StringView value) override { + std::string key_str(key); + std::string value_str(value); + write_(key_str.c_str(), value_str.c_str()); + } +}; + +dd::SpanConfig make_span_config(const dd_span_options_t *options) { + dd::SpanConfig span_config; + if (options == nullptr) { + return span_config; + } + if (options->name != nullptr) { + span_config.name = options->name; + } + if (options->resource != nullptr) { + span_config.resource = options->resource; + } + if (options->service != nullptr) { + span_config.service = options->service; + } + return span_config; +} + +void set_error(dd_error_t *error, int code, const char *message) { + if (error == nullptr) { + return; + } + error->code = code; + std::strncpy(error->message, message, sizeof(error->message) - 1); + error->message[sizeof(error->message) - 1] = '\0'; +} + +} // namespace + +extern "C" { + +dd_conf_t *dd_tracer_conf_new(void) { + try { + return reinterpret_cast(new dd::TracerConfig); + } catch (...) { + return nullptr; + } +} + +void dd_tracer_conf_free(dd_conf_t *handle) { + if (handle == nullptr) { + return; + } + delete reinterpret_cast(handle); +} + +void dd_tracer_conf_set(dd_conf_t *handle, dd_tracer_option option, + void *value) { + if (handle == nullptr || value == nullptr) { + return; + } + + auto *cfg = reinterpret_cast(handle); + + switch (option) { + case DD_OPT_SERVICE_NAME: + cfg->service = static_cast(value); + break; + case DD_OPT_ENV: + cfg->environment = static_cast(value); + break; + case DD_OPT_VERSION: + cfg->version = static_cast(value); + break; + case DD_OPT_AGENT_URL: + cfg->agent.url = static_cast(value); + break; + case DD_OPT_INTEGRATION_NAME: + cfg->integration_name = static_cast(value); + break; + case DD_OPT_INTEGRATION_VERSION: + cfg->integration_version = static_cast(value); + break; + } +} + +dd_tracer_t *dd_tracer_new(const dd_conf_t *conf_handle, dd_error_t *error) { + if (conf_handle == nullptr) { + set_error(error, 1, "conf_handle is NULL"); + return nullptr; + } + + const auto *config = reinterpret_cast(conf_handle); + const auto validated_config = dd::finalize_config(*config); + if (!validated_config) { + set_error(error, 2, validated_config.error().message.c_str()); + return nullptr; + } + + try { + return reinterpret_cast(new dd::Tracer{*validated_config}); + } catch (...) { + set_error(error, 3, "failed to allocate tracer"); + return nullptr; + } +} + +void dd_tracer_free(dd_tracer_t *tracer_handle) { + if (tracer_handle == nullptr) { + return; + } + delete reinterpret_cast(tracer_handle); +} + +dd_span_t *dd_tracer_create_span(dd_tracer_t *tracer_handle, + const dd_span_options_t *options) { + if (tracer_handle == nullptr || options == nullptr || + options->name == nullptr) { + return nullptr; + } + + auto *tracer = reinterpret_cast(tracer_handle); + auto span_config = make_span_config(options); + + try { + return reinterpret_cast( + new dd::Span(tracer->create_span(span_config))); + } catch (...) { + return nullptr; + } +} + +dd_span_t *dd_tracer_extract_or_create_span( + dd_tracer_t *tracer_handle, dd_context_read_callback on_context_read, + const dd_span_options_t *options) { + if (tracer_handle == nullptr || on_context_read == nullptr || + options == nullptr || options->name == nullptr) { + return nullptr; + } + + auto *tracer = reinterpret_cast(tracer_handle); + auto span_config = make_span_config(options); + + ContextReader reader(on_context_read); + try { + return reinterpret_cast( + new dd::Span(tracer->extract_or_create_span(reader, span_config))); + } catch (...) { + return nullptr; + } +} + +void dd_span_free(dd_span_t *span_handle) { + if (span_handle == nullptr) { + return; + } + delete reinterpret_cast(span_handle); +} + +void dd_span_set_tag(dd_span_t *span_handle, const char *key, + const char *value) { + if (span_handle == nullptr || key == nullptr || value == nullptr) { + return; + } + reinterpret_cast(span_handle)->set_tag(key, value); +} + +void dd_span_set_error(dd_span_t *span_handle, int error_value) { + if (span_handle == nullptr) { + return; + } + reinterpret_cast(span_handle)->set_error(error_value != 0); +} + +void dd_span_set_error_message(dd_span_t *span_handle, + const char *error_message) { + if (span_handle == nullptr || error_message == nullptr) { + return; + } + reinterpret_cast(span_handle)->set_error_message(error_message); +} + +void dd_span_inject(dd_span_t *span_handle, + dd_context_write_callback on_context_write) { + if (span_handle == nullptr || on_context_write == nullptr) { + return; + } + + auto *span = reinterpret_cast(span_handle); + ContextWriter writer(on_context_write); + span->inject(writer); +} + +dd_span_t *dd_span_create_child(dd_span_t *span_handle, + const dd_span_options_t *options) { + if (span_handle == nullptr || options == nullptr || + options->name == nullptr) { + return nullptr; + } + + auto *span = reinterpret_cast(span_handle); + auto span_config = make_span_config(options); + + try { + return reinterpret_cast( + new dd::Span(span->create_child(span_config))); + } catch (...) { + return nullptr; + } +} + +void dd_span_finish(dd_span_t *span_handle) { + if (span_handle == nullptr) { + return; + } + reinterpret_cast(span_handle) + ->set_end_time(std::chrono::steady_clock::now()); +} + +int dd_span_get_trace_id(dd_span_t *span_handle, char *buffer, + size_t buffer_size) { + if (span_handle == nullptr || buffer == nullptr || buffer_size == 0) { + return -1; + } + + auto *span = reinterpret_cast(span_handle); + std::string hex = span->trace_id().hex_padded(); + + if (hex.size() >= buffer_size) { + return -1; + } + + std::strncpy(buffer, hex.c_str(), buffer_size); + // Safe narrowing: hex trace IDs are at most 32 characters. + return static_cast(hex.size()); +} + +int dd_span_get_span_id(dd_span_t *span_handle, char *buffer, + size_t buffer_size) { + if (span_handle == nullptr || buffer == nullptr || buffer_size == 0) { + return -1; + } + + auto *span = reinterpret_cast(span_handle); + std::string hex = dd::hex_padded(span->id()); + + if (hex.size() >= buffer_size) { + return -1; + } + + std::strncpy(buffer, hex.c_str(), buffer_size); + // Safe narrowing: hex span IDs are 16 characters. + return static_cast(hex.size()); +} + +void dd_span_set_resource(dd_span_t *span_handle, const char *resource) { + if (span_handle == nullptr || resource == nullptr) { + return; + } + reinterpret_cast(span_handle)->set_resource_name(resource); +} + +void dd_span_set_service(dd_span_t *span_handle, const char *service) { + if (span_handle == nullptr || service == nullptr) { + return; + } + reinterpret_cast(span_handle)->set_service_name(service); +} + +} // extern "C" diff --git a/binding/c/test/test_c_binding.cpp b/binding/c/test/test_c_binding.cpp new file mode 100644 index 00000000..3bcf76c3 --- /dev/null +++ b/binding/c/test/test_c_binding.cpp @@ -0,0 +1,267 @@ +#include +#include + +#include "mocks/collectors.h" +#include "null_logger.h" +#include "test.h" + +namespace dd = datadog::tracing; + +namespace { + +constexpr size_t trace_id_buf_size = 33; +constexpr size_t span_id_buf_size = 17; + +std::unordered_map g_headers; + +const char *test_header_reader(const char *key) { + auto it = g_headers.find(key); + if (it != g_headers.end()) { + return it->second.c_str(); + } + return nullptr; +} + +void test_header_writer(const char *key, const char *value) { + g_headers[key] = value; +} + +struct TestTracer { + std::shared_ptr collector; + dd_tracer_t *tracer; + + ~TestTracer() { dd_tracer_free(tracer); } +}; + +TestTracer make_tracer() { + auto *conf = dd_tracer_conf_new(); + dd_tracer_conf_set(conf, DD_OPT_SERVICE_NAME, (void *)"test-service"); + + // Inject mocks before const handoff to dd_tracer_new. + auto *cfg = reinterpret_cast(conf); + TestTracer result; + result.collector = std::make_shared(); + cfg->collector = result.collector; + cfg->logger = std::make_shared(); + + result.tracer = dd_tracer_new(conf, nullptr); + dd_tracer_conf_free(conf); + REQUIRE(result.tracer != nullptr); + return result; +} + +} // namespace + +TEST_CASE("tracer lifecycle", "[c_binding]") { + auto *conf = dd_tracer_conf_new(); + REQUIRE(conf != nullptr); + + dd_tracer_conf_set(conf, DD_OPT_SERVICE_NAME, (void *)"my-service"); + dd_tracer_conf_set(conf, DD_OPT_ENV, (void *)"staging"); + dd_tracer_conf_set(conf, DD_OPT_VERSION, (void *)"1.0.0"); + dd_tracer_conf_set(conf, DD_OPT_AGENT_URL, (void *)"http://foo:8080"); + dd_tracer_conf_set(conf, DD_OPT_INTEGRATION_NAME, (void *)"my-integration"); + dd_tracer_conf_set(conf, DD_OPT_INTEGRATION_VERSION, (void *)"2.0.0"); + + // Inject mocks so dd_tracer_new succeeds without a real agent. + auto *cfg = reinterpret_cast(conf); + cfg->collector = std::make_shared(); + cfg->logger = std::make_shared(); + + auto *tracer = dd_tracer_new(conf, nullptr); + REQUIRE(tracer != nullptr); + + dd_tracer_conf_free(conf); + dd_tracer_free(tracer); +} + +TEST_CASE("tracer new propagates error", "[c_binding]") { + // Create config without injecting mocks — dd_tracer_new should fail + // and populate the error struct. + auto *conf = dd_tracer_conf_new(); + dd_tracer_conf_set(conf, DD_OPT_AGENT_URL, (void *)"not://valid"); + + dd_error_t err = {}; + auto *tracer = dd_tracer_new(conf, &err); + CHECK(tracer == nullptr); + CHECK(err.code != 0); + CHECK(err.message[0] != '\0'); + + dd_tracer_conf_free(conf); +} + +TEST_CASE("span create, tag, finish, free", "[c_binding]") { + auto ctx = make_tracer(); + + dd_span_options_t opts = {.name = "test.op"}; + auto *span = dd_tracer_create_span(ctx.tracer, &opts); + REQUIRE(span != nullptr); + + dd_span_set_tag(span, "http.method", "GET"); + dd_span_set_resource(span, "GET /api/users"); + dd_span_set_service(span, "user-service"); + dd_span_set_error(span, 1); + dd_span_set_error_message(span, "something broke"); + + dd_span_finish(span); + dd_span_free(span); + + const auto &sd = ctx.collector->first_span(); + CHECK(sd.tags.at("http.method") == "GET"); + CHECK(sd.resource == "GET /api/users"); + CHECK(sd.service == "user-service"); + CHECK(sd.error == true); + CHECK(sd.tags.at("error.message") == "something broke"); +} + +TEST_CASE("create span with resource", "[c_binding]") { + auto ctx = make_tracer(); + + dd_span_options_t opts = {.name = "web.request", + .resource = "GET /api/users"}; + auto *span = dd_tracer_create_span(ctx.tracer, &opts); + REQUIRE(span != nullptr); + + dd_span_finish(span); + dd_span_free(span); + + const auto &sd = ctx.collector->first_span(); + CHECK(sd.resource == "GET /api/users"); +} + +TEST_CASE("span free without finish auto-finishes", "[c_binding]") { + auto ctx = make_tracer(); + + dd_span_options_t opts = {.name = "auto.finish"}; + auto *span = dd_tracer_create_span(ctx.tracer, &opts); + REQUIRE(span != nullptr); + + dd_span_set_tag(span, "key", "value"); + + // Free without calling dd_span_finish — should auto-finish. + dd_span_free(span); + + const auto &sd = ctx.collector->first_span(); + CHECK(sd.name == "auto.finish"); + CHECK(sd.tags.at("key") == "value"); +} + +TEST_CASE("inject then extract preserves trace ID", "[c_binding]") { + auto ctx = make_tracer(); + + dd_span_options_t opts_1 = {.name = "producer"}; + auto *span_1 = dd_tracer_create_span(ctx.tracer, &opts_1); + g_headers.clear(); + dd_span_inject(span_1, test_header_writer); + CHECK(!g_headers.empty()); + + char trace_id_1[trace_id_buf_size] = {}; + dd_span_get_trace_id(span_1, trace_id_1, sizeof(trace_id_1)); + + dd_span_options_t opts_2 = {.name = "consumer", + .resource = "GET /downstream"}; + auto *span_2 = + dd_tracer_extract_or_create_span(ctx.tracer, test_header_reader, &opts_2); + REQUIRE(span_2 != nullptr); + + char trace_id_2[trace_id_buf_size] = {}; + dd_span_get_trace_id(span_2, trace_id_2, sizeof(trace_id_2)); + + CHECK(std::string(trace_id_1) == std::string(trace_id_2)); + + dd_span_finish(span_1); + dd_span_free(span_1); + dd_span_finish(span_2); + dd_span_free(span_2); +} + +TEST_CASE("child span shares trace ID", "[c_binding]") { + auto ctx = make_tracer(); + + dd_span_options_t parent_opts = {.name = "parent.op"}; + auto *parent = dd_tracer_create_span(ctx.tracer, &parent_opts); + REQUIRE(parent != nullptr); + + dd_span_options_t child_opts = {.name = "child.op"}; + auto *child = dd_span_create_child(parent, &child_opts); + REQUIRE(child != nullptr); + + char parent_trace[trace_id_buf_size] = {}; + char child_trace[trace_id_buf_size] = {}; + dd_span_get_trace_id(parent, parent_trace, sizeof(parent_trace)); + dd_span_get_trace_id(child, child_trace, sizeof(child_trace)); + CHECK(std::string(parent_trace) == std::string(child_trace)); + + char parent_span_id[span_id_buf_size] = {}; + char child_span_id[span_id_buf_size] = {}; + dd_span_get_span_id(parent, parent_span_id, sizeof(parent_span_id)); + dd_span_get_span_id(child, child_span_id, sizeof(child_span_id)); + CHECK(std::string(parent_span_id) != std::string(child_span_id)); + + dd_span_finish(child); + dd_span_free(child); + dd_span_finish(parent); + dd_span_free(parent); +} + +TEST_CASE("child span with service and resource", "[c_binding]") { + auto ctx = make_tracer(); + + dd_span_options_t parent_opts = {.name = "parent.op"}; + auto *parent = dd_tracer_create_span(ctx.tracer, &parent_opts); + REQUIRE(parent != nullptr); + + dd_span_options_t child_opts = { + .name = "db.query", .resource = "SELECT *", .service = "postgres"}; + auto *child = dd_span_create_child(parent, &child_opts); + REQUIRE(child != nullptr); + + dd_span_finish(child); + dd_span_free(child); + dd_span_finish(parent); + dd_span_free(parent); + + // Spans are sent as a chunk in registration order: + // parent first, child second. + REQUIRE(ctx.collector->chunks.size() >= 1); + REQUIRE(ctx.collector->chunks[0].size() >= 2); + const auto &child_sd = *ctx.collector->chunks[0][1]; + CHECK(child_sd.name == "db.query"); + CHECK(child_sd.resource == "SELECT *"); + CHECK(child_sd.service == "postgres"); +} + +TEST_CASE("tracer new with invalid config and null error", "[c_binding]") { + // Invalid config + NULL error pointer should not crash. + auto *conf = dd_tracer_conf_new(); + dd_tracer_conf_set(conf, DD_OPT_AGENT_URL, (void *)"not://valid"); + + auto *tracer = dd_tracer_new(conf, nullptr); + CHECK(tracer == nullptr); + + dd_tracer_conf_free(conf); +} + +TEST_CASE("null arguments do not crash", "[c_binding]") { + // Functions that return handles should return nullptr. + CHECK(dd_tracer_new(nullptr, nullptr) == nullptr); + CHECK(dd_tracer_create_span(nullptr, nullptr) == nullptr); + CHECK(dd_span_create_child(nullptr, nullptr) == nullptr); + + char buf[trace_id_buf_size] = {}; + CHECK(dd_span_get_trace_id(nullptr, buf, sizeof(buf)) == -1); + CHECK(dd_span_get_span_id(nullptr, buf, sizeof(buf)) == -1); + + // Void functions with null handles should simply not crash. + dd_tracer_conf_free(nullptr); + dd_tracer_conf_set(nullptr, DD_OPT_SERVICE_NAME, (void *)"x"); + dd_tracer_free(nullptr); + dd_span_free(nullptr); + dd_span_set_tag(nullptr, "k", "v"); + dd_span_set_error(nullptr, 1); + dd_span_set_error_message(nullptr, "msg"); + dd_span_inject(nullptr, test_header_writer); + dd_span_finish(nullptr); + dd_span_set_resource(nullptr, "res"); + dd_span_set_service(nullptr, "svc"); +}