From 65c23aaf9be817c6df7198fbd9f00a1769a53a97 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 27 Jan 2026 17:10:25 +0100 Subject: [PATCH 01/23] WIP: feat: metrics Co-Authored-By: Claude Opus 4.5 --- examples/example.c | 69 ++++ include/sentry.h | 81 +++++ src/CMakeLists.txt | 2 + src/backends/sentry_backend_breakpad.cpp | 6 +- src/backends/sentry_backend_crashpad.cpp | 6 +- src/backends/sentry_backend_inproc.c | 6 +- src/sentry_core.c | 17 +- src/sentry_envelope.c | 34 ++ src/sentry_envelope.h | 6 + src/sentry_metrics.c | 180 +++++++++++ src/sentry_metrics.h | 33 ++ src/sentry_options.c | 20 ++ src/sentry_options.h | 3 + tests/__init__.py | 5 + tests/assertions.py | 32 ++ tests/test_integration_http.py | 283 +++++++++++++++++ tests/unit/CMakeLists.txt | 1 + tests/unit/test_metrics.c | 384 +++++++++++++++++++++++ tests/unit/tests.inc | 9 + 19 files changed, 1171 insertions(+), 6 deletions(-) create mode 100644 src/sentry_metrics.c create mode 100644 src/sentry_metrics.h create mode 100644 tests/unit/test_metrics.c diff --git a/examples/example.c b/examples/example.c index 87f8fdd19..b8493b97b 100644 --- a/examples/example.c +++ b/examples/example.c @@ -183,6 +183,25 @@ discarding_before_send_log_callback(sentry_value_t log, void *user_data) return log; } +static sentry_value_t +before_send_metric_callback(sentry_value_t metric, void *user_data) +{ + (void)user_data; + sentry_value_t attribute + = sentry_value_new_attribute(sentry_value_new_string("little"), NULL); + sentry_value_set_by_key(sentry_value_get_by_key(metric, "attributes"), + "coffeepot.size", attribute); + return metric; +} + +static sentry_value_t +discarding_before_send_metric_callback(sentry_value_t metric, void *user_data) +{ + (void)user_data; + sentry_value_decref(metric); + return sentry_value_new_null(); +} + // Test logger that outputs in a format the integration tests can parse static void test_logger_callback( @@ -504,6 +523,20 @@ main(int argc, char **argv) sentry_options_set_logs_with_attributes(options, true); } + if (has_arg(argc, argv, "enable-metrics")) { + sentry_options_set_enable_metrics(options, true); + } + + if (has_arg(argc, argv, "before-send-metric")) { + sentry_options_set_before_send_metric( + options, before_send_metric_callback, NULL); + } + + if (has_arg(argc, argv, "discarding-before-send-metric")) { + sentry_options_set_before_send_metric( + options, discarding_before_send_metric_callback, NULL); + } + if (0 != sentry_init(options)) { return EXIT_FAILURE; } @@ -605,6 +638,38 @@ main(int argc, char **argv) } } + if (sentry_options_get_enable_metrics(options)) { + if (has_arg(argc, argv, "capture-metric")) { + sentry_metrics_count( + "test.counter", 1.0, NULL, sentry_value_new_null()); + } + if (has_arg(argc, argv, "capture-metric-all-types")) { + sentry_metrics_count( + "test.counter", 1.0, NULL, sentry_value_new_null()); + sentry_metrics_gauge( + "test.gauge", 42.5, "percent", sentry_value_new_null()); + sentry_metrics_distribution("test.distribution", 123.456, + "millisecond", sentry_value_new_null()); + } + if (has_arg(argc, argv, "metric-with-attributes")) { + sentry_value_t attributes = sentry_value_new_object(); + sentry_value_t attr = sentry_value_new_attribute( + sentry_value_new_string("my_value"), NULL); + sentry_value_set_by_key(attributes, "my.custom.attribute", attr); + sentry_metrics_count( + "test.counter.with.attributes", 1.0, NULL, attributes); + } + if (has_arg(argc, argv, "metrics-timer")) { + for (int i = 0; i < 10; i++) { + sentry_metrics_count( + "batch.counter", 1.0, NULL, sentry_value_new_null()); + } + sleep_s(6); + sentry_metrics_count( + "post.sleep.counter", 1.0, NULL, sentry_value_new_null()); + } + } + if (!has_arg(argc, argv, "no-setup")) { sentry_set_transaction("test-transaction"); sentry_set_level(SENTRY_LEVEL_WARNING); @@ -852,6 +917,10 @@ main(int argc, char **argv) if (has_arg(argc, argv, "logs-scoped-transaction")) { sentry_log_debug("logging during scoped transaction event"); } + if (has_arg(argc, argv, "metrics-scoped-transaction")) { + sentry_metrics_count("scoped.transaction.metric", 1.0, NULL, + sentry_value_new_null()); + } } sentry_transaction_finish(tx); diff --git a/include/sentry.h b/include/sentry.h index a2ebe57bb..18ec3cb53 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -2126,6 +2126,87 @@ typedef sentry_value_t (*sentry_before_send_log_function_t)( SENTRY_EXPERIMENTAL_API void sentry_options_set_before_send_log( sentry_options_t *opts, sentry_before_send_log_function_t func, void *data); +/** + * Enables or disables the metrics feature. + * When disabled, all calls to `sentry_metrics_*()` are no-ops. + */ +SENTRY_EXPERIMENTAL_API void sentry_options_set_enable_metrics( + sentry_options_t *opts, int enable_metrics); +SENTRY_EXPERIMENTAL_API int sentry_options_get_enable_metrics( + const sentry_options_t *opts); + +/** + * Type of the `before_send_metric` callback. + * + * The callback takes ownership of the `metric` and should usually return + * that same metric. In case the metric should be discarded, the + * callback needs to call `sentry_value_decref` on the provided metric and + * return a `sentry_value_new_null()` instead. + */ +typedef sentry_value_t (*sentry_before_send_metric_function_t)( + sentry_value_t metric, void *user_data); + +/** + * Sets the `before_send_metric` callback. + */ +SENTRY_EXPERIMENTAL_API void sentry_options_set_before_send_metric( + sentry_options_t *opts, sentry_before_send_metric_function_t func, + void *data); + +/** + * Result type for metric operations. + * - Success means the metric was enqueued + * - Discard means the `before_send_metric` callback discarded the metric + * - Failed means the metric wasn't enqueued (buffers are full) + * - Disabled means metrics are disabled + */ +typedef enum { + SENTRY_METRICS_RESULT_SUCCESS = 0, + SENTRY_METRICS_RESULT_DISCARD = 1, + SENTRY_METRICS_RESULT_FAILED = 2, + SENTRY_METRICS_RESULT_DISABLED = 3 +} sentry_metrics_result_t; + +/** + * Metrics interface for recording application metrics. + * + * Metrics are buffered and sent in batches. Each metric includes: + * - name: Hierarchical name with dot separators (e.g., "api.requests") + * - value: The numeric value to record + * - unit: Optional measurement unit (e.g., "millisecond", "byte"), or NULL + * - attributes: Optional sentry_value_t object with custom attributes, or + * sentry_value_new_null(). Each attribute should be created with + * sentry_value_new_attribute(). + * + * Metrics are automatically associated with the current trace and span if + * available. Default attributes (environment, release, SDK info) are attached + * automatically. + */ + +/** + * Records a counter metric. Counters track incrementing values like + * request counts or error counts. + */ +SENTRY_EXPERIMENTAL_API sentry_metrics_result_t sentry_metrics_count( + const char *name, double value, const char *unit, + sentry_value_t attributes); + +/** + * Records a gauge metric. Gauges track values that can go up or down, + * like memory usage or active connections. + */ +SENTRY_EXPERIMENTAL_API sentry_metrics_result_t sentry_metrics_gauge( + const char *name, double value, const char *unit, + sentry_value_t attributes); + +/** + * Records a distribution metric. Distributions track the statistical + * distribution of values, useful for timing data and percentiles. + */ +SENTRY_EXPERIMENTAL_API sentry_metrics_result_t sentry_metrics_distribution( + const char *name, double value, const char *unit, + sentry_value_t attributes); + #ifdef SENTRY_PLATFORM_LINUX /** diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1f35e2614..3e0c6715e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -22,6 +22,8 @@ sentry_target_sources_cwd(sentry sentry_logger.h sentry_logs.c sentry_logs.h + sentry_metrics.c + sentry_metrics.h sentry_options.c sentry_options.h sentry_os.c diff --git a/src/backends/sentry_backend_breakpad.cpp b/src/backends/sentry_backend_breakpad.cpp index 86eb3bec4..4d515b381 100644 --- a/src/backends/sentry_backend_breakpad.cpp +++ b/src/backends/sentry_backend_breakpad.cpp @@ -9,6 +9,7 @@ extern "C" { #include "sentry_envelope.h" #include "sentry_logger.h" #include "sentry_logs.h" +#include "sentry_metrics.h" #include "sentry_options.h" #ifdef SENTRY_PLATFORM_WINDOWS # include "sentry_os.h" @@ -152,10 +153,13 @@ breakpad_backend_callback(const google_breakpad::MinidumpDescriptor &descriptor, should_handle = !sentry_value_is_null(result); } - // Flush logs in a crash-safe manner before crash handling + // Flush logs and metrics in a crash-safe manner before crash handling if (options->enable_logs) { sentry__logs_flush_crash_safe(); } + if (options->enable_metrics) { + sentry__metrics_flush_crash_safe(); + } if (should_handle) { sentry_envelope_t *envelope = sentry__prepare_event( diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index 781481359..58c0da65a 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -10,6 +10,7 @@ extern "C" { #include "sentry_envelope.h" #include "sentry_logger.h" #include "sentry_logs.h" +#include "sentry_metrics.h" #include "sentry_options.h" #ifdef SENTRY_PLATFORM_WINDOWS # include "sentry_os.h" @@ -356,10 +357,13 @@ sentry__crashpad_handler(int signum, siginfo_t *info, ucontext_t *user_context) crash_event, nullptr, options->before_send_data); } - // Flush logs in a crash-safe manner before crash handling + // Flush logs and metrics in a crash-safe manner before crash handling if (options->enable_logs) { sentry__logs_flush_crash_safe(); } + if (options->enable_metrics) { + sentry__metrics_flush_crash_safe(); + } should_dump = !sentry_value_is_null(crash_event); diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 3f02024e3..76c73c175 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -8,6 +8,7 @@ #include "sentry_envelope.h" #include "sentry_logger.h" #include "sentry_logs.h" +#include "sentry_metrics.h" #include "sentry_options.h" #if defined(SENTRY_PLATFORM_WINDOWS) # include "sentry_os.h" @@ -699,10 +700,13 @@ handle_ucontext(const sentry_ucontext_t *uctx) should_handle = !sentry_value_is_null(event); } - // Flush logs in a crash-safe manner before crash handling + // Flush logs and metrics in a crash-safe manner before crash handling if (options->enable_logs) { sentry__logs_flush_crash_safe(); } + if (options->enable_metrics) { + sentry__metrics_flush_crash_safe(); + } if (should_handle) { sentry_envelope_t *envelope = sentry__prepare_event( diff --git a/src/sentry_core.c b/src/sentry_core.c index 895629052..20c9bbba6 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -9,6 +9,7 @@ #include "sentry_database.h" #include "sentry_envelope.h" #include "sentry_logs.h" +#include "sentry_metrics.h" #include "sentry_options.h" #include "sentry_path.h" #include "sentry_process.h" @@ -296,6 +297,10 @@ sentry_init(sentry_options_t *options) sentry__logs_startup(); } + if (options->enable_metrics) { + sentry__metrics_startup(); + } + sentry__mutex_unlock(&g_options_lock); return 0; @@ -317,6 +322,9 @@ sentry_flush(uint64_t timeout) if (options->enable_logs) { sentry__logs_force_flush(); } + if (options->enable_metrics) { + sentry__metrics_force_flush(); + } rv = sentry__transport_flush(options->transport, timeout); } return rv; @@ -325,13 +333,16 @@ sentry_flush(uint64_t timeout) int sentry_close(void) { - // Shutdown logs system before locking options to ensure logs are flushed. - // This prevents a potential deadlock on the options during log envelope - // creation. + // Shutdown logs and metrics systems before locking options to ensure they + // are flushed. This prevents a potential deadlock on the options during + // envelope creation. SENTRY_WITH_OPTIONS (options) { if (options->enable_logs) { sentry__logs_shutdown(options->shutdown_timeout); } + if (options->enable_metrics) { + sentry__metrics_shutdown(options->shutdown_timeout); + } } SENTRY__MUTEX_INIT_DYN_ONCE(g_options_lock); diff --git a/src/sentry_envelope.c b/src/sentry_envelope.c index 72e6de221..64467fd8d 100644 --- a/src/sentry_envelope.c +++ b/src/sentry_envelope.c @@ -472,6 +472,40 @@ sentry__envelope_add_logs(sentry_envelope_t *envelope, sentry_value_t logs) return item; } +sentry_envelope_item_t * +sentry__envelope_add_metrics( + sentry_envelope_t *envelope, sentry_value_t metrics) +{ + sentry_envelope_item_t *item = envelope_add_item(envelope); + if (!item) { + return NULL; + } + + sentry_jsonwriter_t *jw = sentry__jsonwriter_new_sb(NULL); + if (!jw) { + return NULL; + } + + sentry__jsonwriter_write_value(jw, metrics); + item->payload = sentry__jsonwriter_into_string(jw, &item->payload_len); + if (!item->payload) { + return NULL; + } + + sentry__envelope_item_set_header( + item, "type", sentry_value_new_string("trace_metric")); + sentry__envelope_item_set_header(item, "item_count", + sentry_value_new_int32((int32_t)sentry_value_get_length( + sentry_value_get_by_key(metrics, "items")))); + sentry__envelope_item_set_header(item, "content_type", + sentry_value_new_string( + "application/vnd.sentry.items.trace-metric+json")); + sentry_value_t length = sentry_value_new_int32((int32_t)item->payload_len); + sentry__envelope_item_set_header(item, "length", length); + + return item; +} + sentry_envelope_item_t * sentry__envelope_add_user_report( sentry_envelope_t *envelope, sentry_value_t user_report) diff --git a/src/sentry_envelope.h b/src/sentry_envelope.h index 3cbddd401..0cae74e69 100644 --- a/src/sentry_envelope.h +++ b/src/sentry_envelope.h @@ -62,6 +62,12 @@ sentry_envelope_item_t *sentry__envelope_add_user_report( sentry_envelope_item_t *sentry__envelope_add_logs( sentry_envelope_t *envelope, sentry_value_t logs); +/** + * Add a list of metrics to this envelope. + */ +sentry_envelope_item_t *sentry__envelope_add_metrics( + sentry_envelope_t *envelope, sentry_value_t metrics); + /** * Add a user feedback to this envelope. */ diff --git a/src/sentry_metrics.c b/src/sentry_metrics.c new file mode 100644 index 000000000..9f638f76d --- /dev/null +++ b/src/sentry_metrics.c @@ -0,0 +1,180 @@ +#include "sentry_metrics.h" +#include "sentry_batcher.h" +#include "sentry_core.h" +#include "sentry_envelope.h" +#include "sentry_options.h" +#include "sentry_utils.h" +#include "sentry_value.h" + +typedef enum { + SENTRY_METRIC_COUNT, + SENTRY_METRIC_GAUGE, + SENTRY_METRIC_DISTRIBUTION, +} sentry_metric_type_t; + +static sentry_batcher_t g_batcher = { + { + { + .index = 0, + .adding = 0, + .sealed = 0, + }, + { + .index = 0, + .adding = 0, + .sealed = 0, + }, + }, + .active_idx = 0, + .flushing = 0, + .thread_state = SENTRY_BATCHER_THREAD_STOPPED, + .batch_func = sentry__envelope_add_metrics, +}; + +static const char * +metric_type_string(sentry_metric_type_t type) +{ + switch (type) { + case SENTRY_METRIC_COUNT: + return "counter"; + case SENTRY_METRIC_GAUGE: + return "gauge"; + case SENTRY_METRIC_DISTRIBUTION: + return "distribution"; + default: + return "unknown"; + } +} + +static sentry_value_t +construct_metric(sentry_metric_type_t type, const char *name, double value, + const char *unit, sentry_value_t user_attributes) +{ + sentry_value_t metric = sentry_value_new_object(); + + uint64_t usec_time = sentry__usec_time(); + sentry_value_set_by_key(metric, "timestamp", + sentry_value_new_double((double)usec_time / 1000000.0)); + sentry_value_set_by_key( + metric, "type", sentry_value_new_string(metric_type_string(type))); + sentry_value_set_by_key(metric, "name", sentry_value_new_string(name)); + sentry_value_set_by_key(metric, "value", sentry_value_new_double(value)); + if (unit && unit[0] != '\0') { + sentry_value_set_by_key(metric, "unit", sentry_value_new_string(unit)); + } + + sentry_value_t attributes = sentry_value_new_object(); + sentry__apply_attributes(metric, attributes); + if (sentry_value_get_type(user_attributes) == SENTRY_VALUE_TYPE_OBJECT + && sentry_value_get_length(user_attributes) > 0) { + sentry__value_merge_objects(attributes, user_attributes); + } + sentry_value_decref(user_attributes); + + if (sentry_value_get_length(attributes) > 0) { + sentry_value_set_by_key(metric, "attributes", attributes); + } else { + sentry_value_decref(attributes); + } + + return metric; +} + +static sentry_metrics_result_t +record_metric(sentry_metric_type_t type, const char *name, double value, + const char *unit, sentry_value_t attributes) +{ + bool enable_metrics = false; + SENTRY_WITH_OPTIONS (options) { + if (options->enable_metrics) + enable_metrics = true; + } + if (enable_metrics) { + bool discarded = false; + sentry_value_t metric + = construct_metric(type, name, value, unit, attributes); + SENTRY_WITH_OPTIONS (options) { + if (options->before_send_metric_func) { + metric = options->before_send_metric_func( + metric, options->before_send_metric_data); + if (sentry_value_is_null(metric)) { + SENTRY_DEBUG("metric was discarded by the " + "`before_send_metric` hook"); + discarded = true; + } + } + } + if (discarded) { + return SENTRY_METRICS_RESULT_DISCARD; + } + if (!sentry__batcher_enqueue(&g_batcher, metric)) { + sentry_value_decref(metric); + return SENTRY_METRICS_RESULT_FAILED; + } + return SENTRY_METRICS_RESULT_SUCCESS; + } + sentry_value_decref(attributes); + return SENTRY_METRICS_RESULT_DISABLED; +} + +sentry_metrics_result_t +sentry_metrics_count( + const char *name, double value, const char *unit, sentry_value_t attributes) +{ + return record_metric(SENTRY_METRIC_COUNT, name, value, unit, attributes); +} + +sentry_metrics_result_t +sentry_metrics_gauge( + const char *name, double value, const char *unit, sentry_value_t attributes) +{ + return record_metric(SENTRY_METRIC_GAUGE, name, value, unit, attributes); +} + +sentry_metrics_result_t +sentry_metrics_distribution( + const char *name, double value, const char *unit, sentry_value_t attributes) +{ + return record_metric( + SENTRY_METRIC_DISTRIBUTION, name, value, unit, attributes); +} + +void +sentry__metrics_startup(void) +{ + sentry__batcher_startup(&g_batcher, sentry__envelope_add_metrics); +} + +void +sentry__metrics_shutdown(uint64_t timeout) +{ + SENTRY_DEBUG("shutting down metrics system"); + sentry__batcher_shutdown(&g_batcher, timeout); + SENTRY_DEBUG("metrics system shutdown complete"); +} + +void +sentry__metrics_flush_crash_safe(void) +{ + SENTRY_DEBUG("crash-safe metrics flush"); + sentry__batcher_flush_crash_safe(&g_batcher); + SENTRY_DEBUG("crash-safe metrics flush complete"); +} + +void +sentry__metrics_force_flush(void) +{ + sentry__batcher_force_flush(&g_batcher); +} + +#ifdef SENTRY_UNITTEST +/** + * Wait for the metrics batching thread to be ready. + * This is a test-only helper to avoid race conditions in tests. + */ +void +sentry__metrics_wait_for_thread_startup(void) +{ + sentry__batcher_wait_for_thread_startup(&g_batcher); +} +#endif diff --git a/src/sentry_metrics.h b/src/sentry_metrics.h new file mode 100644 index 000000000..4f8b0b137 --- /dev/null +++ b/src/sentry_metrics.h @@ -0,0 +1,33 @@ +#ifndef SENTRY_METRICS_H_INCLUDED +#define SENTRY_METRICS_H_INCLUDED + +#include "sentry_boot.h" + +/** + * Sets up the metrics timer/flush thread + */ +void sentry__metrics_startup(void); + +/** + * Instructs the metrics timer/flush thread to shut down. + */ +void sentry__metrics_shutdown(uint64_t timeout); + +/** + * Crash-safe metrics flush that avoids thread synchronization. + * This should be used during crash handling to flush metrics without + * waiting for the batching thread to shut down cleanly. + */ +void sentry__metrics_flush_crash_safe(void); + +void sentry__metrics_force_flush(void); + +#ifdef SENTRY_UNITTEST +/** + * Wait for the metrics batching thread to be ready. + * This is a test-only helper to avoid race conditions in tests. + */ +void sentry__metrics_wait_for_thread_startup(void); +#endif + +#endif diff --git a/src/sentry_options.c b/src/sentry_options.c index b9b6ea4c6..0d71957e8 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -757,6 +757,26 @@ sentry_options_get_logs_with_attributes(const sentry_options_t *opts) return opts->logs_with_attributes; } +void +sentry_options_set_enable_metrics(sentry_options_t *opts, int enable_metrics) +{ + opts->enable_metrics = !!enable_metrics; +} + +int +sentry_options_get_enable_metrics(const sentry_options_t *opts) +{ + return opts->enable_metrics; +} + +void +sentry_options_set_before_send_metric(sentry_options_t *opts, + sentry_before_send_metric_function_t func, void *user_data) +{ + opts->before_send_metric_func = func; + opts->before_send_metric_data = user_data; +} + #ifdef SENTRY_PLATFORM_LINUX sentry_handler_strategy_t diff --git a/src/sentry_options.h b/src/sentry_options.h index 280703d40..9a035ebe2 100644 --- a/src/sentry_options.h +++ b/src/sentry_options.h @@ -68,6 +68,9 @@ struct sentry_options_s { // takes the first varg as a `sentry_value_t` object containing attributes // if no custom attributes are to be passed, use `sentry_value_new_object()` bool logs_with_attributes; + bool enable_metrics; + sentry_before_send_metric_function_t before_send_metric_func; + void *before_send_metric_data; /* everything from here on down are options which are stored here but not exposed through the options API */ diff --git a/tests/__init__.py b/tests/__init__.py index f4cbfe9ec..526ef6abd 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -58,6 +58,10 @@ def is_logs_envelope(data): return b'"type":"log"' in data +def is_metrics_envelope(data): + return b'"type":"trace_metric"' in data + + def is_feedback_envelope(data): return b'"type":"feedback"' in data @@ -346,6 +350,7 @@ def deserialize_from( "transaction", "user_report", "log", + "trace_metric", ]: rv = cls(headers=headers, payload=PayloadRef(json=json.loads(payload))) else: diff --git a/tests/assertions.py b/tests/assertions.py index 05d3ea320..ee3ef4886 100644 --- a/tests/assertions.py +++ b/tests/assertions.py @@ -274,6 +274,38 @@ def assert_logs(envelope, expected_item_count=1, expected_trace_id=None): assert log_item["trace_id"] == expected_trace_id +def assert_metrics(envelope, expected_item_count=1, expected_trace_id=None): + metrics = None + for item in envelope: + assert item.headers.get("type") == "trace_metric" + assert item.headers.get("item_count") >= expected_item_count + assert ( + item.headers.get("content_type") + == "application/vnd.sentry.items.trace-metric+json" + ) + metrics = item.payload.json + + assert isinstance(metrics, dict) + assert "items" in metrics + assert len(metrics["items"]) >= expected_item_count + for i in range(expected_item_count): + metric_item = metrics["items"][i] + assert "name" in metric_item + assert "type" in metric_item + assert metric_item["type"] in ["counter", "gauge", "distribution"] + assert "value" in metric_item + assert "timestamp" in metric_item + assert "trace_id" in metric_item + assert "attributes" in metric_item + attrs = metric_item["attributes"] + assert "sentry.environment" in attrs + assert "sentry.release" in attrs + assert "sentry.sdk.name" in attrs + assert "sentry.sdk.version" in attrs + if expected_trace_id: + assert metric_item["trace_id"] == expected_trace_id + + def assert_attachment_view_hierarchy(envelope): expected = { "type": "attachment", diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index 228b634d7..cbc15468b 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -15,8 +15,10 @@ run, Envelope, split_log_request_cond, + extract_request, is_feedback_envelope, is_logs_envelope, + is_metrics_envelope, SENTRY_VERSION, ) from .proxy import ( @@ -43,6 +45,7 @@ assert_failed_proxy_auth_request, assert_attachment_view_hierarchy, assert_logs, + assert_metrics, ) from .conditions import has_http, has_breakpad, has_files, is_kcov @@ -1801,3 +1804,283 @@ def test_logs_global_and_local_attributes_merge(cmake, httpserver): assert "global.attribute.array" in attributes_0 assert attributes_0["global.attribute.array"]["value"] == ["item1", "item2"] assert attributes_0["global.attribute.array"]["type"] == "array" + + +def test_metrics_capture(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) + + httpserver.expect_request( + "/api/123456/envelope/", + headers={"x-sentry-auth": auth_header}, + ).respond_with_data("OK") + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + + run( + tmp_path, + "sentry_example", + ["log", "enable-metrics", "capture-metric"], + env=env, + ) + + assert len(httpserver.log) == 1 + + req = httpserver.log[0][0] + body = req.get_data() + + envelope = Envelope.deserialize(body) + envelope.print_verbose() + assert_metrics(envelope, 1) + + +def test_metrics_all_types(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) + + httpserver.expect_request( + "/api/123456/envelope/", + headers={"x-sentry-auth": auth_header}, + ).respond_with_data("OK") + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + + run( + tmp_path, + "sentry_example", + ["log", "enable-metrics", "capture-metric-all-types"], + env=env, + ) + + assert len(httpserver.log) == 1 + + req = httpserver.log[0][0] + body = req.get_data() + + envelope = Envelope.deserialize(body) + envelope.print_verbose() + assert_metrics(envelope, 3) + + # Verify the different metric types + metrics = envelope.items[0].payload.json + types_found = {item["type"] for item in metrics["items"]} + assert types_found == {"counter", "gauge", "distribution"} + + +def test_metrics_with_custom_attributes(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) + + httpserver.expect_request( + "/api/123456/envelope/", + headers={"x-sentry-auth": auth_header}, + ).respond_with_data("OK") + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + + run( + tmp_path, + "sentry_example", + ["log", "enable-metrics", "metric-with-attributes"], + env=env, + ) + + assert len(httpserver.log) == 1 + + req = httpserver.log[0][0] + body = req.get_data() + + envelope = Envelope.deserialize(body) + envelope.print_verbose() + assert_metrics(envelope, 1) + + # Check custom attribute exists + metric_item = envelope.items[0].payload.json["items"][0] + attrs = metric_item["attributes"] + assert "my.custom.attribute" in attrs + assert attrs["my.custom.attribute"]["value"] == "my_value" + assert attrs["my.custom.attribute"]["type"] == "string" + + +def test_metrics_timer(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) + + httpserver.expect_request( + "/api/123456/envelope/", + headers={"x-sentry-auth": auth_header}, + ).respond_with_data("OK") + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + + run( + tmp_path, + "sentry_example", + ["log", "enable-metrics", "metrics-timer"], + env=env, + ) + + # We expect 2 envelopes: one from the timer flush (after sleep), one at shutdown + assert len(httpserver.log) == 2 + + req_0 = httpserver.log[0][0] + body_0 = req_0.get_data() + envelope_0 = Envelope.deserialize(body_0) + assert_metrics(envelope_0, 10) + + req_1 = httpserver.log[1][0] + body_1 = req_1.get_data() + envelope_1 = Envelope.deserialize(body_1) + assert_metrics(envelope_1, 1) + + +def test_metrics_scoped_transaction(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) + + httpserver.expect_request( + "/api/123456/envelope/", + headers={"x-sentry-auth": auth_header}, + ).respond_with_data("OK") + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + + run( + tmp_path, + "sentry_example", + [ + "log", + "enable-metrics", + "metrics-scoped-transaction", + "capture-transaction", + "scope-transaction-event", + ], + env=env, + ) + + # Event, transaction, and metrics + assert len(httpserver.log) == 3 + + event_req = httpserver.log[0][0] + event_body = event_req.get_data() + event_envelope = Envelope.deserialize(event_body) + assert_event(event_envelope) + + event_trace_id = event_envelope.items[0].payload.json["contexts"]["trace"][ + "trace_id" + ] + + tx_req = httpserver.log[1][0] + tx_body = tx_req.get_data() + tx_envelope = Envelope.deserialize(tx_body) + tx_trace_id = tx_envelope.items[0].payload.json["contexts"]["trace"]["trace_id"] + assert tx_trace_id == event_trace_id + + metrics_req = httpserver.log[2][0] + metrics_body = metrics_req.get_data() + metrics_envelope = Envelope.deserialize(metrics_body) + assert_metrics(metrics_envelope, 1, event_trace_id) + + +def test_before_send_metric(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) + + httpserver.expect_oneshot_request( + "/api/123456/envelope/", + headers={"x-sentry-auth": auth_header}, + ).respond_with_data("OK") + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + + run( + tmp_path, + "sentry_example", + ["log", "enable-metrics", "capture-metric", "before-send-metric"], + env=env, + ) + + assert len(httpserver.log) == 1 + req = httpserver.log[0][0] + body = req.get_data() + + envelope = Envelope.deserialize(body) + envelope.print_verbose() + + # Extract the metric item + (metric_item,) = envelope.items + + assert metric_item.headers["type"] == "trace_metric" + payload = metric_item.payload.json + + # Get the first metric item from the metrics payload + metric_entry = payload["items"][0] + attributes = metric_entry["attributes"] + + # Check that the before_send_metric callback added the expected attribute + assert "coffeepot.size" in attributes + assert attributes["coffeepot.size"]["value"] == "little" + assert attributes["coffeepot.size"]["type"] == "string" + + +def test_before_send_metric_discard(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) + + httpserver.expect_oneshot_request( + "/api/123456/envelope/", + headers={"x-sentry-auth": auth_header}, + ).respond_with_data("OK") + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + + run( + tmp_path, + "sentry_example", + ["log", "enable-metrics", "capture-metric", "discarding-before-send-metric"], + env=env, + ) + + # metric should have been discarded + assert len(httpserver.log) == 0 + + +def test_metrics_disabled(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) + + httpserver.expect_request( + "/api/123456/envelope/", + headers={"x-sentry-auth": auth_header}, + ).respond_with_data("OK") + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + + # Run without enable-metrics flag + run( + tmp_path, + "sentry_example", + ["log", "capture-metric"], + env=env, + ) + + # No metrics should be sent when feature is disabled + assert len(httpserver.log) == 0 + + +def test_metrics_event(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) + + httpserver.expect_request( + "/api/123456/envelope/", + headers={"x-sentry-auth": auth_header}, + ).respond_with_data("OK") + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + + run( + tmp_path, + "sentry_example", + ["log", "enable-metrics", "capture-metric", "capture-event"], + env=env, + ) + + assert len(httpserver.log) == 2 + + event_req = httpserver.log[0][0] + event_body = event_req.get_data() + event_envelope = Envelope.deserialize(event_body) + assert_event(event_envelope) + + # ensure that the event and the metric are part of the same trace + event_trace_id = event_envelope.items[0].payload.json["contexts"]["trace"][ + "trace_id" + ] + + metrics_req = httpserver.log[1][0] + metrics_body = metrics_req.get_data() + metrics_envelope = Envelope.deserialize(metrics_body) + assert_metrics(metrics_envelope, 1, event_trace_id) diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index b6c8dc0fc..4f32fdac8 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -32,6 +32,7 @@ add_executable(sentry_test_unit test_info.c test_logger.c test_logs.c + test_metrics.c test_modulefinder.c test_mpack.c test_options.c diff --git a/tests/unit/test_metrics.c b/tests/unit/test_metrics.c new file mode 100644 index 000000000..2e4199821 --- /dev/null +++ b/tests/unit/test_metrics.c @@ -0,0 +1,384 @@ +#include "sentry_metrics.h" +#include "sentry_testsupport.h" + +#include "sentry_envelope.h" +#include + +#ifdef SENTRY_PLATFORM_WINDOWS +# include +# define sleep_ms(MILLISECONDS) Sleep(MILLISECONDS) +#else +# include +# define sleep_ms(MILLISECONDS) usleep(MILLISECONDS * 1000) +#endif + +typedef struct { + uint64_t called_count; + bool has_validation_error; +} transport_validation_data_t; + +static void +validate_metrics_envelope(sentry_envelope_t *envelope, void *data) +{ + transport_validation_data_t *validation_data = data; + + // Check we have at least one envelope item + if (sentry__envelope_get_item_count(envelope) == 0) { + validation_data->has_validation_error = true; + sentry_envelope_free(envelope); + return; + } + + // Get the first item and check its type + const sentry_envelope_item_t *item = sentry__envelope_get_item(envelope, 0); + sentry_value_t type_header = sentry__envelope_item_get_header(item, "type"); + const char *type = sentry_value_as_string(type_header); + + // Only validate and count metric envelopes, skip others (e.g., session) + if (strcmp(type, "trace_metric") == 0) { + validation_data->called_count += 1; + } + + sentry_envelope_free(envelope); +} + +SENTRY_TEST(metrics_count) +{ + transport_validation_data_t validation_data = { 0, false }; + + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_enable_metrics(options, true); + + sentry_transport_t *transport + = sentry_transport_new(validate_metrics_envelope); + sentry_transport_set_state(transport, &validation_data); + sentry_options_set_transport(options, transport); + + sentry_init(options); + sentry__metrics_wait_for_thread_startup(); + + // Record a counter metric + TEST_CHECK_INT_EQUAL( + sentry_metrics_count("test.counter", 1, NULL, sentry_value_new_null()), + SENTRY_METRICS_RESULT_SUCCESS); + + sentry_close(); + + TEST_CHECK(!validation_data.has_validation_error); + TEST_CHECK_INT_EQUAL(validation_data.called_count, 1); +} + +SENTRY_TEST(metrics_gauge) +{ + transport_validation_data_t validation_data = { 0, false }; + + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_enable_metrics(options, true); + + sentry_transport_t *transport + = sentry_transport_new(validate_metrics_envelope); + sentry_transport_set_state(transport, &validation_data); + sentry_options_set_transport(options, transport); + + sentry_init(options); + sentry__metrics_wait_for_thread_startup(); + + // Record a gauge metric + TEST_CHECK_INT_EQUAL(sentry_metrics_gauge("test.gauge", 42.5, "percent", + sentry_value_new_null()), + SENTRY_METRICS_RESULT_SUCCESS); + + sentry_close(); + + TEST_CHECK(!validation_data.has_validation_error); + TEST_CHECK_INT_EQUAL(validation_data.called_count, 1); +} + +SENTRY_TEST(metrics_distribution) +{ + transport_validation_data_t validation_data = { 0, false }; + + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_enable_metrics(options, true); + + sentry_transport_t *transport + = sentry_transport_new(validate_metrics_envelope); + sentry_transport_set_state(transport, &validation_data); + sentry_options_set_transport(options, transport); + + sentry_init(options); + sentry__metrics_wait_for_thread_startup(); + + // Record a distribution metric + TEST_CHECK_INT_EQUAL(sentry_metrics_distribution("test.distribution", + 123.456, "millisecond", sentry_value_new_null()), + SENTRY_METRICS_RESULT_SUCCESS); + + sentry_close(); + + TEST_CHECK(!validation_data.has_validation_error); + TEST_CHECK_INT_EQUAL(validation_data.called_count, 1); +} + +SENTRY_TEST(metrics_with_attributes) +{ + transport_validation_data_t validation_data = { 0, false }; + + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_enable_metrics(options, true); + + sentry_transport_t *transport + = sentry_transport_new(validate_metrics_envelope); + sentry_transport_set_state(transport, &validation_data); + sentry_options_set_transport(options, transport); + + sentry_init(options); + sentry__metrics_wait_for_thread_startup(); + + // Record a metric with custom attributes + sentry_value_t attributes = sentry_value_new_object(); + sentry_value_set_by_key( + attributes, "environment", sentry_value_new_string("production")); + sentry_value_set_by_key( + attributes, "service", sentry_value_new_string("api")); + + TEST_CHECK_INT_EQUAL( + sentry_metrics_count("requests.total", 1, NULL, attributes), + SENTRY_METRICS_RESULT_SUCCESS); + + sentry_close(); + + TEST_CHECK(!validation_data.has_validation_error); + TEST_CHECK_INT_EQUAL(validation_data.called_count, 1); +} + +static sentry_value_t +before_send_metric_discard(sentry_value_t metric, void *data) +{ + (void)data; + sentry_value_decref(metric); + return sentry_value_new_null(); +} + +SENTRY_TEST(metrics_before_send_discard) +{ + transport_validation_data_t validation_data = { 0, false }; + + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_enable_metrics(options, true); + sentry_options_set_before_send_metric( + options, before_send_metric_discard, NULL); + + sentry_transport_t *transport + = sentry_transport_new(validate_metrics_envelope); + sentry_transport_set_state(transport, &validation_data); + sentry_options_set_transport(options, transport); + + sentry_init(options); + sentry__metrics_wait_for_thread_startup(); + + // This metric should be discarded by the before_send hook + TEST_CHECK_INT_EQUAL( + sentry_metrics_count("test.counter", 1, NULL, sentry_value_new_null()), + SENTRY_METRICS_RESULT_DISCARD); + + sentry_close(); + + // Transport should not be called since the metric was discarded + TEST_CHECK(!validation_data.has_validation_error); + TEST_CHECK_INT_EQUAL(validation_data.called_count, 0); +} + +static sentry_value_t +before_send_metric_modify(sentry_value_t metric, void *data) +{ + (void)data; + // Modify the metric by adding a custom attribute + sentry_value_t attributes = sentry_value_get_by_key(metric, "attributes"); + if (sentry_value_is_null(attributes)) { + attributes = sentry_value_new_object(); + sentry_value_set_by_key(metric, "attributes", attributes); + } + sentry_value_set_by_key( + attributes, "modified", sentry_value_new_bool(true)); + return metric; +} + +SENTRY_TEST(metrics_before_send_modify) +{ + transport_validation_data_t validation_data = { 0, false }; + + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_enable_metrics(options, true); + sentry_options_set_before_send_metric( + options, before_send_metric_modify, NULL); + + sentry_transport_t *transport + = sentry_transport_new(validate_metrics_envelope); + sentry_transport_set_state(transport, &validation_data); + sentry_options_set_transport(options, transport); + + sentry_init(options); + sentry__metrics_wait_for_thread_startup(); + + // This metric should be modified by the before_send hook + TEST_CHECK_INT_EQUAL( + sentry_metrics_count("test.counter", 1, NULL, sentry_value_new_null()), + SENTRY_METRICS_RESULT_SUCCESS); + + sentry_close(); + + TEST_CHECK(!validation_data.has_validation_error); + TEST_CHECK_INT_EQUAL(validation_data.called_count, 1); +} + +SENTRY_TEST(metrics_disabled) +{ + transport_validation_data_t validation_data = { 0, false }; + + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + // Metrics are disabled by default + + sentry_transport_t *transport + = sentry_transport_new(validate_metrics_envelope); + sentry_transport_set_state(transport, &validation_data); + sentry_options_set_transport(options, transport); + + sentry_init(options); + + // These should return DISABLED since metrics are not enabled + TEST_CHECK_INT_EQUAL( + sentry_metrics_count("test.counter", 1, NULL, sentry_value_new_null()), + SENTRY_METRICS_RESULT_DISABLED); + TEST_CHECK_INT_EQUAL( + sentry_metrics_gauge("test.gauge", 42.5, NULL, sentry_value_new_null()), + SENTRY_METRICS_RESULT_DISABLED); + TEST_CHECK_INT_EQUAL(sentry_metrics_distribution("test.distribution", 123.0, + NULL, sentry_value_new_null()), + SENTRY_METRICS_RESULT_DISABLED); + + sentry_close(); + + // Transport should not be called since metrics are disabled + TEST_CHECK(!validation_data.has_validation_error); + TEST_CHECK_INT_EQUAL(validation_data.called_count, 0); +} + +SENTRY_TEST(metrics_force_flush) +{ + transport_validation_data_t validation_data = { 0, false }; + + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_enable_metrics(options, true); + + sentry_transport_t *transport + = sentry_transport_new(validate_metrics_envelope); + sentry_transport_set_state(transport, &validation_data); + sentry_options_set_transport(options, transport); + + sentry_init(options); + sentry__metrics_wait_for_thread_startup(); + + // Record multiple metrics with force flush between each + TEST_CHECK_INT_EQUAL( + sentry_metrics_count("counter.1", 1, NULL, sentry_value_new_null()), + SENTRY_METRICS_RESULT_SUCCESS); + sentry_flush(5000); + TEST_CHECK_INT_EQUAL( + sentry_metrics_gauge("gauge.1", 42.5, NULL, sentry_value_new_null()), + SENTRY_METRICS_RESULT_SUCCESS); + sentry_flush(5000); + TEST_CHECK_INT_EQUAL(sentry_metrics_distribution("dist.1", 100.0, + "millisecond", sentry_value_new_null()), + SENTRY_METRICS_RESULT_SUCCESS); + sentry_flush(5000); + + sentry_close(); + + TEST_CHECK(!validation_data.has_validation_error); + TEST_CHECK_INT_EQUAL(validation_data.called_count, 3); +} + +static sentry_value_t g_captured_metric = { 0 }; + +static sentry_value_t +capture_metric(sentry_value_t metric, void *data) +{ + (void)data; + g_captured_metric = metric; + sentry_value_incref(metric); + return metric; +} + +static void +discard_envelope(sentry_envelope_t *envelope, void *data) +{ + (void)data; + sentry_envelope_free(envelope); +} + +SENTRY_TEST(metrics_default_attributes) +{ + g_captured_metric = sentry_value_new_null(); + + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_enable_metrics(options, true); + sentry_options_set_environment(options, "test-env"); + sentry_options_set_release(options, "1.0.0"); + sentry_options_set_before_send_metric(options, capture_metric, NULL); + + sentry_transport_t *transport = sentry_transport_new(discard_envelope); + sentry_options_set_transport(options, transport); + + sentry_init(options); + sentry__metrics_wait_for_thread_startup(); + + sentry_metrics_count("test.metric", 1, NULL, sentry_value_new_null()); + sentry_close(); + + // Validate trace_id is set directly on metric + sentry_value_t trace_id + = sentry_value_get_by_key(g_captured_metric, "trace_id"); + TEST_CHECK(!sentry_value_is_null(trace_id)); + + // Validate attributes object exists + sentry_value_t attributes + = sentry_value_get_by_key(g_captured_metric, "attributes"); + TEST_CHECK(!sentry_value_is_null(attributes)); + + // Validate default attributes are present with typed format + sentry_value_t env + = sentry_value_get_by_key(attributes, "sentry.environment"); + TEST_CHECK(!sentry_value_is_null(env)); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(env, "value")), + "test-env"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(env, "type")), "string"); + + sentry_value_t release + = sentry_value_get_by_key(attributes, "sentry.release"); + TEST_CHECK(!sentry_value_is_null(release)); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(release, "value")), + "1.0.0"); + + sentry_value_t sdk_name + = sentry_value_get_by_key(attributes, "sentry.sdk.name"); + TEST_CHECK(!sentry_value_is_null(sdk_name)); + + sentry_value_t sdk_version + = sentry_value_get_by_key(attributes, "sentry.sdk.version"); + TEST_CHECK(!sentry_value_is_null(sdk_version)); + + sentry_value_decref(g_captured_metric); +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 43f531a52..7de9a1f85 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -94,6 +94,15 @@ XX(logs_force_flush) XX(logs_param_conversion) XX(logs_param_types) XX(message_with_null_text_is_valid) +XX(metrics_before_send_discard) +XX(metrics_before_send_modify) +XX(metrics_count) +XX(metrics_default_attributes) +XX(metrics_disabled) +XX(metrics_distribution) +XX(metrics_force_flush) +XX(metrics_gauge) +XX(metrics_with_attributes) XX(module_addr) XX(module_finder) XX(mpack_newlines) From a73c3bc27b8c748e2a3df3a0f061634a6501373e Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 29 Jan 2026 13:09:46 +0100 Subject: [PATCH 02/23] docs: add changelog entry for metrics feature Co-Authored-By: Claude Opus 4.5 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22b846172..2b8c75f64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +**Features**: + +- Add support for metrics. It is currently experimental, and one can enable it by setting `sentry_options_set_enable_metrics`. When enabled, you can record a metric using `sentry_metrics_count()`, `sentry_metrics_gauge()`, or `sentry_metrics_distribution()`. Metrics can be filtered by setting the `before_send_metric` hook. ([#1498](https://github.com/getsentry/sentry-native/pull/1498)) + ## 0.12.4 **Fixes**: From 875c3d9f177afda1b2db789351b38f1a50e17c21 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 29 Jan 2026 13:17:58 +0100 Subject: [PATCH 03/23] docs: add metrics to attribute documentation Co-Authored-By: Claude Opus 4.5 --- include/sentry.h | 1 + 1 file changed, 1 insertion(+) diff --git a/include/sentry.h b/include/sentry.h index 18ec3cb53..37190271e 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -1863,6 +1863,7 @@ SENTRY_API void sentry_remove_extra_n(const char *key, size_t key_len); * Sets attributes created with `sentry_value_new_attribute` to be applied to * all: * - logs + * - metrics */ SENTRY_API void sentry_set_attribute(const char *key, sentry_value_t attribute); SENTRY_API void sentry_set_attribute_n( From 3c07fa5323bea8678fd12d5eff2676ff9484b24c Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 29 Jan 2026 17:18:38 +0100 Subject: [PATCH 04/23] ref(metrics): change sentry_metrics_count to use int64_t Per the Sentry metrics specification, counter metrics should use 64-bit signed integers. This is consistent with sentry_value_new_int64 / sentry_value_as_int64 in sentry.h. Also refactored internal record_metric to pass sentry_value_t instead of double, allowing proper type preservation. Co-Authored-By: Claude Opus 4.5 --- examples/example.c | 12 ++++++------ include/sentry.h | 2 +- src/sentry_metrics.c | 22 ++++++++++++---------- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/examples/example.c b/examples/example.c index b8493b97b..076f1e6fa 100644 --- a/examples/example.c +++ b/examples/example.c @@ -641,11 +641,11 @@ main(int argc, char **argv) if (sentry_options_get_enable_metrics(options)) { if (has_arg(argc, argv, "capture-metric")) { sentry_metrics_count( - "test.counter", 1.0, NULL, sentry_value_new_null()); + "test.counter", 1, NULL, sentry_value_new_null()); } if (has_arg(argc, argv, "capture-metric-all-types")) { sentry_metrics_count( - "test.counter", 1.0, NULL, sentry_value_new_null()); + "test.counter", 1, NULL, sentry_value_new_null()); sentry_metrics_gauge( "test.gauge", 42.5, "percent", sentry_value_new_null()); sentry_metrics_distribution("test.distribution", 123.456, @@ -657,16 +657,16 @@ main(int argc, char **argv) sentry_value_new_string("my_value"), NULL); sentry_value_set_by_key(attributes, "my.custom.attribute", attr); sentry_metrics_count( - "test.counter.with.attributes", 1.0, NULL, attributes); + "test.counter.with.attributes", 1, NULL, attributes); } if (has_arg(argc, argv, "metrics-timer")) { for (int i = 0; i < 10; i++) { sentry_metrics_count( - "batch.counter", 1.0, NULL, sentry_value_new_null()); + "batch.counter", 1, NULL, sentry_value_new_null()); } sleep_s(6); sentry_metrics_count( - "post.sleep.counter", 1.0, NULL, sentry_value_new_null()); + "post.sleep.counter", 1, NULL, sentry_value_new_null()); } } @@ -918,7 +918,7 @@ main(int argc, char **argv) sentry_log_debug("logging during scoped transaction event"); } if (has_arg(argc, argv, "metrics-scoped-transaction")) { - sentry_metrics_count("scoped.transaction.metric", 1.0, NULL, + sentry_metrics_count("scoped.transaction.metric", 1, NULL, sentry_value_new_null()); } } diff --git a/include/sentry.h b/include/sentry.h index 37190271e..c62ea33a0 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -2189,7 +2189,7 @@ typedef enum { * request counts or error counts. */ SENTRY_EXPERIMENTAL_API sentry_metrics_result_t sentry_metrics_count( - const char *name, double value, const char *unit, + const char *name, int64_t value, const char *unit, sentry_value_t attributes); /** diff --git a/src/sentry_metrics.c b/src/sentry_metrics.c index 9f638f76d..24647bd4f 100644 --- a/src/sentry_metrics.c +++ b/src/sentry_metrics.c @@ -47,8 +47,8 @@ metric_type_string(sentry_metric_type_t type) } static sentry_value_t -construct_metric(sentry_metric_type_t type, const char *name, double value, - const char *unit, sentry_value_t user_attributes) +construct_metric(sentry_metric_type_t type, const char *name, + sentry_value_t value, const char *unit, sentry_value_t user_attributes) { sentry_value_t metric = sentry_value_new_object(); @@ -58,7 +58,7 @@ construct_metric(sentry_metric_type_t type, const char *name, double value, sentry_value_set_by_key( metric, "type", sentry_value_new_string(metric_type_string(type))); sentry_value_set_by_key(metric, "name", sentry_value_new_string(name)); - sentry_value_set_by_key(metric, "value", sentry_value_new_double(value)); + sentry_value_set_by_key(metric, "value", value); if (unit && unit[0] != '\0') { sentry_value_set_by_key(metric, "unit", sentry_value_new_string(unit)); } @@ -81,7 +81,7 @@ construct_metric(sentry_metric_type_t type, const char *name, double value, } static sentry_metrics_result_t -record_metric(sentry_metric_type_t type, const char *name, double value, +record_metric(sentry_metric_type_t type, const char *name, sentry_value_t value, const char *unit, sentry_value_t attributes) { bool enable_metrics = false; @@ -118,25 +118,27 @@ record_metric(sentry_metric_type_t type, const char *name, double value, } sentry_metrics_result_t -sentry_metrics_count( - const char *name, double value, const char *unit, sentry_value_t attributes) +sentry_metrics_count(const char *name, int64_t value, const char *unit, + sentry_value_t attributes) { - return record_metric(SENTRY_METRIC_COUNT, name, value, unit, attributes); + return record_metric(SENTRY_METRIC_COUNT, name, + sentry_value_new_int64(value), unit, attributes); } sentry_metrics_result_t sentry_metrics_gauge( const char *name, double value, const char *unit, sentry_value_t attributes) { - return record_metric(SENTRY_METRIC_GAUGE, name, value, unit, attributes); + return record_metric(SENTRY_METRIC_GAUGE, name, + sentry_value_new_double(value), unit, attributes); } sentry_metrics_result_t sentry_metrics_distribution( const char *name, double value, const char *unit, sentry_value_t attributes) { - return record_metric( - SENTRY_METRIC_DISTRIBUTION, name, value, unit, attributes); + return record_metric(SENTRY_METRIC_DISTRIBUTION, name, + sentry_value_new_double(value), unit, attributes); } void From ccc3870b12edddfad312aa295fc4dffac643e768 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 29 Jan 2026 17:31:33 +0100 Subject: [PATCH 05/23] docs(metrics): document attributes ownership transfer Co-Authored-By: Claude Opus 4.5 --- include/sentry.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/include/sentry.h b/include/sentry.h index 475817c48..4b61ca65d 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -2179,6 +2179,11 @@ typedef enum { * sentry_value_new_null(). Each attribute should be created with * sentry_value_new_attribute(). * + * Ownership of the attributes is transferred to the metric function. + * + * To re-use the same attributes, call `sentry_value_incref` on it + * before passing the attributes to the metric function. + * * Metrics are automatically associated with the current trace and span if * available. Default attributes (environment, release, SDK info) are attached * automatically. From 52ff5d65ef5ffbcdb914d9360126e3a760088c9b Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 29 Jan 2026 19:06:48 +0100 Subject: [PATCH 06/23] fix(metrics): fix memory leak when metrics disabled The value parameter was not being decremented when metrics were disabled, causing a memory leak. Co-Authored-By: Claude Opus 4.5 --- src/sentry_metrics.c | 1 + tests/unit/tests.inc | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/sentry_metrics.c b/src/sentry_metrics.c index 24647bd4f..3e9198c63 100644 --- a/src/sentry_metrics.c +++ b/src/sentry_metrics.c @@ -113,6 +113,7 @@ record_metric(sentry_metric_type_t type, const char *name, sentry_value_t value, } return SENTRY_METRICS_RESULT_SUCCESS; } + sentry_value_decref(value); sentry_value_decref(attributes); return SENTRY_METRICS_RESULT_DISABLED; } diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 6756e7966..f47494207 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -78,12 +78,12 @@ XX(embedded_info_sentry_version) XX(empty_transport) XX(event_with_id) XX(exception_without_type_or_value_still_valid) -XX(formatted_log_messages) -XX(feedback_without_hint) -XX(feedback_with_null_hint) -XX(feedback_with_file_attachment) XX(feedback_with_bytes_attachment) +XX(feedback_with_file_attachment) XX(feedback_with_multiple_attachments) +XX(feedback_with_null_hint) +XX(feedback_without_hint) +XX(formatted_log_messages) XX(fuzz_json) XX(init_failure) XX(internal_uuid_api) From f8eb035f72f9d69d0575855602c63599d4edb778 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 30 Jan 2026 09:37:27 +0100 Subject: [PATCH 07/23] feat(metrics): add SENTRY_UNIT_* constants for telemetry units SDKs "should offer constants or similar that help customers send in units we support" as specified in the developer docs. See: - https://develop.sentry.dev/sdk/telemetry/metrics/ - https://develop.sentry.dev/sdk/telemetry/attributes/#units Co-Authored-By: Claude Opus 4.5 --- examples/example.c | 6 +++--- include/sentry.h | 41 ++++++++++++++++++++++++++++++++++++++- tests/unit/test_metrics.c | 11 ++++++----- 3 files changed, 49 insertions(+), 9 deletions(-) diff --git a/examples/example.c b/examples/example.c index 678aeb2b4..a85b54001 100644 --- a/examples/example.c +++ b/examples/example.c @@ -646,10 +646,10 @@ main(int argc, char **argv) if (has_arg(argc, argv, "capture-metric-all-types")) { sentry_metrics_count( "test.counter", 1, NULL, sentry_value_new_null()); - sentry_metrics_gauge( - "test.gauge", 42.5, "percent", sentry_value_new_null()); + sentry_metrics_gauge("test.gauge", 42.5, SENTRY_UNIT_PERCENT, + sentry_value_new_null()); sentry_metrics_distribution("test.distribution", 123.456, - "millisecond", sentry_value_new_null()); + SENTRY_UNIT_MILLISECOND, sentry_value_new_null()); } if (has_arg(argc, argv, "metric-with-attributes")) { sentry_value_t attributes = sentry_value_new_object(); diff --git a/include/sentry.h b/include/sentry.h index 4b61ca65d..a7ed916b2 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -324,6 +324,45 @@ SENTRY_API sentry_value_t sentry_value_new_user_n(const char *id, size_t id_len, const char *username, size_t username_len, const char *email, size_t email_len, const char *ip_address, size_t ip_address_len); +/** + * Measurement units for telemetry. + * + * These constants represent the standardized units supported by Sentry. + * Custom units can also be passed as arbitrary strings. + * + * See: https://develop.sentry.dev/sdk/telemetry/attributes/#units + */ + +/* Duration units */ +#define SENTRY_UNIT_NANOSECOND "nanosecond" +#define SENTRY_UNIT_MICROSECOND "microsecond" +#define SENTRY_UNIT_MILLISECOND "millisecond" +#define SENTRY_UNIT_SECOND "second" +#define SENTRY_UNIT_MINUTE "minute" +#define SENTRY_UNIT_HOUR "hour" +#define SENTRY_UNIT_DAY "day" +#define SENTRY_UNIT_WEEK "week" + +/* Information units */ +#define SENTRY_UNIT_BIT "bit" +#define SENTRY_UNIT_BYTE "byte" +#define SENTRY_UNIT_KILOBYTE "kilobyte" +#define SENTRY_UNIT_KIBIBYTE "kibibyte" +#define SENTRY_UNIT_MEGABYTE "megabyte" +#define SENTRY_UNIT_MEBIBYTE "mebibyte" +#define SENTRY_UNIT_GIGABYTE "gigabyte" +#define SENTRY_UNIT_GIBIBYTE "gibibyte" +#define SENTRY_UNIT_TERABYTE "terabyte" +#define SENTRY_UNIT_TEBIBYTE "tebibyte" +#define SENTRY_UNIT_PETABYTE "petabyte" +#define SENTRY_UNIT_PEBIBYTE "pebibyte" +#define SENTRY_UNIT_EXABYTE "exabyte" +#define SENTRY_UNIT_EXBIBYTE "exbibyte" + +/* Fraction units */ +#define SENTRY_UNIT_RATIO "ratio" +#define SENTRY_UNIT_PERCENT "percent" + /** * Creates a new attribute object. * value is required, unit is optional. @@ -2174,7 +2213,7 @@ typedef enum { * Metrics are buffered and sent in batches. Each metric includes: * - name: Hierarchical name with dot separators (e.g., "api.requests") * - value: The numeric value to record - * - unit: Optional measurement unit (e.g., "millisecond", "byte"), or NULL + * - unit: Optional measurement unit (e.g., SENTRY_UNIT_MILLISECOND), or NULL * - attributes: Optional sentry_value_t object with custom attributes, or * sentry_value_new_null(). Each attribute should be created with * sentry_value_new_attribute(). diff --git a/tests/unit/test_metrics.c b/tests/unit/test_metrics.c index 2e4199821..bc0b7ccb7 100644 --- a/tests/unit/test_metrics.c +++ b/tests/unit/test_metrics.c @@ -86,8 +86,8 @@ SENTRY_TEST(metrics_gauge) sentry__metrics_wait_for_thread_startup(); // Record a gauge metric - TEST_CHECK_INT_EQUAL(sentry_metrics_gauge("test.gauge", 42.5, "percent", - sentry_value_new_null()), + TEST_CHECK_INT_EQUAL(sentry_metrics_gauge("test.gauge", 42.5, + SENTRY_UNIT_PERCENT, sentry_value_new_null()), SENTRY_METRICS_RESULT_SUCCESS); sentry_close(); @@ -113,8 +113,9 @@ SENTRY_TEST(metrics_distribution) sentry__metrics_wait_for_thread_startup(); // Record a distribution metric - TEST_CHECK_INT_EQUAL(sentry_metrics_distribution("test.distribution", - 123.456, "millisecond", sentry_value_new_null()), + TEST_CHECK_INT_EQUAL( + sentry_metrics_distribution("test.distribution", 123.456, + SENTRY_UNIT_MILLISECOND, sentry_value_new_null()), SENTRY_METRICS_RESULT_SUCCESS); sentry_close(); @@ -297,7 +298,7 @@ SENTRY_TEST(metrics_force_flush) SENTRY_METRICS_RESULT_SUCCESS); sentry_flush(5000); TEST_CHECK_INT_EQUAL(sentry_metrics_distribution("dist.1", 100.0, - "millisecond", sentry_value_new_null()), + SENTRY_UNIT_MILLISECOND, sentry_value_new_null()), SENTRY_METRICS_RESULT_SUCCESS); sentry_flush(5000); From 941a1fa86dc9ef7719c21feda5199724b6835ed5 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 30 Jan 2026 09:40:32 +0100 Subject: [PATCH 08/23] ref(metrics): remove unit parameter from sentry_metrics_count Per the developer docs, units are only used for distribution and gauge metrics, not counters. Co-Authored-By: Claude Opus 4.5 --- examples/example.c | 17 +++++++---------- include/sentry.h | 3 +-- src/sentry_metrics.c | 5 ++--- tests/unit/test_metrics.c | 15 +++++++-------- 4 files changed, 17 insertions(+), 23 deletions(-) diff --git a/examples/example.c b/examples/example.c index a85b54001..ab72ee1f1 100644 --- a/examples/example.c +++ b/examples/example.c @@ -640,12 +640,10 @@ main(int argc, char **argv) if (sentry_options_get_enable_metrics(options)) { if (has_arg(argc, argv, "capture-metric")) { - sentry_metrics_count( - "test.counter", 1, NULL, sentry_value_new_null()); + sentry_metrics_count("test.counter", 1, sentry_value_new_null()); } if (has_arg(argc, argv, "capture-metric-all-types")) { - sentry_metrics_count( - "test.counter", 1, NULL, sentry_value_new_null()); + sentry_metrics_count("test.counter", 1, sentry_value_new_null()); sentry_metrics_gauge("test.gauge", 42.5, SENTRY_UNIT_PERCENT, sentry_value_new_null()); sentry_metrics_distribution("test.distribution", 123.456, @@ -656,17 +654,16 @@ main(int argc, char **argv) sentry_value_t attr = sentry_value_new_attribute( sentry_value_new_string("my_value"), NULL); sentry_value_set_by_key(attributes, "my.custom.attribute", attr); - sentry_metrics_count( - "test.counter.with.attributes", 1, NULL, attributes); + sentry_metrics_count("test.counter.with.attributes", 1, attributes); } if (has_arg(argc, argv, "metrics-timer")) { for (int i = 0; i < 10; i++) { sentry_metrics_count( - "batch.counter", 1, NULL, sentry_value_new_null()); + "batch.counter", 1, sentry_value_new_null()); } sleep_s(6); sentry_metrics_count( - "post.sleep.counter", 1, NULL, sentry_value_new_null()); + "post.sleep.counter", 1, sentry_value_new_null()); } } @@ -947,8 +944,8 @@ main(int argc, char **argv) sentry_log_debug("logging during scoped transaction event"); } if (has_arg(argc, argv, "metrics-scoped-transaction")) { - sentry_metrics_count("scoped.transaction.metric", 1, NULL, - sentry_value_new_null()); + sentry_metrics_count( + "scoped.transaction.metric", 1, sentry_value_new_null()); } } diff --git a/include/sentry.h b/include/sentry.h index a7ed916b2..4a77189d6 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -2233,8 +2233,7 @@ typedef enum { * request counts or error counts. */ SENTRY_EXPERIMENTAL_API sentry_metrics_result_t sentry_metrics_count( - const char *name, int64_t value, const char *unit, - sentry_value_t attributes); + const char *name, int64_t value, sentry_value_t attributes); /** * Records a gauge metric. Gauges track values that can go up or down, diff --git a/src/sentry_metrics.c b/src/sentry_metrics.c index 3e9198c63..d8d1b1ba3 100644 --- a/src/sentry_metrics.c +++ b/src/sentry_metrics.c @@ -119,11 +119,10 @@ record_metric(sentry_metric_type_t type, const char *name, sentry_value_t value, } sentry_metrics_result_t -sentry_metrics_count(const char *name, int64_t value, const char *unit, - sentry_value_t attributes) +sentry_metrics_count(const char *name, int64_t value, sentry_value_t attributes) { return record_metric(SENTRY_METRIC_COUNT, name, - sentry_value_new_int64(value), unit, attributes); + sentry_value_new_int64(value), NULL, attributes); } sentry_metrics_result_t diff --git a/tests/unit/test_metrics.c b/tests/unit/test_metrics.c index bc0b7ccb7..83d032605 100644 --- a/tests/unit/test_metrics.c +++ b/tests/unit/test_metrics.c @@ -60,7 +60,7 @@ SENTRY_TEST(metrics_count) // Record a counter metric TEST_CHECK_INT_EQUAL( - sentry_metrics_count("test.counter", 1, NULL, sentry_value_new_null()), + sentry_metrics_count("test.counter", 1, sentry_value_new_null()), SENTRY_METRICS_RESULT_SUCCESS); sentry_close(); @@ -147,8 +147,7 @@ SENTRY_TEST(metrics_with_attributes) sentry_value_set_by_key( attributes, "service", sentry_value_new_string("api")); - TEST_CHECK_INT_EQUAL( - sentry_metrics_count("requests.total", 1, NULL, attributes), + TEST_CHECK_INT_EQUAL(sentry_metrics_count("requests.total", 1, attributes), SENTRY_METRICS_RESULT_SUCCESS); sentry_close(); @@ -185,7 +184,7 @@ SENTRY_TEST(metrics_before_send_discard) // This metric should be discarded by the before_send hook TEST_CHECK_INT_EQUAL( - sentry_metrics_count("test.counter", 1, NULL, sentry_value_new_null()), + sentry_metrics_count("test.counter", 1, sentry_value_new_null()), SENTRY_METRICS_RESULT_DISCARD); sentry_close(); @@ -230,7 +229,7 @@ SENTRY_TEST(metrics_before_send_modify) // This metric should be modified by the before_send hook TEST_CHECK_INT_EQUAL( - sentry_metrics_count("test.counter", 1, NULL, sentry_value_new_null()), + sentry_metrics_count("test.counter", 1, sentry_value_new_null()), SENTRY_METRICS_RESULT_SUCCESS); sentry_close(); @@ -256,7 +255,7 @@ SENTRY_TEST(metrics_disabled) // These should return DISABLED since metrics are not enabled TEST_CHECK_INT_EQUAL( - sentry_metrics_count("test.counter", 1, NULL, sentry_value_new_null()), + sentry_metrics_count("test.counter", 1, sentry_value_new_null()), SENTRY_METRICS_RESULT_DISABLED); TEST_CHECK_INT_EQUAL( sentry_metrics_gauge("test.gauge", 42.5, NULL, sentry_value_new_null()), @@ -290,7 +289,7 @@ SENTRY_TEST(metrics_force_flush) // Record multiple metrics with force flush between each TEST_CHECK_INT_EQUAL( - sentry_metrics_count("counter.1", 1, NULL, sentry_value_new_null()), + sentry_metrics_count("counter.1", 1, sentry_value_new_null()), SENTRY_METRICS_RESULT_SUCCESS); sentry_flush(5000); TEST_CHECK_INT_EQUAL( @@ -343,7 +342,7 @@ SENTRY_TEST(metrics_default_attributes) sentry_init(options); sentry__metrics_wait_for_thread_startup(); - sentry_metrics_count("test.metric", 1, NULL, sentry_value_new_null()); + sentry_metrics_count("test.metric", 1, sentry_value_new_null()); sentry_close(); // Validate trace_id is set directly on metric From d41848584d279aadd9f887c2b97ed1362c30ee33 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 30 Jan 2026 12:21:12 +0100 Subject: [PATCH 09/23] ref(batcher): split shutdown/flush into begin/wait for parallelization Split blocking shutdown and force_flush operations into begin (non-blocking trigger) and wait (blocking completion) phases. This allows logs and metrics operations to run in parallel in sentry_flush() and sentry_close(). Co-Authored-By: Claude Opus 4.5 --- src/sentry_batcher.c | 18 ++++++++++++++---- src/sentry_batcher.h | 6 ++++-- src/sentry_core.c | 27 ++++++++++++++++++++------- src/sentry_logs.c | 22 +++++++++++++++++----- src/sentry_logs.h | 19 ++++++++++++++++--- src/sentry_metrics.c | 22 +++++++++++++++++----- src/sentry_metrics.h | 19 ++++++++++++++++--- 7 files changed, 104 insertions(+), 29 deletions(-) diff --git a/src/sentry_batcher.c b/src/sentry_batcher.c index 9d60b4fe1..eadd0e801 100644 --- a/src/sentry_batcher.c +++ b/src/sentry_batcher.c @@ -309,10 +309,8 @@ sentry__batcher_startup( } void -sentry__batcher_shutdown(sentry_batcher_t *batcher, uint64_t timeout) +sentry__batcher_shutdown_begin(sentry_batcher_t *batcher) { - (void)timeout; - // Atomically transition to STOPPED and get the previous state // This handles the race where thread might be in STARTING state: // - If thread's CAS hasn't run yet: CAS will fail, thread exits cleanly @@ -328,6 +326,12 @@ sentry__batcher_shutdown(sentry_batcher_t *batcher, uint64_t timeout) // Thread was started (either STARTING or RUNNING), signal it to stop sentry__cond_wake(&batcher->request_flush); +} + +void +sentry__batcher_shutdown_wait(sentry_batcher_t *batcher, uint64_t timeout) +{ + (void)timeout; // Always join the thread to avoid leaks sentry__thread_join(batcher->batching_thread); @@ -359,7 +363,13 @@ sentry__batcher_flush_crash_safe(sentry_batcher_t *batcher) } void -sentry__batcher_force_flush(sentry_batcher_t *batcher) +sentry__batcher_force_flush_begin(sentry_batcher_t *batcher) +{ + sentry__cond_wake(&batcher->request_flush); +} + +void +sentry__batcher_force_flush_wait(sentry_batcher_t *batcher) { while (sentry__atomic_fetch(&batcher->flushing)) { sentry__cpu_relax(); diff --git a/src/sentry_batcher.h b/src/sentry_batcher.h index 49fa4bc4f..d5c636a30 100644 --- a/src/sentry_batcher.h +++ b/src/sentry_batcher.h @@ -47,9 +47,11 @@ void sentry__batcher_flush(sentry_batcher_t *batcher, bool crash_safe); bool sentry__batcher_enqueue(sentry_batcher_t *batcher, sentry_value_t item); void sentry__batcher_startup( sentry_batcher_t *batcher, sentry_batch_func_t batch_func); -void sentry__batcher_shutdown(sentry_batcher_t *batcher, uint64_t timeout); +void sentry__batcher_shutdown_begin(sentry_batcher_t *batcher); +void sentry__batcher_shutdown_wait(sentry_batcher_t *batcher, uint64_t timeout); void sentry__batcher_flush_crash_safe(sentry_batcher_t *batcher); -void sentry__batcher_force_flush(sentry_batcher_t *batcher); +void sentry__batcher_force_flush_begin(sentry_batcher_t *batcher); +void sentry__batcher_force_flush_wait(sentry_batcher_t *batcher); #ifdef SENTRY_UNITTEST void sentry__batcher_wait_for_thread_startup(sentry_batcher_t *batcher); diff --git a/src/sentry_core.c b/src/sentry_core.c index a09ae7fa6..32715c211 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -320,11 +320,18 @@ sentry_flush(uint64_t timeout) { int rv = 0; SENTRY_WITH_OPTIONS (options) { + // flush logs and metrics in parallel if (options->enable_logs) { - sentry__logs_force_flush(); + sentry__logs_force_flush_begin(); } if (options->enable_metrics) { - sentry__metrics_force_flush(); + sentry__metrics_force_flush_begin(); + } + if (options->enable_logs) { + sentry__logs_force_flush_wait(); + } + if (options->enable_metrics) { + sentry__metrics_force_flush_wait(); } rv = sentry__transport_flush(options->transport, timeout); } @@ -334,15 +341,21 @@ sentry_flush(uint64_t timeout) int sentry_close(void) { - // Shutdown logs and metrics systems before locking options to ensure they - // are flushed. This prevents a potential deadlock on the options during - // envelope creation. + // Shutdown logs and metrics in parallel before locking options to ensure + // they are flushed. This prevents a potential deadlock on the options + // during envelope creation. SENTRY_WITH_OPTIONS (options) { if (options->enable_logs) { - sentry__logs_shutdown(options->shutdown_timeout); + sentry__logs_shutdown_begin(); + } + if (options->enable_metrics) { + sentry__metrics_shutdown_begin(); + } + if (options->enable_logs) { + sentry__logs_shutdown_wait(options->shutdown_timeout); } if (options->enable_metrics) { - sentry__metrics_shutdown(options->shutdown_timeout); + sentry__metrics_shutdown_wait(options->shutdown_timeout); } } diff --git a/src/sentry_logs.c b/src/sentry_logs.c index a39213782..89fb7c8ff 100644 --- a/src/sentry_logs.c +++ b/src/sentry_logs.c @@ -468,10 +468,16 @@ sentry__logs_startup(void) } void -sentry__logs_shutdown(uint64_t timeout) +sentry__logs_shutdown_begin(void) { - SENTRY_DEBUG("shutting down logs system"); - sentry__batcher_shutdown(&g_batcher, timeout); + SENTRY_DEBUG("beginning logs system shutdown"); + sentry__batcher_shutdown_begin(&g_batcher); +} + +void +sentry__logs_shutdown_wait(uint64_t timeout) +{ + sentry__batcher_shutdown_wait(&g_batcher, timeout); SENTRY_DEBUG("logs system shutdown complete"); } @@ -484,9 +490,15 @@ sentry__logs_flush_crash_safe(void) } void -sentry__logs_force_flush(void) +sentry__logs_force_flush_begin(void) +{ + sentry__batcher_force_flush_begin(&g_batcher); +} + +void +sentry__logs_force_flush_wait(void) { - sentry__batcher_force_flush(&g_batcher); + sentry__batcher_force_flush_wait(&g_batcher); } #ifdef SENTRY_UNITTEST diff --git a/src/sentry_logs.h b/src/sentry_logs.h index 866dd526c..bff72ce90 100644 --- a/src/sentry_logs.h +++ b/src/sentry_logs.h @@ -12,9 +12,14 @@ log_return_value_t sentry__logs_log( void sentry__logs_startup(void); /** - * Instructs the logs timer/flush thread to shut down. + * Begin non-blocking shutdown of the logs timer/flush thread. */ -void sentry__logs_shutdown(uint64_t timeout); +void sentry__logs_shutdown_begin(void); + +/** + * Wait for the logs timer/flush thread to complete shutdown. + */ +void sentry__logs_shutdown_wait(uint64_t timeout); /** * Crash-safe logs flush that avoids thread synchronization. @@ -23,7 +28,15 @@ void sentry__logs_shutdown(uint64_t timeout); */ void sentry__logs_flush_crash_safe(void); -void sentry__logs_force_flush(void); +/** + * Begin non-blocking force flush of logs. + */ +void sentry__logs_force_flush_begin(void); + +/** + * Wait for the logs force flush to complete. + */ +void sentry__logs_force_flush_wait(void); #ifdef SENTRY_UNITTEST int populate_message_parameters( diff --git a/src/sentry_metrics.c b/src/sentry_metrics.c index d8d1b1ba3..de2b82940 100644 --- a/src/sentry_metrics.c +++ b/src/sentry_metrics.c @@ -148,10 +148,16 @@ sentry__metrics_startup(void) } void -sentry__metrics_shutdown(uint64_t timeout) +sentry__metrics_shutdown_begin(void) { - SENTRY_DEBUG("shutting down metrics system"); - sentry__batcher_shutdown(&g_batcher, timeout); + SENTRY_DEBUG("beginning metrics system shutdown"); + sentry__batcher_shutdown_begin(&g_batcher); +} + +void +sentry__metrics_shutdown_wait(uint64_t timeout) +{ + sentry__batcher_shutdown_wait(&g_batcher, timeout); SENTRY_DEBUG("metrics system shutdown complete"); } @@ -164,9 +170,15 @@ sentry__metrics_flush_crash_safe(void) } void -sentry__metrics_force_flush(void) +sentry__metrics_force_flush_begin(void) +{ + sentry__batcher_force_flush_begin(&g_batcher); +} + +void +sentry__metrics_force_flush_wait(void) { - sentry__batcher_force_flush(&g_batcher); + sentry__batcher_force_flush_wait(&g_batcher); } #ifdef SENTRY_UNITTEST diff --git a/src/sentry_metrics.h b/src/sentry_metrics.h index 4f8b0b137..062656d09 100644 --- a/src/sentry_metrics.h +++ b/src/sentry_metrics.h @@ -9,9 +9,14 @@ void sentry__metrics_startup(void); /** - * Instructs the metrics timer/flush thread to shut down. + * Begin non-blocking shutdown of the metrics timer/flush thread. */ -void sentry__metrics_shutdown(uint64_t timeout); +void sentry__metrics_shutdown_begin(void); + +/** + * Wait for the metrics timer/flush thread to complete shutdown. + */ +void sentry__metrics_shutdown_wait(uint64_t timeout); /** * Crash-safe metrics flush that avoids thread synchronization. @@ -20,7 +25,15 @@ void sentry__metrics_shutdown(uint64_t timeout); */ void sentry__metrics_flush_crash_safe(void); -void sentry__metrics_force_flush(void); +/** + * Begin non-blocking force flush of metrics. + */ +void sentry__metrics_force_flush_begin(void); + +/** + * Wait for the metrics force flush to complete. + */ +void sentry__metrics_force_flush_wait(void); #ifdef SENTRY_UNITTEST /** From 9993fc6fb960f5fb069f8b16e943484bf56f5972 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 30 Jan 2026 13:44:26 +0100 Subject: [PATCH 10/23] test(metrics): add integration tests matching logs test coverage Add metrics integration tests to match the existing logs test coverage: - test_metrics_threaded: concurrent metrics from 50 threads - test_metrics_global_and_local_attributes_merge: global + local attributes - test_metrics_discarded_on_crash_no_backend: metrics discarded without backend - test_metrics_on_crash: parameterized test for inproc/breakpad backends Also refactor thread creation in example.c into a shared run_threads() helper function used by both logs-threads and metrics-threads commands. Co-Authored-By: Claude Opus 4.5 --- examples/example.c | 89 ++++++++++++------ tests/test_integration_http.py | 163 +++++++++++++++++++++++++++++++++ 2 files changed, 224 insertions(+), 28 deletions(-) diff --git a/examples/example.c b/examples/example.c index ab72ee1f1..33573c25a 100644 --- a/examples/example.c +++ b/examples/example.c @@ -336,8 +336,8 @@ create_debug_crumb(const char *message) } #define NUM_THREADS 50 -#define NUM_LOGS 100 // how many log calls each thread makes -#define LOG_SLEEP_MS 1 // time (in ms) between log calls +#define NUM_TELEMETRY 100 // how many telemetry calls each thread makes +#define TELEMETRY_SLEEP_MS 1 // time (in ms) between telemetry calls #if defined(SENTRY_PLATFORM_WINDOWS) # define sleep_ms(MILLISECONDS) Sleep(MILLISECONDS) @@ -346,29 +346,80 @@ create_debug_crumb(const char *message) #endif #ifdef SENTRY_PLATFORM_WINDOWS +typedef DWORD(WINAPI *thread_func_t)(LPVOID); + DWORD WINAPI log_thread_func(LPVOID lpParam) { (void)lpParam; - for (int i = 0; i < NUM_LOGS; i++) { + for (int i = 0; i < NUM_TELEMETRY; i++) { sentry_log_debug( "thread log %d on thread %lu", i, get_current_thread_id()); - sleep_ms(LOG_SLEEP_MS); + sleep_ms(TELEMETRY_SLEEP_MS); + } + return 0; +} + +DWORD WINAPI +metric_thread_func(LPVOID lpParam) +{ + (void)lpParam; + for (int i = 0; i < NUM_TELEMETRY; i++) { + sentry_metrics_count("thread.counter", 1, sentry_value_new_null()); + sleep_ms(TELEMETRY_SLEEP_MS); } return 0; } + +static void +run_threads(thread_func_t func) +{ + HANDLE threads[NUM_THREADS]; + for (int t = 0; t < NUM_THREADS; t++) { + threads[t] = CreateThread(NULL, 0, func, NULL, 0, NULL); + } + WaitForMultipleObjects(NUM_THREADS, threads, TRUE, INFINITE); + for (int t = 0; t < NUM_THREADS; t++) { + CloseHandle(threads[t]); + } +} #else +typedef void *(*thread_func_t)(void *); + void * log_thread_func(void *arg) { (void)arg; - for (int i = 0; i < NUM_LOGS; i++) { + for (int i = 0; i < NUM_TELEMETRY; i++) { sentry_log_debug( "thread log %d on thread %llu", i, get_current_thread_id()); - sleep_ms(LOG_SLEEP_MS); + sleep_ms(TELEMETRY_SLEEP_MS); + } + return NULL; +} + +void * +metric_thread_func(void *arg) +{ + (void)arg; + for (int i = 0; i < NUM_TELEMETRY; i++) { + sentry_metrics_count("thread.counter", 1, sentry_value_new_null()); + sleep_ms(TELEMETRY_SLEEP_MS); } return NULL; } + +static void +run_threads(thread_func_t func) +{ + pthread_t threads[NUM_THREADS]; + for (int t = 0; t < NUM_THREADS; t++) { + pthread_create(&threads[t], NULL, func, NULL); + } + for (int t = 0; t < NUM_THREADS; t++) { + pthread_join(threads[t], NULL); + } +} #endif int @@ -613,28 +664,7 @@ main(int argc, char **argv) sentry_log_debug("post-sleep log"); } if (has_arg(argc, argv, "logs-threads")) { - // Spawn multiple threads to test concurrent logging -#ifdef SENTRY_PLATFORM_WINDOWS - HANDLE threads[NUM_THREADS]; - for (int t = 0; t < NUM_THREADS; t++) { - threads[t] - = CreateThread(NULL, 0, log_thread_func, NULL, 0, NULL); - } - - WaitForMultipleObjects(NUM_THREADS, threads, TRUE, INFINITE); - - for (int t = 0; t < NUM_THREADS; t++) { - CloseHandle(threads[t]); - } -#else - pthread_t threads[NUM_THREADS]; - for (int t = 0; t < NUM_THREADS; t++) { - pthread_create(&threads[t], NULL, log_thread_func, NULL); - } - for (int t = 0; t < NUM_THREADS; t++) { - pthread_join(threads[t], NULL); - } -#endif + run_threads(log_thread_func); } } @@ -665,6 +695,9 @@ main(int argc, char **argv) sentry_metrics_count( "post.sleep.counter", 1, sentry_value_new_null()); } + if (has_arg(argc, argv, "metrics-threads")) { + run_threads(metric_thread_func); + } } if (!has_arg(argc, argv, "no-setup")) { diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index 0ea750f4e..90f6f0658 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -2117,3 +2117,166 @@ def test_metrics_event(cmake, httpserver): metrics_body = metrics_req.get_data() metrics_envelope = Envelope.deserialize(metrics_body) assert_metrics(metrics_envelope, 1, event_trace_id) + + +def test_metrics_threaded(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) + + httpserver.expect_request( + "/api/123456/envelope/", + headers={"x-sentry-auth": auth_header}, + ).respond_with_data("OK") + + run( + tmp_path, + "sentry_example", + ["log", "enable-metrics", "metrics-threads"], + env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)), + ) + + # there is a chance we drop metrics while flushing buffers + assert 1 <= len(httpserver.log) <= 50 + total_count = 0 + + for i in range(len(httpserver.log)): + req = httpserver.log[i][0] + body = req.get_data() + + envelope = Envelope.deserialize(body) + assert_metrics(envelope) + total_count += envelope.items[0].headers["item_count"] + print(f"Total amount of captured metrics: {total_count}") + assert total_count >= 100 + + +def test_metrics_global_and_local_attributes_merge(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) + + httpserver.expect_oneshot_request( + "/api/123456/envelope/", + headers={"x-sentry-auth": auth_header}, + ).respond_with_data("OK") + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + + run( + tmp_path, + "sentry_example", + ["log", "enable-metrics", "set-global-attribute", "metric-with-attributes"], + env=env, + ) + + assert len(httpserver.log) == 1 + req = httpserver.log[0][0] + body = req.get_data() + + envelope = Envelope.deserialize(body) + + # Show what the envelope looks like if the test fails + envelope.print_verbose() + + # Extract the metric item + (metric_item,) = envelope.items + + assert metric_item.headers["type"] == "trace_metric" + payload = metric_item.payload.json + + assert len(payload["items"]) == 1 + + metric_entry = payload["items"][0] + attributes = metric_entry["attributes"] + + # Verify local attribute (should overwrite global with same key) + assert "my.custom.attribute" in attributes + assert attributes["my.custom.attribute"]["value"] == "my_value" + assert attributes["my.custom.attribute"]["type"] == "string" + + # Verify global attributes are present + assert "global.attribute.bool" in attributes + assert attributes["global.attribute.bool"]["value"] == True + assert attributes["global.attribute.bool"]["type"] == "boolean" + + assert "global.attribute.int" in attributes + assert attributes["global.attribute.int"]["value"] == 123 + assert attributes["global.attribute.int"]["type"] == "integer" + + assert "global.attribute.double" in attributes + assert attributes["global.attribute.double"]["value"] == 1.23 + assert attributes["global.attribute.double"]["type"] == "double" + + assert "global.attribute.string" in attributes + assert attributes["global.attribute.string"]["value"] == "my_global_value" + assert attributes["global.attribute.string"]["type"] == "string" + + assert "global.attribute.array" in attributes + assert attributes["global.attribute.array"]["value"] == ["item1", "item2"] + assert attributes["global.attribute.array"]["type"] == "array" + + +def test_metrics_discarded_on_crash_no_backend(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) + + httpserver.expect_oneshot_request( + "/api/123456/envelope/", + headers={"x-sentry-auth": auth_header}, + ).respond_with_data("OK") + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver), SENTRY_RELEASE="🤮🚀") + + run( + tmp_path, + "sentry_example", + ["log", "enable-metrics", "capture-metric", "crash"], + expect_failure=True, + env=env, + ) + + # metric should have been discarded since we have no backend to hook into the crash + assert len(httpserver.log) == 0 + + +@pytest.mark.parametrize( + "backend", + [ + "inproc", + pytest.param( + "breakpad", + marks=pytest.mark.skipif( + not has_breakpad, reason="breakpad backend not available" + ), + ), + ], +) +def test_metrics_on_crash(cmake, httpserver, backend): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": backend}) + + httpserver.expect_request( + "/api/123456/envelope/", + headers={"x-sentry-auth": auth_header}, + ).respond_with_data("OK") + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + + run( + tmp_path, + "sentry_example", + ["log", "enable-metrics", "capture-metric", "crash"], + expect_failure=True, + env=env, + ) + + run( + tmp_path, + "sentry_example", + ["log", "no-setup"], + env=env, + ) + + # we expect 1 envelope with the metric, and 1 for the crash + assert len(httpserver.log) == 2 + metrics_request, crash_request = split_log_request_cond( + httpserver.log, is_metrics_envelope + ) + metrics = metrics_request.get_data() + + metrics_envelope = Envelope.deserialize(metrics) + + assert metrics_envelope is not None + assert_metrics(metrics_envelope, 1) From 8b0c9c68f2af643f169588840fac3feebd4ea9b0 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 30 Jan 2026 16:03:15 +0100 Subject: [PATCH 11/23] fix(logs): avoid modifying custom per-log attributes (#1500) Allow reusing attribute objects across multiple log calls, as documented. Co-Authored-By: Claude Opus 4.5 --- src/sentry_logs.c | 14 +++++--------- src/sentry_scope.c | 2 ++ tests/unit/test_logs.c | 31 +++++++++++++++++++++++++++++++ tests/unit/tests.inc | 1 + 4 files changed, 39 insertions(+), 9 deletions(-) diff --git a/src/sentry_logs.c b/src/sentry_logs.c index 89fb7c8ff..4c389441f 100644 --- a/src/sentry_logs.c +++ b/src/sentry_logs.c @@ -243,10 +243,7 @@ static sentry_value_t construct_log(sentry_level_t level, const char *message, va_list args) { sentry_value_t log = sentry_value_new_object(); - sentry_value_t attributes = sentry_value_new_null(); - SENTRY_WITH_SCOPE (scope) { - attributes = sentry__value_clone(scope->attributes); - } + sentry_value_t attributes = sentry_value_new_object(); SENTRY_WITH_OPTIONS (options) { // Extract custom attributes if the option is enabled @@ -258,16 +255,15 @@ construct_log(sentry_level_t level, const char *message, va_list args) va_end(args_copy); if (sentry_value_get_type(custom_attributes) == SENTRY_VALUE_TYPE_OBJECT) { - // Merge global attributes INTO per-log attributes - // (per-log attributes take precedence for conflicts) - sentry__value_merge_objects(custom_attributes, attributes); + // Clone custom attributes first (per-log attributes take + // precedence for conflicts) sentry_value_decref(attributes); - attributes = custom_attributes; + attributes = sentry__value_clone(custom_attributes); } else { SENTRY_DEBUG("Discarded custom attributes on log: non-object " "sentry_value_t passed in"); - sentry_value_decref(custom_attributes); } + sentry_value_decref(custom_attributes); } // Format the message with remaining args (or all args if not using diff --git a/src/sentry_scope.c b/src/sentry_scope.c index e48dd9b39..a89e6cb11 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -779,6 +779,8 @@ void sentry__scope_apply_attributes(const sentry_scope_t *scope, sentry_value_t telemetry, sentry_value_t attributes) { + sentry__value_merge_objects(attributes, scope->attributes); + sentry_value_t trace_id = sentry_value_get_by_key( sentry_value_get_by_key(scope->propagation_context, "trace"), "trace_id"); diff --git a/tests/unit/test_logs.c b/tests/unit/test_logs.c index f90f59f27..b2df39cc5 100644 --- a/tests/unit/test_logs.c +++ b/tests/unit/test_logs.c @@ -379,3 +379,34 @@ SENTRY_TEST(logs_custom_attributes_with_format_strings) TEST_CHECK(!validation_data.has_validation_error); TEST_CHECK_INT_EQUAL(validation_data.called_count, 1); } + +SENTRY_TEST(logs_custom_attributes_not_modified) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_enable_logs(options, true); + sentry_options_set_logs_with_attributes(options, true); + sentry_init(options); + + sentry_set_attribute("global.key", + sentry_value_new_attribute( + sentry_value_new_string("global_value"), NULL)); + + sentry_value_t attrs = sentry_value_new_object(); + sentry_value_set_by_key(attrs, "local.key", + sentry_value_new_attribute( + sentry_value_new_string("local_value"), NULL)); + sentry_value_incref(attrs); + + sentry_log_info("Test message", attrs); + + // attrs should still contain only local.key, not global.key + TEST_CHECK_INT_EQUAL(sentry_value_get_length(attrs), 1); + TEST_CHECK( + !sentry_value_is_null(sentry_value_get_by_key(attrs, "local.key"))); + TEST_CHECK( + sentry_value_is_null(sentry_value_get_by_key(attrs, "global.key"))); + + sentry_value_decref(attrs); + sentry_close(); +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index f47494207..ce736a7fc 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -93,6 +93,7 @@ XX(iso_time) XX(lazy_attachments) XX(logger_enable_disable_functionality) XX(logger_level) +XX(logs_custom_attributes_not_modified) XX(logs_custom_attributes_with_format_strings) XX(logs_disabled_by_default) XX(logs_force_flush) From 725b062c040419e1a78f396af2fd66f50ac298c1 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 30 Jan 2026 14:43:54 +0100 Subject: [PATCH 12/23] fix(metrics): scope attributes Global attributes set via sentry_set_attribute() were not being merged into metrics. Now scope attributes are merged with user attributes having priority on conflicts. Co-Authored-By: Claude Opus 4.5 --- src/sentry_metrics.c | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/sentry_metrics.c b/src/sentry_metrics.c index de2b82940..e512b7108 100644 --- a/src/sentry_metrics.c +++ b/src/sentry_metrics.c @@ -3,6 +3,7 @@ #include "sentry_core.h" #include "sentry_envelope.h" #include "sentry_options.h" +#include "sentry_scope.h" #include "sentry_utils.h" #include "sentry_value.h" @@ -63,14 +64,14 @@ construct_metric(sentry_metric_type_t type, const char *name, sentry_value_set_by_key(metric, "unit", sentry_value_new_string(unit)); } - sentry_value_t attributes = sentry_value_new_object(); - sentry__apply_attributes(metric, attributes); - if (sentry_value_get_type(user_attributes) == SENTRY_VALUE_TYPE_OBJECT - && sentry_value_get_length(user_attributes) > 0) { - sentry__value_merge_objects(attributes, user_attributes); - } + sentry_value_t attributes + = sentry_value_get_type(user_attributes) == SENTRY_VALUE_TYPE_OBJECT + ? sentry__value_clone(user_attributes) + : sentry_value_new_object(); sentry_value_decref(user_attributes); + sentry__apply_attributes(metric, attributes); + if (sentry_value_get_length(attributes) > 0) { sentry_value_set_by_key(metric, "attributes", attributes); } else { From d9c5e2775b80469cd86a18cf0101ec39c4640ef6 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 2 Feb 2026 15:38:56 +0100 Subject: [PATCH 13/23] Update CHANGELOG.md --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd197a265..59c29ddcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,16 @@ # Changelog +## Unreleased + +**Features**: + +- Add support for metrics. It is currently experimental, and one can enable it by setting `sentry_options_set_enable_metrics`. When enabled, you can record a metric using `sentry_metrics_count()`, `sentry_metrics_gauge()`, or `sentry_metrics_distribution()`. Metrics can be filtered by setting the `before_send_metric` hook. ([#1498](https://github.com/getsentry/sentry-native/pull/1498)) + ## 0.12.5 **Features**: - Add attachment support to user feedback. ([#1414](https://github.com/getsentry/sentry-native/pull/1414)) -- Add support for metrics. It is currently experimental, and one can enable it by setting `sentry_options_set_enable_metrics`. When enabled, you can record a metric using `sentry_metrics_count()`, `sentry_metrics_gauge()`, or `sentry_metrics_distribution()`. Metrics can be filtered by setting the `before_send_metric` hook. ([#1498](https://github.com/getsentry/sentry-native/pull/1498)) **Fixes**: From 898c169d14d4003693a79ee32b4ad791957e768f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 2 Feb 2026 18:36:29 +0100 Subject: [PATCH 14/23] fix batcher shutdown split thread lifecycle guard --- src/sentry_batcher.c | 5 +++-- src/sentry_batcher.h | 2 +- src/sentry_core.c | 10 ++++++---- src/sentry_logs.c | 4 ++-- src/sentry_logs.h | 3 ++- src/sentry_metrics.c | 4 ++-- src/sentry_metrics.h | 3 ++- 7 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/sentry_batcher.c b/src/sentry_batcher.c index eadd0e801..265b65308 100644 --- a/src/sentry_batcher.c +++ b/src/sentry_batcher.c @@ -308,7 +308,7 @@ sentry__batcher_startup( } } -void +bool sentry__batcher_shutdown_begin(sentry_batcher_t *batcher) { // Atomically transition to STOPPED and get the previous state @@ -321,11 +321,12 @@ sentry__batcher_shutdown_begin(sentry_batcher_t *batcher) // If thread was never started, nothing to do if (old_state == SENTRY_BATCHER_THREAD_STOPPED) { SENTRY_DEBUG("batcher thread was not started, skipping shutdown"); - return; + return false; } // Thread was started (either STARTING or RUNNING), signal it to stop sentry__cond_wake(&batcher->request_flush); + return true; } void diff --git a/src/sentry_batcher.h b/src/sentry_batcher.h index d5c636a30..42d9641d0 100644 --- a/src/sentry_batcher.h +++ b/src/sentry_batcher.h @@ -47,7 +47,7 @@ void sentry__batcher_flush(sentry_batcher_t *batcher, bool crash_safe); bool sentry__batcher_enqueue(sentry_batcher_t *batcher, sentry_value_t item); void sentry__batcher_startup( sentry_batcher_t *batcher, sentry_batch_func_t batch_func); -void sentry__batcher_shutdown_begin(sentry_batcher_t *batcher); +bool sentry__batcher_shutdown_begin(sentry_batcher_t *batcher); void sentry__batcher_shutdown_wait(sentry_batcher_t *batcher, uint64_t timeout); void sentry__batcher_flush_crash_safe(sentry_batcher_t *batcher); void sentry__batcher_force_flush_begin(sentry_batcher_t *batcher); diff --git a/src/sentry_core.c b/src/sentry_core.c index 32715c211..00cdf6537 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -345,16 +345,18 @@ sentry_close(void) // they are flushed. This prevents a potential deadlock on the options // during envelope creation. SENTRY_WITH_OPTIONS (options) { + bool wait_logs = false; if (options->enable_logs) { - sentry__logs_shutdown_begin(); + wait_logs = sentry__logs_shutdown_begin(); } + bool wait_metrics = false; if (options->enable_metrics) { - sentry__metrics_shutdown_begin(); + wait_metrics = sentry__metrics_shutdown_begin(); } - if (options->enable_logs) { + if (wait_logs) { sentry__logs_shutdown_wait(options->shutdown_timeout); } - if (options->enable_metrics) { + if (wait_metrics) { sentry__metrics_shutdown_wait(options->shutdown_timeout); } } diff --git a/src/sentry_logs.c b/src/sentry_logs.c index 4c389441f..a12c45fbe 100644 --- a/src/sentry_logs.c +++ b/src/sentry_logs.c @@ -463,11 +463,11 @@ sentry__logs_startup(void) sentry__batcher_startup(&g_batcher, sentry__envelope_add_logs); } -void +bool sentry__logs_shutdown_begin(void) { SENTRY_DEBUG("beginning logs system shutdown"); - sentry__batcher_shutdown_begin(&g_batcher); + return sentry__batcher_shutdown_begin(&g_batcher); } void diff --git a/src/sentry_logs.h b/src/sentry_logs.h index bff72ce90..402d2ff68 100644 --- a/src/sentry_logs.h +++ b/src/sentry_logs.h @@ -14,10 +14,11 @@ void sentry__logs_startup(void); /** * Begin non-blocking shutdown of the logs timer/flush thread. */ -void sentry__logs_shutdown_begin(void); +bool sentry__logs_shutdown_begin(void); /** * Wait for the logs timer/flush thread to complete shutdown. + * Should only be called if sentry__logs_shutdown_begin returned true. */ void sentry__logs_shutdown_wait(uint64_t timeout); diff --git a/src/sentry_metrics.c b/src/sentry_metrics.c index e512b7108..2e9735bea 100644 --- a/src/sentry_metrics.c +++ b/src/sentry_metrics.c @@ -148,11 +148,11 @@ sentry__metrics_startup(void) sentry__batcher_startup(&g_batcher, sentry__envelope_add_metrics); } -void +bool sentry__metrics_shutdown_begin(void) { SENTRY_DEBUG("beginning metrics system shutdown"); - sentry__batcher_shutdown_begin(&g_batcher); + return sentry__batcher_shutdown_begin(&g_batcher); } void diff --git a/src/sentry_metrics.h b/src/sentry_metrics.h index 062656d09..f62ca93ad 100644 --- a/src/sentry_metrics.h +++ b/src/sentry_metrics.h @@ -11,10 +11,11 @@ void sentry__metrics_startup(void); /** * Begin non-blocking shutdown of the metrics timer/flush thread. */ -void sentry__metrics_shutdown_begin(void); +bool sentry__metrics_shutdown_begin(void); /** * Wait for the metrics timer/flush thread to complete shutdown. + * Should only be called if sentry__metrics_shutdown_begin returned true. */ void sentry__metrics_shutdown_wait(uint64_t timeout); From 0326af98e5b3fb033c303c840de79a14b8bd4a56 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 2 Feb 2026 18:43:05 +0100 Subject: [PATCH 15/23] consolidate sentry__envelope_add_logs/metrics shared helper --- src/sentry_envelope.c | 52 ++++++++++++++----------------------------- 1 file changed, 17 insertions(+), 35 deletions(-) diff --git a/src/sentry_envelope.c b/src/sentry_envelope.c index 64467fd8d..ccd78226f 100644 --- a/src/sentry_envelope.c +++ b/src/sentry_envelope.c @@ -440,8 +440,9 @@ sentry__envelope_add_transaction( return item; } -sentry_envelope_item_t * -sentry__envelope_add_logs(sentry_envelope_t *envelope, sentry_value_t logs) +static sentry_envelope_item_t * +add_telemetry(sentry_envelope_t *envelope, sentry_value_t telemetry, + const char *type, const char *content_type) { sentry_envelope_item_t *item = envelope_add_item(envelope); if (!item) { @@ -453,57 +454,38 @@ sentry__envelope_add_logs(sentry_envelope_t *envelope, sentry_value_t logs) return NULL; } - sentry__jsonwriter_write_value(jw, logs); + sentry__jsonwriter_write_value(jw, telemetry); item->payload = sentry__jsonwriter_into_string(jw, &item->payload_len); if (!item->payload) { return NULL; } sentry__envelope_item_set_header( - item, "type", sentry_value_new_string("log")); + item, "type", sentry_value_new_string(type)); sentry__envelope_item_set_header(item, "item_count", sentry_value_new_int32((int32_t)sentry_value_get_length( - sentry_value_get_by_key(logs, "items")))); - sentry__envelope_item_set_header(item, "content_type", - sentry_value_new_string("application/vnd.sentry.items.log+json")); + sentry_value_get_by_key(telemetry, "items")))); + sentry__envelope_item_set_header( + item, "content_type", sentry_value_new_string(content_type)); sentry_value_t length = sentry_value_new_int32((int32_t)item->payload_len); sentry__envelope_item_set_header(item, "length", length); return item; } +sentry_envelope_item_t * +sentry__envelope_add_logs(sentry_envelope_t *envelope, sentry_value_t logs) +{ + return add_telemetry( + envelope, logs, "log", "application/vnd.sentry.items.log+json"); +} + sentry_envelope_item_t * sentry__envelope_add_metrics( sentry_envelope_t *envelope, sentry_value_t metrics) { - sentry_envelope_item_t *item = envelope_add_item(envelope); - if (!item) { - return NULL; - } - - sentry_jsonwriter_t *jw = sentry__jsonwriter_new_sb(NULL); - if (!jw) { - return NULL; - } - - sentry__jsonwriter_write_value(jw, metrics); - item->payload = sentry__jsonwriter_into_string(jw, &item->payload_len); - if (!item->payload) { - return NULL; - } - - sentry__envelope_item_set_header( - item, "type", sentry_value_new_string("trace_metric")); - sentry__envelope_item_set_header(item, "item_count", - sentry_value_new_int32((int32_t)sentry_value_get_length( - sentry_value_get_by_key(metrics, "items")))); - sentry__envelope_item_set_header(item, "content_type", - sentry_value_new_string( - "application/vnd.sentry.items.trace-metric+json")); - sentry_value_t length = sentry_value_new_int32((int32_t)item->payload_len); - sentry__envelope_item_set_header(item, "length", length); - - return item; + return add_telemetry(envelope, metrics, "trace_metric", + "application/vnd.sentry.items.metric+json"); } sentry_envelope_item_t * From d9eec5451755ebdb13de829f3713f2e974b03665 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 2 Feb 2026 19:38:49 +0100 Subject: [PATCH 16/23] fix sentry__envelope_add_metrics --- src/sentry_envelope.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry_envelope.c b/src/sentry_envelope.c index ccd78226f..fc81cea24 100644 --- a/src/sentry_envelope.c +++ b/src/sentry_envelope.c @@ -485,7 +485,7 @@ sentry__envelope_add_metrics( sentry_envelope_t *envelope, sentry_value_t metrics) { return add_telemetry(envelope, metrics, "trace_metric", - "application/vnd.sentry.items.metric+json"); + "application/vnd.sentry.items.trace-metric+json"); } sentry_envelope_item_t * From 0154bd5ff85d9ae4b8078b890f000911360d06ce Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 2 Feb 2026 19:40:00 +0100 Subject: [PATCH 17/23] clean up unused import --- tests/test_integration_http.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index 90f6f0658..c7f389f08 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -15,7 +15,6 @@ run, Envelope, split_log_request_cond, - extract_request, is_feedback_envelope, is_logs_envelope, is_metrics_envelope, From c57c0c72903bedf417da5e5874b4ec0bfe503c53 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 3 Feb 2026 09:56:33 +0100 Subject: [PATCH 18/23] fix race condition in batcher force_flush_wait The split of force_flush into _begin() + _wait() introduced a race where _begin() wakes the batcher thread, but _wait() could return early if the batcher thread wins the race to acquire the flushing lock. This could cause sentry_flush() to return before data was actually sent. Fix by making sentry__batcher_flush return bool indicating success, and loop in _wait() until a flush actually completes. Co-Authored-By: Claude Opus 4.5 --- src/sentry_batcher.c | 18 +++++++++++------- src/sentry_batcher.h | 2 +- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/sentry_batcher.c b/src/sentry_batcher.c index 265b65308..a8ae841b8 100644 --- a/src/sentry_batcher.c +++ b/src/sentry_batcher.c @@ -47,7 +47,7 @@ check_for_flush_condition(sentry_batcher_t *batcher) >= SENTRY_BATCHER_QUEUE_LENGTH; } -void +bool sentry__batcher_flush(sentry_batcher_t *batcher, bool crash_safe) { if (crash_safe) { @@ -59,7 +59,7 @@ sentry__batcher_flush(sentry_batcher_t *batcher, bool crash_safe) SENTRY_WARN( "sentry__batcher_flush: timeout waiting for flushing " "lock in crash-safe mode"); - return; + return false; } // backoff max-wait with max_attempts = 200 based sleep slots: @@ -74,7 +74,7 @@ sentry__batcher_flush(sentry_batcher_t *batcher, bool crash_safe) const long already_flushing = sentry__atomic_store(&batcher->flushing, 1); if (already_flushing) { - return; + return false; } } do { @@ -135,6 +135,7 @@ sentry__batcher_flush(sentry_batcher_t *batcher, bool crash_safe) } while (check_for_flush_condition(batcher)); sentry__atomic_store(&batcher->flushing, 0); + return true; } #define ENQUEUE_MAX_RETRIES 2 @@ -372,10 +373,13 @@ sentry__batcher_force_flush_begin(sentry_batcher_t *batcher) void sentry__batcher_force_flush_wait(sentry_batcher_t *batcher) { - while (sentry__atomic_fetch(&batcher->flushing)) { - sentry__cpu_relax(); - } - sentry__batcher_flush(batcher, false); + do { + // wait for in-progress flush to complete + while (sentry__atomic_fetch(&batcher->flushing)) { + sentry__cpu_relax(); + } + // retry if the batcher thread (woken by _begin) wins the race + } while (!sentry__batcher_flush(batcher, false)); } #ifdef SENTRY_UNITTEST diff --git a/src/sentry_batcher.h b/src/sentry_batcher.h index 42d9641d0..494f178c7 100644 --- a/src/sentry_batcher.h +++ b/src/sentry_batcher.h @@ -43,7 +43,7 @@ typedef struct { sentry_batch_func_t batch_func; // function to add items to envelope } sentry_batcher_t; -void sentry__batcher_flush(sentry_batcher_t *batcher, bool crash_safe); +bool sentry__batcher_flush(sentry_batcher_t *batcher, bool crash_safe); bool sentry__batcher_enqueue(sentry_batcher_t *batcher, sentry_value_t item); void sentry__batcher_startup( sentry_batcher_t *batcher, sentry_batch_func_t batch_func); From a878869f28e283866934c82d23d7a09646886aa1 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 4 Feb 2026 13:56:40 +0100 Subject: [PATCH 19/23] Update include/sentry.h Co-authored-by: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> --- include/sentry.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/sentry.h b/include/sentry.h index 92095143b..71006adf6 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -365,7 +365,7 @@ SENTRY_API sentry_value_t sentry_value_new_user_n(const char *id, size_t id_len, /** * Creates a new attribute object. - * value is required, unit is optional. + * value is required, unit is optional, but has to be one of the `SENTRY_UNIT_X` macros. * * value must be a bool, int, double or string `sentry_value_t` * OR a list of bool, int, double or string (with all items being the same type) From 3bff6c7c8c79c658eef08371b02ae9ca01dd0faa Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 4 Feb 2026 13:59:36 +0100 Subject: [PATCH 20/23] Fix formatting --- include/sentry.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/sentry.h b/include/sentry.h index 71006adf6..d2b7dbeca 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -365,7 +365,8 @@ SENTRY_API sentry_value_t sentry_value_new_user_n(const char *id, size_t id_len, /** * Creates a new attribute object. - * value is required, unit is optional, but has to be one of the `SENTRY_UNIT_X` macros. + * value is required, unit is optional, but has to be one of the + * `SENTRY_UNIT_X` macros. * * value must be a bool, int, double or string `sentry_value_t` * OR a list of bool, int, double or string (with all items being the same type) From 9478d36f166b9736dcf04eb3e53ce6ff91fbf27c Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 4 Feb 2026 14:23:37 +0100 Subject: [PATCH 21/23] Add missing test for batching --- tests/unit/test_metrics.c | 33 +++++++++++++++++++++++++++++++++ tests/unit/tests.inc | 1 + 2 files changed, 34 insertions(+) diff --git a/tests/unit/test_metrics.c b/tests/unit/test_metrics.c index 83d032605..7b58fe8f5 100644 --- a/tests/unit/test_metrics.c +++ b/tests/unit/test_metrics.c @@ -124,6 +124,39 @@ SENTRY_TEST(metrics_distribution) TEST_CHECK_INT_EQUAL(validation_data.called_count, 1); } +SENTRY_TEST(metrics_batch) +{ + transport_validation_data_t validation_data = { 0, false }; + + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_enable_metrics(options, true); + + sentry_transport_t *transport + = sentry_transport_new(validate_metrics_envelope); + sentry_transport_set_state(transport, &validation_data); + sentry_options_set_transport(options, transport); + + sentry_init(options); + sentry__metrics_wait_for_thread_startup(); + + // Batch buffer is 5 items in tests + for (int i = 0; i < 5; i++) { + TEST_CHECK_INT_EQUAL( + sentry_metrics_count("test.counter", 1, sentry_value_new_null()), + SENTRY_METRICS_RESULT_SUCCESS); + } + // Sleep to allow first batch to flush + sleep_ms(20); + TEST_CHECK_INT_EQUAL( + sentry_metrics_count("test.counter", 1, sentry_value_new_null()), + SENTRY_METRICS_RESULT_SUCCESS); + sentry_close(); + + TEST_CHECK(!validation_data.has_validation_error); + TEST_CHECK_INT_EQUAL(validation_data.called_count, 2); +} + SENTRY_TEST(metrics_with_attributes) { transport_validation_data_t validation_data = { 0, false }; diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 83b89601b..ffa5f35d3 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -100,6 +100,7 @@ XX(logs_force_flush) XX(logs_param_conversion) XX(logs_param_types) XX(message_with_null_text_is_valid) +XX(metrics_batch) XX(metrics_before_send_discard) XX(metrics_before_send_modify) XX(metrics_count) From 480f943eb0f3f56f1e5a7e31536f36b56ff6adde Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 4 Feb 2026 15:05:59 +0100 Subject: [PATCH 22/23] Fix redundant batch_func initialization in batchers --- src/sentry_logs.c | 2 +- src/sentry_metrics.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sentry_logs.c b/src/sentry_logs.c index a12c45fbe..e7b24d1fb 100644 --- a/src/sentry_logs.c +++ b/src/sentry_logs.c @@ -25,7 +25,7 @@ static sentry_batcher_t g_batcher = { .active_idx = 0, .flushing = 0, .thread_state = SENTRY_BATCHER_THREAD_STOPPED, - .batch_func = sentry__envelope_add_logs, + .batch_func = NULL, }; static const char * diff --git a/src/sentry_metrics.c b/src/sentry_metrics.c index 2e9735bea..f29f8b576 100644 --- a/src/sentry_metrics.c +++ b/src/sentry_metrics.c @@ -29,7 +29,7 @@ static sentry_batcher_t g_batcher = { .active_idx = 0, .flushing = 0, .thread_state = SENTRY_BATCHER_THREAD_STOPPED, - .batch_func = sentry__envelope_add_metrics, + .batch_func = NULL, }; static const char * From ca4a0e6091fec6e114eb23c64dc81bc583bee936 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 4 Feb 2026 15:13:51 +0100 Subject: [PATCH 23/23] Keep static batch_func initializer --- src/sentry_batcher.c | 5 +---- src/sentry_batcher.h | 3 +-- src/sentry_logs.c | 4 ++-- src/sentry_metrics.c | 4 ++-- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/sentry_batcher.c b/src/sentry_batcher.c index a8ae841b8..405acdc33 100644 --- a/src/sentry_batcher.c +++ b/src/sentry_batcher.c @@ -282,11 +282,8 @@ batcher_thread_func(void *data) } void -sentry__batcher_startup( - sentry_batcher_t *batcher, sentry_batch_func_t batch_func) +sentry__batcher_startup(sentry_batcher_t *batcher) { - batcher->batch_func = batch_func; - // Mark thread as starting before actually spawning so thread can transition // to RUNNING. This prevents shutdown from thinking the thread was never // started if it races with the thread's initialization. diff --git a/src/sentry_batcher.h b/src/sentry_batcher.h index 494f178c7..899815581 100644 --- a/src/sentry_batcher.h +++ b/src/sentry_batcher.h @@ -45,8 +45,7 @@ typedef struct { bool sentry__batcher_flush(sentry_batcher_t *batcher, bool crash_safe); bool sentry__batcher_enqueue(sentry_batcher_t *batcher, sentry_value_t item); -void sentry__batcher_startup( - sentry_batcher_t *batcher, sentry_batch_func_t batch_func); +void sentry__batcher_startup(sentry_batcher_t *batcher); bool sentry__batcher_shutdown_begin(sentry_batcher_t *batcher); void sentry__batcher_shutdown_wait(sentry_batcher_t *batcher, uint64_t timeout); void sentry__batcher_flush_crash_safe(sentry_batcher_t *batcher); diff --git a/src/sentry_logs.c b/src/sentry_logs.c index e7b24d1fb..1221e0938 100644 --- a/src/sentry_logs.c +++ b/src/sentry_logs.c @@ -25,7 +25,7 @@ static sentry_batcher_t g_batcher = { .active_idx = 0, .flushing = 0, .thread_state = SENTRY_BATCHER_THREAD_STOPPED, - .batch_func = NULL, + .batch_func = sentry__envelope_add_logs, }; static const char * @@ -460,7 +460,7 @@ sentry_log_fatal(const char *message, ...) void sentry__logs_startup(void) { - sentry__batcher_startup(&g_batcher, sentry__envelope_add_logs); + sentry__batcher_startup(&g_batcher); } bool diff --git a/src/sentry_metrics.c b/src/sentry_metrics.c index f29f8b576..68e86cb51 100644 --- a/src/sentry_metrics.c +++ b/src/sentry_metrics.c @@ -29,7 +29,7 @@ static sentry_batcher_t g_batcher = { .active_idx = 0, .flushing = 0, .thread_state = SENTRY_BATCHER_THREAD_STOPPED, - .batch_func = NULL, + .batch_func = sentry__envelope_add_metrics, }; static const char * @@ -145,7 +145,7 @@ sentry_metrics_distribution( void sentry__metrics_startup(void) { - sentry__batcher_startup(&g_batcher, sentry__envelope_add_metrics); + sentry__batcher_startup(&g_batcher); } bool