diff --git a/examples/example.c b/examples/example.c index ec8d972a91..669ca10949 100644 --- a/examples/example.c +++ b/examples/example.c @@ -158,6 +158,33 @@ discarding_before_transaction_callback(sentry_value_t tx, void *user_data) return tx; } +static sentry_value_t +before_send_log_callback(sentry_value_t log, void *user_data) +{ + (void)user_data; + sentry_value_t attribute = sentry_value_new_object(); + sentry_value_set_by_key( + attribute, "value", sentry_value_new_string("little")); + sentry_value_set_by_key( + attribute, "type", sentry_value_new_string("string")); + sentry_value_set_by_key(sentry_value_get_by_key(log, "attributes"), + "coffeepot.size", attribute); + return log; +} + +static sentry_value_t +discarding_before_send_log_callback(sentry_value_t log, void *user_data) +{ + (void)user_data; + if (sentry_value_is_null( + sentry_value_get_by_key(sentry_value_get_by_key(log, "attributes"), + "sentry.message.template"))) { + sentry_value_decref(log); + return sentry_value_new_null(); + } + return log; +} + static void print_envelope(sentry_envelope_t *envelope, void *unused_state) { @@ -374,6 +401,16 @@ main(int argc, char **argv) options, discarding_before_transaction_callback, NULL); } + if (has_arg(argc, argv, "before-send-log")) { + sentry_options_set_before_send_log( + options, before_send_log_callback, NULL); + } + + if (has_arg(argc, argv, "discarding-before-send-log")) { + sentry_options_set_before_send_log( + options, discarding_before_send_log_callback, NULL); + } + if (has_arg(argc, argv, "traces-sampler")) { sentry_options_set_traces_sampler(options, traces_sampler_callback); } @@ -424,6 +461,9 @@ main(int argc, char **argv) // TODO incorporate into test if (sentry_options_get_enable_logs(options)) { + if (has_arg(argc, argv, "capture-log")) { + sentry_log_debug("I'm a log message!"); + } if (has_arg(argc, argv, "logs-timer")) { for (int i = 0; i < 10; i++) { sentry_log_info("Informational log nr.%d", i); diff --git a/include/sentry.h b/include/sentry.h index d642fd133e..2969f2f0c7 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -1883,6 +1883,23 @@ SENTRY_EXPERIMENTAL_API void sentry_log_warn(const char *message, ...); SENTRY_EXPERIMENTAL_API void sentry_log_error(const char *message, ...); SENTRY_EXPERIMENTAL_API void sentry_log_fatal(const char *message, ...); +/** + * Type of the `before_send_log` callback. + * + * The callback takes ownership of the `log`, and should usually return + * that same log. In case the log should be discarded, the + * callback needs to call `sentry_value_decref` on the provided log, and + * return a `sentry_value_new_null()` instead. + */ +typedef sentry_value_t (*sentry_before_send_log_function_t)( + sentry_value_t log, void *user_data); + +/** + * Sets the `before_send_log` callback. + */ +SENTRY_EXPERIMENTAL_API void sentry_options_set_before_send_log( + sentry_options_t *opts, sentry_before_send_log_function_t func, void *data); + #ifdef SENTRY_PLATFORM_LINUX /** diff --git a/src/sentry_logs.c b/src/sentry_logs.c index fdbf36167f..7976bd35c4 100644 --- a/src/sentry_logs.c +++ b/src/sentry_logs.c @@ -328,12 +328,13 @@ skip_length(const char *fmt_ptr) return fmt_ptr; } -static void +// returns how many parameters were added to the attributes object +static int populate_message_parameters( sentry_value_t attributes, const char *message, va_list args) { if (!message || sentry_value_is_null(attributes)) { - return; + return 0; } const char *fmt_ptr = message; @@ -375,6 +376,7 @@ populate_message_parameters( } va_end(args_copy); + return param_index; } static void @@ -494,11 +496,13 @@ construct_log(sentry_level_t level, const char *message, va_list args) "string", "sentry.sdk.name"); add_attribute(attributes, sentry_value_new_string(sentry_sdk_version()), "string", "sentry.sdk.version"); - add_attribute(attributes, sentry_value_new_string(message), "string", - "sentry.message.template"); // Parse variadic arguments and add them to attributes - populate_message_parameters(attributes, message, args_copy_3); + if (populate_message_parameters(attributes, message, args_copy_3)) { + // only add message template if we have parameters + add_attribute(attributes, sentry_value_new_string(message), "string", + "sentry.message.template"); + } va_end(args_copy_3); sentry_value_set_by_key(log, "attributes", attributes); @@ -506,6 +510,7 @@ construct_log(sentry_level_t level, const char *message, va_list args) return log; } +// TODO change to int return void sentry__logs_log(sentry_level_t level, const char *message, va_list args) { @@ -514,9 +519,24 @@ sentry__logs_log(sentry_level_t level, const char *message, va_list args) if (options->enable_logs) enable_logs = true; } + int discarded = false; if (enable_logs) { // create log from message sentry_value_t log = construct_log(level, message, args); + SENTRY_WITH_OPTIONS (options) { + if (options->before_send_log_func) { + log = options->before_send_log_func( + log, options->before_send_log_data); + if (sentry_value_is_null(log)) { + SENTRY_DEBUG( + "log was discarded by the `before_send_log` hook"); + discarded = true; + } + } + } + if (discarded) { + return; + } if (!enqueue_log_single(log)) { sentry_value_decref(log); } diff --git a/src/sentry_options.c b/src/sentry_options.c index 76f3d2352b..fa42b33c54 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -153,6 +153,14 @@ sentry_options_set_before_transaction( opts->before_transaction_data = user_data; } +void +sentry_options_set_before_send_log(sentry_options_t *opts, + sentry_before_send_log_function_t func, void *user_data) +{ + opts->before_send_log_func = func; + opts->before_send_log_data = user_data; +} + void sentry_options_set_dsn_n( sentry_options_t *opts, const char *raw_dsn, size_t raw_dsn_len) diff --git a/src/sentry_options.h b/src/sentry_options.h index ad7d9b11e7..4eb03abc53 100644 --- a/src/sentry_options.h +++ b/src/sentry_options.h @@ -52,6 +52,8 @@ struct sentry_options_s { void *on_crash_data; sentry_transaction_function_t before_transaction_func; void *before_transaction_data; + sentry_before_send_log_function_t before_send_log_func; + void *before_send_log_data; /* Experimentally exposed */ double traces_sample_rate; diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index 54c0338e66..6e8f53d25b 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -1382,3 +1382,66 @@ def test_logs_threaded(cmake, httpserver): total_count += envelope.items[0].headers["item_count"] print(f"Total amount of captured logs: {total_count}") assert total_count >= 100 + + +def test_before_send_log(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-logs", "capture-log", "before-send-log"], + check=True, + 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 log item + (log_item,) = envelope.items + + assert log_item.headers["type"] == "log" + payload = log_item.payload.json + + # Get the first log item from the logs payload + log_entry = payload["items"][0] + attributes = log_entry["attributes"] + + # Check that the before_send_log 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_log_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), SENTRY_RELEASE="🤮🚀") + + run( + tmp_path, + "sentry_example", + ["log", "enable-logs", "capture-log", "discarding-before-send-log"], + check=True, + env=env, + ) + + # log should have been discarded + assert len(httpserver.log) == 0