diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74ae91cbf..a8b13fc9f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -121,7 +121,7 @@ jobs: SYSTEM_VERSION_COMPAT: 0 RUN_ANALYZER: asan,llvm-cov - name: Windows (old VS, 32-bit) - os: windows-2019 + os: windows-2022 TEST_X86: 1 - name: Windows (latest) os: windows-latest diff --git a/examples/example.c b/examples/example.c index 4e1790d5d..9f6602822 100644 --- a/examples/example.c +++ b/examples/example.c @@ -359,6 +359,26 @@ main(int argc, char **argv) sentry_init(options); + // TODO incorporate into test + if (sentry_options_get_enable_logs(options)) { + sentry_log_trace("We log it up %i%%, %s style", 100, "trace"); + sentry_log_debug("We log it up %i%%, %s style", 100, "debug"); + sentry_log_info("We log it up %i%%, %s style", 100, "info"); + sentry_log_warn("We log it up %i%%, %s style", 100, "warn"); + sentry_log_error("We log it up %i%%, %s style", 100, "error"); + sentry_log_fatal("We log it up %i%%, %s style", 100, "fatal"); + + // Test the logger with various parameter types + sentry_log_info( + "API call to %s completed in %d ms with %f success rate", + "/api/products", 2500, 0.95); + + sentry_log_warn("Processing %d items, found %u errors, pointer: %p", + 100, 5u, (void *)0x12345678); + + sentry_log_error("Character '%c' is invalid", 'X'); + } + if (!has_arg(argc, argv, "no-setup")) { sentry_set_transaction("test-transaction"); sentry_set_level(SENTRY_LEVEL_WARNING); @@ -566,6 +586,9 @@ main(int argc, char **argv) SENTRY_LEVEL_INFO, "my-logger", "Hello World!"); sentry_capture_event(event); } + if (sentry_options_get_enable_logs(options)) { + sentry_log_debug("logging after scoped transaction event"); + } sentry_transaction_finish(tx); } diff --git a/include/sentry.h b/include/sentry.h index b5ceb617a..0a9ccb62d 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -137,6 +137,8 @@ typedef enum { SENTRY_VALUE_TYPE_NULL, SENTRY_VALUE_TYPE_BOOL, SENTRY_VALUE_TYPE_INT32, + SENTRY_VALUE_TYPE_INT64, + SENTRY_VALUE_TYPE_UINT64, SENTRY_VALUE_TYPE_DOUBLE, SENTRY_VALUE_TYPE_STRING, SENTRY_VALUE_TYPE_LIST, @@ -200,6 +202,16 @@ SENTRY_API sentry_value_t sentry_value_new_null(void); */ SENTRY_API sentry_value_t sentry_value_new_int32(int32_t value); +/** + * Creates a new 64-bit signed integer value. + */ +SENTRY_API sentry_value_t sentry_value_new_int64(int64_t value); + +/** + * Creates a new 64-bit unsigned integer value. + */ +SENTRY_API sentry_value_t sentry_value_new_uint64(uint64_t value); + /** * Creates a new double value. */ @@ -338,6 +350,16 @@ SENTRY_API size_t sentry_value_get_length(sentry_value_t value); */ SENTRY_API int32_t sentry_value_as_int32(sentry_value_t value); +/** + * Converts a value into a 64 bit signed integer. + */ +SENTRY_API int64_t sentry_value_as_int64(sentry_value_t value); + +/** + * Converts a value into a 64 bit unsigned integer. + */ +SENTRY_API uint64_t sentry_value_as_uint64(sentry_value_t value); + /** * Converts a value into a double value. */ @@ -370,6 +392,7 @@ SENTRY_API char *sentry_value_to_json(sentry_value_t value); * Sentry levels for events and breadcrumbs. */ typedef enum sentry_level_e { + SENTRY_LEVEL_TRACE = -2, SENTRY_LEVEL_DEBUG = -1, SENTRY_LEVEL_INFO = 0, SENTRY_LEVEL_WARNING = 1, @@ -1750,6 +1773,13 @@ SENTRY_EXPERIMENTAL_API void sentry_options_set_enable_logs( SENTRY_EXPERIMENTAL_API int sentry_options_get_enable_logs( const sentry_options_t *opts); +SENTRY_EXPERIMENTAL_API void sentry_log_trace(const char *message, ...); +SENTRY_EXPERIMENTAL_API void sentry_log_debug(const char *message, ...); +SENTRY_EXPERIMENTAL_API void sentry_log_info(const char *message, ...); +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, ...); + #ifdef SENTRY_PLATFORM_LINUX /** diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 700f79753..171da4ab1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -15,6 +15,8 @@ sentry_target_sources_cwd(sentry sentry_json.h sentry_logger.c sentry_logger.h + sentry_logs.c + sentry_logs.h sentry_options.c sentry_options.h sentry_os.c diff --git a/src/sentry_core.c b/src/sentry_core.c index 24d7a94bf..0bcfd2472 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -125,6 +125,7 @@ sentry_init(sentry_options_t *options) sentry_close(); sentry_logger_t logger = { NULL, NULL, SENTRY_LEVEL_DEBUG }; + if (options->debug) { logger = options->logger; } diff --git a/src/sentry_envelope.c b/src/sentry_envelope.c index 137115c86..da67ae2de 100644 --- a/src/sentry_envelope.c +++ b/src/sentry_envelope.c @@ -295,6 +295,32 @@ sentry__envelope_add_transaction( return item; } +sentry_envelope_item_t * +sentry__envelope_add_logs(sentry_envelope_t *envelope, sentry_value_t logs) +{ + 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, logs); + item->payload = sentry__jsonwriter_into_string(jw, &item->payload_len); + + sentry__envelope_item_set_header( + item, "type", sentry_value_new_string("log")); + sentry__envelope_item_set_header(item, "item_count", + sentry_value_new_int32((int32_t)sentry_value_get_length(logs))); + sentry__envelope_item_set_header(item, "content_type", + sentry_value_new_string("application/vnd.sentry.items.log+json")); + + return item; +} + sentry_envelope_item_t * sentry__envelope_add_user_feedback( sentry_envelope_t *envelope, sentry_value_t user_feedback) diff --git a/src/sentry_envelope.h b/src/sentry_envelope.h index 842cbb475..4cfc7d90f 100644 --- a/src/sentry_envelope.h +++ b/src/sentry_envelope.h @@ -42,6 +42,12 @@ sentry_envelope_item_t *sentry__envelope_add_event( sentry_envelope_item_t *sentry__envelope_add_transaction( sentry_envelope_t *envelope, sentry_value_t transaction); +/** + * Add a list of logs to this envelope. + */ +sentry_envelope_item_t *sentry__envelope_add_logs( + sentry_envelope_t *envelope, sentry_value_t logs); + /** * Add a user feedback to this envelope. */ diff --git a/src/sentry_json.c b/src/sentry_json.c index 8902406f7..ea7454f81 100644 --- a/src/sentry_json.c +++ b/src/sentry_json.c @@ -364,6 +364,26 @@ sentry__jsonwriter_write_int32(sentry_jsonwriter_t *jw, int32_t val) } } +void +sentry__jsonwriter_write_int64(sentry_jsonwriter_t *jw, int64_t val) +{ + if (can_write_item(jw)) { + char buf[24]; + snprintf(buf, sizeof(buf), "%" PRId64, val); + write_str(jw, buf); + } +} + +void +sentry__jsonwriter_write_uint64(sentry_jsonwriter_t *jw, uint64_t val) +{ + if (can_write_item(jw)) { + char buf[24]; + snprintf(buf, sizeof(buf), "%" PRIu64, val); + write_str(jw, buf); + } +} + void sentry__jsonwriter_write_double(sentry_jsonwriter_t *jw, double val) { diff --git a/src/sentry_json.h b/src/sentry_json.h index 1467547c0..20f302518 100644 --- a/src/sentry_json.h +++ b/src/sentry_json.h @@ -53,6 +53,16 @@ void sentry__jsonwriter_write_bool(sentry_jsonwriter_t *jw, bool val); */ void sentry__jsonwriter_write_int32(sentry_jsonwriter_t *jw, int32_t val); +/** + * Write a 64-bit signed integer, encoded as JSON number. + */ +void sentry__jsonwriter_write_int64(sentry_jsonwriter_t *jw, int64_t val); + +/** + * Write a 64-bit unsigned integer, encoded as JSON number. + */ +void sentry__jsonwriter_write_uint64(sentry_jsonwriter_t *jw, uint64_t val); + /** * Write a 64-bit float, encoded as JSON number. */ diff --git a/src/sentry_logger.c b/src/sentry_logger.c index 98b965d7a..642c9433c 100644 --- a/src/sentry_logger.c +++ b/src/sentry_logger.c @@ -69,6 +69,8 @@ const char * sentry__logger_describe(sentry_level_t level) { switch (level) { + case SENTRY_LEVEL_TRACE: + return "TRACE "; case SENTRY_LEVEL_DEBUG: return "DEBUG "; case SENTRY_LEVEL_INFO: @@ -87,8 +89,7 @@ sentry__logger_describe(sentry_level_t level) void sentry__logger_log(sentry_level_t level, const char *message, ...) { - if (g_logger.logger_level != SENTRY_LEVEL_DEBUG - && level < g_logger.logger_level) { + if (level < g_logger.logger_level) { return; } sentry_logger_t logger = g_logger; diff --git a/src/sentry_logger.h b/src/sentry_logger.h index af21b016d..14e4d0451 100644 --- a/src/sentry_logger.h +++ b/src/sentry_logger.h @@ -18,6 +18,11 @@ const char *sentry__logger_describe(sentry_level_t level); void sentry__logger_log(sentry_level_t level, const char *message, ...); +#define SENTRY_TRACEF(message, ...) \ + sentry__logger_log(SENTRY_LEVEL_TRACE, message, __VA_ARGS__) + +#define SENTRY_TRACE(message) sentry__logger_log(SENTRY_LEVEL_TRACE, message) + #define SENTRY_DEBUGF(message, ...) \ sentry__logger_log(SENTRY_LEVEL_DEBUG, message, __VA_ARGS__) diff --git a/src/sentry_logs.c b/src/sentry_logs.c new file mode 100644 index 000000000..9f87e3bd9 --- /dev/null +++ b/src/sentry_logs.c @@ -0,0 +1,421 @@ +#include "sentry_logs.h" +#include "sentry_core.h" +#include "sentry_envelope.h" +#include "sentry_options.h" +#include "sentry_os.h" +#include "sentry_scope.h" + +#include +#include + +static const char * +level_as_string(sentry_level_t level) +{ + switch (level) { + case SENTRY_LEVEL_TRACE: + return "trace"; + case SENTRY_LEVEL_DEBUG: + return "debug"; + case SENTRY_LEVEL_INFO: + return "info"; + case SENTRY_LEVEL_WARNING: + return "warn"; + case SENTRY_LEVEL_ERROR: + return "error"; + case SENTRY_LEVEL_FATAL: + return "fatal"; + default: + return "unknown"; + } +} + +static sentry_value_t +construct_param_from_conversion(const char conversion, va_list *args_copy) +{ + sentry_value_t param_obj = sentry_value_new_object(); + switch (conversion) { + case 'd': + case 'i': { + int val = va_arg(*args_copy, int); + sentry_value_set_by_key( + param_obj, "value", sentry_value_new_int64(val)); + sentry_value_set_by_key( + param_obj, "type", sentry_value_new_string("integer")); + break; + } + case 'u': + case 'x': + case 'X': + case 'o': { + unsigned int val = va_arg(*args_copy, unsigned int); + sentry_value_set_by_key(param_obj, "value", + sentry__value_new_string_owned(sentry__uint64_to_string(val))); + sentry_value_set_by_key( + param_obj, "type", sentry_value_new_string("string")); + break; + } + case 'f': + case 'F': + case 'e': + case 'E': + case 'g': + case 'G': { + double val = va_arg(*args_copy, double); + sentry_value_set_by_key( + param_obj, "value", sentry_value_new_double(val)); + sentry_value_set_by_key( + param_obj, "type", sentry_value_new_string("double")); + break; + } + case 'c': { + int val = va_arg(*args_copy, int); + char str[2] = { (char)val, '\0' }; + sentry_value_set_by_key( + param_obj, "value", sentry_value_new_string(str)); + sentry_value_set_by_key( + param_obj, "type", sentry_value_new_string("string")); + break; + } + case 's': { + const char *val = va_arg(*args_copy, const char *); + if (val) { + sentry_value_set_by_key( + param_obj, "value", sentry_value_new_string(val)); + } else { + sentry_value_set_by_key( + param_obj, "value", sentry_value_new_string("(null)")); + } + sentry_value_set_by_key( + param_obj, "type", sentry_value_new_string("string")); + break; + } + case 'p': { + void *val = va_arg(*args_copy, void *); + char ptr_str[32]; + snprintf(ptr_str, sizeof(ptr_str), "%p", val); + sentry_value_set_by_key( + param_obj, "value", sentry_value_new_string(ptr_str)); + sentry_value_set_by_key( + param_obj, "type", sentry_value_new_string("string")); + break; + } + default: + // Unknown format specifier, skip the argument + (void)va_arg(*args_copy, void *); + sentry_value_set_by_key( + param_obj, "value", sentry_value_new_string("(unknown)")); + sentry_value_set_by_key( + param_obj, "type", sentry_value_new_string("string")); + break; + } + + return param_obj; +} + +static const char * +skip_flags(const char *fmt_ptr) +{ + while (*fmt_ptr + && (*fmt_ptr == '-' || *fmt_ptr == '+' || *fmt_ptr == ' ' + || *fmt_ptr == '#' || *fmt_ptr == '0')) { + fmt_ptr++; + } + return fmt_ptr; +} + +static const char * +skip_width(const char *fmt_ptr) +{ + while (*fmt_ptr && (*fmt_ptr >= '0' && *fmt_ptr <= '9')) { + fmt_ptr++; + } + return fmt_ptr; +} + +static const char * +skip_precision(const char *fmt_ptr) +{ + + if (*fmt_ptr == '.') { + fmt_ptr++; + while (*fmt_ptr && (*fmt_ptr >= '0' && *fmt_ptr <= '9')) { + fmt_ptr++; + } + } + return fmt_ptr; +} + +static const char * +skip_length(const char *fmt_ptr) +{ + while (*fmt_ptr + && (*fmt_ptr == 'h' || *fmt_ptr == 'l' || *fmt_ptr == 'L' + || *fmt_ptr == 'z' || *fmt_ptr == 'j' || *fmt_ptr == 't')) { + fmt_ptr++; + } + return fmt_ptr; +} + +static void +populate_message_parameters( + sentry_value_t attributes, const char *message, va_list args) +{ + if (!message || sentry_value_is_null(attributes)) { + return; + } + + const char *fmt_ptr = message; + int param_index = 0; + va_list args_copy; + va_copy(args_copy, args); + + while (*fmt_ptr) { + // Find the next format specifier + if (*fmt_ptr == '%') { + fmt_ptr++; // Skip the '%' + + if (*fmt_ptr == '%') { + // Escaped '%', not a format specifier + fmt_ptr++; + continue; + } + + fmt_ptr = skip_flags(fmt_ptr); + fmt_ptr = skip_width(fmt_ptr); + fmt_ptr = skip_precision(fmt_ptr); + fmt_ptr = skip_length(fmt_ptr); + + // Get the conversion specifier + char conversion = *fmt_ptr; + if (conversion) { + char key[64]; + snprintf(key, sizeof(key), "sentry.message.parameter.%d", + param_index); + sentry_value_t param_obj + = construct_param_from_conversion(conversion, &args_copy); + sentry_value_set_by_key(attributes, key, param_obj); + param_index++; + fmt_ptr++; + } + } else { + fmt_ptr++; + } + } + + va_end(args_copy); +} + +static void +add_attribute(sentry_value_t attributes, sentry_value_t value, const char *type, + const char *name) +{ + sentry_value_t param_obj = sentry_value_new_object(); + sentry_value_set_by_key(param_obj, "value", value); + sentry_value_set_by_key(param_obj, "type", sentry_value_new_string(type)); + sentry_value_set_by_key(attributes, name, param_obj); +} + +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_object(); + + va_list args_copy_1, args_copy_2, args_copy_3; + va_copy(args_copy_1, args); + va_copy(args_copy_2, args); + va_copy(args_copy_3, args); + int len = vsnprintf(NULL, 0, message, args_copy_1) + 1; + va_end(args_copy_1); + size_t size = (size_t)len; + char *fmt_message = sentry_malloc(size); + if (!fmt_message) { + va_end(args_copy_2); + va_end(args_copy_3); + return sentry_value_new_null(); + } + + vsnprintf(fmt_message, size, message, args_copy_2); + va_end(args_copy_2); + + sentry_value_set_by_key(log, "body", sentry_value_new_string(fmt_message)); + sentry_free(fmt_message); + sentry_value_set_by_key( + log, "level", sentry_value_new_string(level_as_string(level))); + + // timestamp in seconds + uint64_t usec_time = sentry__usec_time(); + sentry_value_set_by_key(log, "timestamp", + sentry_value_new_double((double)usec_time / 1000000.0)); + + SENTRY_WITH_SCOPE_MUT (scope) { + sentry_value_set_by_key(log, "trace_id", + sentry__value_clone(sentry_value_get_by_key( + sentry_value_get_by_key(scope->propagation_context, "trace"), + "trace_id"))); + + sentry_value_t parent_span_id = sentry_value_new_object(); + if (scope->transaction_object) { + sentry_value_set_by_key(parent_span_id, "value", + sentry__value_clone(sentry_value_get_by_key( + scope->transaction_object->inner, "span_id"))); + } + + if (scope->span) { + sentry_value_set_by_key(parent_span_id, "value", + sentry__value_clone( + sentry_value_get_by_key(scope->span->inner, "span_id"))); + } + sentry_value_set_by_key( + parent_span_id, "type", sentry_value_new_string("string")); + if (scope->transaction_object || scope->span) { + sentry_value_set_by_key( + attributes, "sentry.trace.parent_span_id", parent_span_id); + } else { + sentry_value_decref(parent_span_id); + } + + if (!sentry_value_is_null(scope->user)) { + sentry_value_t user_id = sentry_value_get_by_key(scope->user, "id"); + if (!sentry_value_is_null(user_id)) { + add_attribute(attributes, user_id, "string", "user.id"); + } + sentry_value_t user_username + = sentry_value_get_by_key(scope->user, "username"); + if (!sentry_value_is_null(user_username)) { + add_attribute(attributes, user_username, "string", "user.name"); + } + sentry_value_t user_email + = sentry_value_get_by_key(scope->user, "email"); + if (!sentry_value_is_null(user_email)) { + add_attribute(attributes, user_email, "string", "user.email"); + } + } + sentry_value_t os_context = sentry__get_os_context(); + if (!sentry_value_is_null(os_context)) { + sentry_value_t os_name = sentry__value_clone( + sentry_value_get_by_key(os_context, "name")); + sentry_value_t os_version = sentry__value_clone( + sentry_value_get_by_key(os_context, "version")); + if (!sentry_value_is_null(os_name)) { + add_attribute(attributes, os_name, "string", "os.name"); + } + if (!sentry_value_is_null(os_version)) { + add_attribute(attributes, os_version, "string", "os.version"); + } + } + sentry_value_decref(os_context); + } + SENTRY_WITH_OPTIONS (options) { + if (options->environment) { + add_attribute(attributes, + sentry_value_new_string(options->environment), "string", + "sentry.environment"); + } + if (options->release) { + add_attribute(attributes, sentry_value_new_string(options->release), + "string", "sentry.release"); + } + } + + add_attribute(attributes, sentry_value_new_string("sentry.native"), + "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); + va_end(args_copy_3); + + sentry_value_set_by_key(log, "attributes", attributes); + + return log; +} + +void +sentry__logs_log(sentry_level_t level, const char *message, va_list args) +{ + bool enable_logs = false; + SENTRY_WITH_OPTIONS (options) { + if (options->enable_logs) + enable_logs = true; + } + if (enable_logs) { + // create log from message + sentry_value_t log = construct_log(level, message, args); + + // TODO split up the code below for batched log sending + // e.g. could we store logs on the scope? + sentry_value_t logs = sentry_value_new_object(); + sentry_value_t logs_list = sentry_value_new_list(); + sentry_value_append(logs_list, log); + sentry_value_set_by_key(logs, "items", logs_list); + // sending of the envelope + sentry_envelope_t *envelope = sentry__envelope_new(); + sentry__envelope_add_logs(envelope, logs); + // TODO remove debug write to file below + // sentry_envelope_write_to_file(envelope, "logs_envelope.json"); + SENTRY_WITH_OPTIONS (options) { + sentry__capture_envelope(options->transport, envelope); + } + // For now, free the logs object since envelope doesn't take + // ownership + sentry_value_decref(logs); + } +} + +void +sentry_log_trace(const char *message, ...) +{ + va_list args; + va_start(args, message); + sentry__logs_log(SENTRY_LEVEL_TRACE, message, args); + va_end(args); +} + +void +sentry_log_debug(const char *message, ...) +{ + va_list args; + va_start(args, message); + sentry__logs_log(SENTRY_LEVEL_DEBUG, message, args); + va_end(args); +} + +void +sentry_log_info(const char *message, ...) +{ + va_list args; + va_start(args, message); + sentry__logs_log(SENTRY_LEVEL_INFO, message, args); + va_end(args); +} + +void +sentry_log_warn(const char *message, ...) +{ + va_list args; + va_start(args, message); + sentry__logs_log(SENTRY_LEVEL_WARNING, message, args); + va_end(args); +} + +void +sentry_log_error(const char *message, ...) +{ + va_list args; + va_start(args, message); + sentry__logs_log(SENTRY_LEVEL_ERROR, message, args); + va_end(args); +} + +void +sentry_log_fatal(const char *message, ...) +{ + va_list args; + va_start(args, message); + sentry__logs_log(SENTRY_LEVEL_FATAL, message, args); + va_end(args); +} diff --git a/src/sentry_logs.h b/src/sentry_logs.h new file mode 100644 index 000000000..5fa6a4a77 --- /dev/null +++ b/src/sentry_logs.h @@ -0,0 +1,8 @@ +#ifndef SENTRY_LOGS_H_INCLUDED +#define SENTRY_LOGS_H_INCLUDED + +#include "sentry_boot.h" + +void sentry__logs_log(sentry_level_t level, const char *message, va_list args); + +#endif diff --git a/src/sentry_string.h b/src/sentry_string.h index 6b98c5766..48fa86f97 100644 --- a/src/sentry_string.h +++ b/src/sentry_string.h @@ -193,6 +193,17 @@ sentry__int64_to_string(int64_t val) return sentry__string_clone(buf); } +/** + * Converts an uint64_t into a string. + */ +static inline char * +sentry__uint64_to_string(uint64_t val) +{ + char buf[24]; + snprintf(buf, sizeof(buf), "%" PRIu64, val); + return sentry__string_clone(buf); +} + #ifdef SENTRY_PLATFORM_WINDOWS /** * Create a utf-8 string from a Wide String. diff --git a/src/sentry_value.c b/src/sentry_value.c index 9e573fe6f..d353573d0 100644 --- a/src/sentry_value.c +++ b/src/sentry_value.c @@ -73,6 +73,8 @@ #define THING_TYPE_OBJECT 1 #define THING_TYPE_STRING 2 #define THING_TYPE_DOUBLE 3 +#define THING_TYPE_INT64 4 +#define THING_TYPE_UINT64 5 /* internal value helpers */ @@ -80,6 +82,8 @@ typedef struct { union { void *_ptr; double _double; + int64_t _i64; + uint64_t _u64; } payload; long refcount; uint8_t type; @@ -106,6 +110,8 @@ static const char * level_as_string(sentry_level_t level) { switch (level) { + case SENTRY_LEVEL_TRACE: + return "trace"; case SENTRY_LEVEL_DEBUG: return "debug"; case SENTRY_LEVEL_WARNING: @@ -311,7 +317,7 @@ sentry_value_new_int32(int32_t value) sentry_value_t sentry_value_new_double(double value) { - thing_t *thing = sentry_malloc(sizeof(thing_t)); + thing_t *thing = SENTRY_MAKE(thing_t); if (!thing) { return sentry_value_new_null(); } @@ -324,6 +330,38 @@ sentry_value_new_double(double value) return rv; } +sentry_value_t +sentry_value_new_int64(int64_t value) +{ + thing_t *thing = SENTRY_MAKE(thing_t); + if (!thing) { + return sentry_value_new_null(); + } + thing->payload._i64 = value; + thing->refcount = 1; + thing->type = (uint8_t)(THING_TYPE_INT64 | THING_TYPE_FROZEN); + + sentry_value_t rv; + rv._bits = (uint64_t)(size_t)thing; + return rv; +} + +sentry_value_t +sentry_value_new_uint64(uint64_t value) +{ + thing_t *thing = SENTRY_MAKE(thing_t); + if (!thing) { + return sentry_value_new_null(); + } + thing->payload._u64 = value; + thing->refcount = 1; + thing->type = (uint8_t)(THING_TYPE_UINT64 | THING_TYPE_FROZEN); + + sentry_value_t rv; + rv._bits = (uint64_t)(size_t)thing; + return rv; +} + sentry_value_t sentry_value_new_bool(int value) { @@ -488,6 +526,10 @@ sentry_value_get_type(sentry_value_t value) return SENTRY_VALUE_TYPE_OBJECT; case THING_TYPE_DOUBLE: return SENTRY_VALUE_TYPE_DOUBLE; + case THING_TYPE_INT64: + return SENTRY_VALUE_TYPE_INT64; + case THING_TYPE_UINT64: + return SENTRY_VALUE_TYPE_UINT64; } UNREACHABLE("invalid thing type"); } else if ((value._bits & TAG_MASK) == TAG_CONST) { @@ -624,6 +666,21 @@ sentry__value_as_uuid(sentry_value_t value) char * sentry__value_stringify(sentry_value_t value) { + // returns empty string if snprintf fails + // (returning -1, so casting this to size_t it becomes > the buffer size) + // or if the value is too large for the buffer +#define STRINGIFY_NUMERIC(fmt, value_fn) \ + do { \ + char buf[24]; \ + size_t written \ + = (size_t)sentry__snprintf_c(buf, sizeof(buf), fmt, value_fn); \ + if (written >= sizeof(buf)) { \ + return sentry__string_clone(""); \ + } \ + buf[written] = '\0'; \ + return sentry__string_clone(buf); \ + } while (0) + switch (sentry_value_get_type(value)) { case SENTRY_VALUE_TYPE_LIST: case SENTRY_VALUE_TYPE_OBJECT: @@ -634,19 +691,17 @@ sentry__value_stringify(sentry_value_t value) sentry_value_is_true(value) ? "true" : "false"); case SENTRY_VALUE_TYPE_STRING: return sentry__string_clone(sentry_value_as_string(value)); + case SENTRY_VALUE_TYPE_INT64: + STRINGIFY_NUMERIC("%" PRIi64, sentry_value_as_int64(value)); + case SENTRY_VALUE_TYPE_UINT64: + STRINGIFY_NUMERIC("%" PRIi64, sentry_value_as_uint64(value)); case SENTRY_VALUE_TYPE_INT32: case SENTRY_VALUE_TYPE_DOUBLE: - default: { - char buf[24]; - size_t written = (size_t)sentry__snprintf_c( - buf, sizeof(buf), "%g", sentry_value_as_double(value)); - if (written >= sizeof(buf)) { - return sentry__string_clone(""); - } - buf[written] = '\0'; - return sentry__string_clone(buf); - } + default: + STRINGIFY_NUMERIC("%g", sentry_value_as_double(value)); } + +#undef STRINGIFY_NUMERIC } sentry_value_t @@ -677,6 +732,8 @@ sentry__value_clone(sentry_value_t value) } case THING_TYPE_STRING: case THING_TYPE_DOUBLE: + case THING_TYPE_INT64: + case THING_TYPE_UINT64: sentry_value_incref(value); return value; default: @@ -871,9 +928,18 @@ sentry_value_as_int32(sentry_value_t value) { if ((value._bits & TAG_MASK) == TAG_INT32) { return (int32_t)((int64_t)value._bits >> 32); - } else { - return 0; } + const thing_t *thing = value_as_thing(value); + if (thing && thing_get_type(thing) == THING_TYPE_INT64) { + SENTRY_WARN("Cannot convert int64 into int32, returning 0"); + } + if (thing && thing_get_type(thing) == THING_TYPE_UINT64) { + SENTRY_WARN("Cannot convert uint64 into int32, returning 0"); + } + if (thing && thing_get_type(thing) == THING_TYPE_DOUBLE) { + SENTRY_WARN("Cannot convert double into int32, returning 0"); + } + return 0; } double @@ -886,9 +952,56 @@ sentry_value_as_double(sentry_value_t value) const thing_t *thing = value_as_thing(value); if (thing && thing_get_type(thing) == THING_TYPE_DOUBLE) { return thing->payload._double; - } else { - return (double)NAN; } + if (thing && thing_get_type(thing) == THING_TYPE_INT64) { + SENTRY_WARN("Cannot convert int64 into double, returning NAN"); + } + if (thing && thing_get_type(thing) == THING_TYPE_UINT64) { + SENTRY_WARN("Cannot convert uint64 into double, returning NAN"); + } + + return (double)NAN; +} + +int64_t +sentry_value_as_int64(sentry_value_t value) +{ + if ((value._bits & TAG_MASK) == TAG_INT32) { + return (int64_t)sentry_value_as_int32(value); + } + + const thing_t *thing = value_as_thing(value); + if (thing && thing_get_type(thing) == THING_TYPE_INT64) { + return thing->payload._i64; + } + if (thing && thing_get_type(thing) == THING_TYPE_UINT64) { + SENTRY_WARN("Cannot convert uint64 into int64, returning 0"); + } + if (thing && thing_get_type(thing) == THING_TYPE_DOUBLE) { + SENTRY_WARN("Cannot convert double into int64, returning 0"); + } + return 0; +} + +uint64_t +sentry_value_as_uint64(sentry_value_t value) +{ + if ((value._bits & TAG_MASK) == TAG_INT32) { + SENTRY_WARN("Cannot convert int32 into uint64, returning 0"); + return 0; + } + + const thing_t *thing = value_as_thing(value); + if (thing && thing_get_type(thing) == THING_TYPE_UINT64) { + return thing->payload._u64; + } + if (thing && thing_get_type(thing) == THING_TYPE_INT64) { + SENTRY_WARN("Cannot convert int64 into uint64, returning 0"); + } + if (thing && thing_get_type(thing) == THING_TYPE_DOUBLE) { + SENTRY_WARN("Cannot convert int64 into uint64, returning 0"); + } + return 0; } const char * @@ -897,9 +1010,8 @@ sentry_value_as_string(sentry_value_t value) const thing_t *thing = value_as_thing(value); if (thing && thing_get_type(thing) == THING_TYPE_STRING) { return (const char *)thing->payload._ptr; - } else { - return ""; } + return ""; } sentry_value_t @@ -936,6 +1048,10 @@ sentry_value_is_true(sentry_value_t value) return 0; case SENTRY_VALUE_TYPE_INT32: return sentry_value_as_int32(value) != 0; + case SENTRY_VALUE_TYPE_INT64: + return sentry_value_as_int64(value) != 0; + case SENTRY_VALUE_TYPE_UINT64: + return sentry_value_as_uint64(value) != 0; case SENTRY_VALUE_TYPE_DOUBLE: return sentry_value_as_double(value) != 0.0; case SENTRY_VALUE_TYPE_STRING: @@ -1000,6 +1116,12 @@ sentry__jsonwriter_write_value(sentry_jsonwriter_t *jw, sentry_value_t value) case SENTRY_VALUE_TYPE_INT32: sentry__jsonwriter_write_int32(jw, sentry_value_as_int32(value)); break; + case SENTRY_VALUE_TYPE_INT64: + sentry__jsonwriter_write_int64(jw, sentry_value_as_int64(value)); + break; + case SENTRY_VALUE_TYPE_UINT64: + sentry__jsonwriter_write_uint64(jw, sentry_value_as_uint64(value)); + break; case SENTRY_VALUE_TYPE_DOUBLE: sentry__jsonwriter_write_double(jw, sentry_value_as_double(value)); break; @@ -1052,6 +1174,12 @@ value_to_msgpack(mpack_writer_t *writer, sentry_value_t value) case SENTRY_VALUE_TYPE_INT32: mpack_write_i32(writer, sentry_value_as_int32(value)); break; + case SENTRY_VALUE_TYPE_INT64: + mpack_write_i64(writer, sentry_value_as_int64(value)); + break; + case SENTRY_VALUE_TYPE_UINT64: + mpack_write_u64(writer, sentry_value_as_uint64(value)); + break; case SENTRY_VALUE_TYPE_DOUBLE: mpack_write_double(writer, sentry_value_as_double(value)); break; diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 23038f408..1c17257b5 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -28,6 +28,7 @@ add_executable(sentry_test_unit test_fuzzfailures.c test_info.c test_logger.c + test_logs.c test_modulefinder.c test_mpack.c test_options.c diff --git a/tests/unit/test_logger.c b/tests/unit/test_logger.c index 89e50553e..ff0a784a2 100644 --- a/tests/unit/test_logger.c +++ b/tests/unit/test_logger.c @@ -51,3 +51,69 @@ SENTRY_TEST(custom_logger) sentry_close(); } } + +static void +test_log_level( + sentry_level_t level, const char *message, va_list args, void *_data) +{ + (void)level; + (void)message; + (void)args; + + logger_test_t *data = _data; + if (data->assert_now) { + data->called += 1; + } +} + +SENTRY_TEST(logger_level) +{ + // Test structure: for each level, test that only messages >= that level are + // logged + const struct { + sentry_level_t level; + int expected_count; // How many of the 5 test messages should be logged + } test_cases[] = { + { SENTRY_LEVEL_TRACE, + 5 }, // All messages: TRACE, DEBUG, INFO, WARN, ERROR + { SENTRY_LEVEL_DEBUG, + 4 }, // DEBUG, INFO, WARN, ERROR (TRACE filtered out) + { SENTRY_LEVEL_INFO, 3 }, // INFO, WARN, ERROR + { SENTRY_LEVEL_WARNING, 2 }, // WARN, ERROR + { SENTRY_LEVEL_ERROR, 1 }, // ERROR only + }; + + for (size_t i = 0; i < 5; i++) { // for each of the 5 logger levels + logger_test_t data = { 0, false }; + + { + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_debug(options, true); + sentry_options_set_logger_level(options, test_cases[i].level); + sentry_options_set_logger(options, test_log_level, &data); + + sentry_init(options); + + data.assert_now = true; + // Test all 5 levels in order from most to least verbose + SENTRY_TRACE("Logging Trace"); // level -2 + SENTRY_DEBUG("Logging Debug"); // level -1 + SENTRY_INFO("Logging Info"); // level 0 + SENTRY_WARN("Logging Warning"); // level 1 + SENTRY_ERROR("Logging Error"); // level 2 + + data.assert_now = false; + + TEST_CHECK_INT_EQUAL(data.called, test_cases[i].expected_count); + + sentry_close(); + } + + { + // *really* clear the logger instance + SENTRY_TEST_OPTIONS_NEW(options); + sentry_init(options); + sentry_close(); + } + } +} diff --git a/tests/unit/test_logs.c b/tests/unit/test_logs.c new file mode 100644 index 000000000..4ad2b4049 --- /dev/null +++ b/tests/unit/test_logs.c @@ -0,0 +1,100 @@ +#include "sentry_logs.h" +#include "sentry_testsupport.h" + +#include "sentry_envelope.h" + +// TODO these tests will need updating after batching is implemented + +static void +validate_logs_envelope(sentry_envelope_t *envelope, void *data) +{ + uint64_t *called = data; + *called += 1; + + // Verify we have at least one envelope item + TEST_CHECK(sentry__envelope_get_item_count(envelope) > 0); + + // Get the first item and check it's a logs item + const sentry_envelope_item_t *item = sentry__envelope_get_item(envelope, 0); + sentry_value_t type_header = sentry__envelope_item_get_header(item, "type"); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(type_header), "log"); + + sentry_envelope_free(envelope); +} + +SENTRY_TEST(basic_logging_functionality) +{ + uint64_t called_transport = 0; + + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_enable_logs(options, true); + + sentry_transport_t *transport + = sentry_transport_new(validate_logs_envelope); + sentry_transport_set_state(transport, &called_transport); + sentry_options_set_transport(options, transport); + + sentry_init(options); + + // These should not crash and should respect the enable_logs option + sentry_log_trace("Trace message"); + sentry_log_debug("Debug message"); + sentry_log_info("Info message"); + sentry_log_warn("Warning message"); + sentry_log_error("Error message"); + sentry_log_fatal("Fatal message"); + + sentry_close(); + + TEST_CHECK_INT_EQUAL(called_transport, 6); +} + +SENTRY_TEST(logs_disabled_by_default) +{ + uint64_t called_transport = 0; + + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + + sentry_transport_t *transport + = sentry_transport_new(validate_logs_envelope); + sentry_transport_set_state(transport, &called_transport); + sentry_options_set_transport(options, transport); + + // Don't explicitly enable logs - they should be disabled by default + sentry_init(options); + + sentry_log_info("This should not be sent"); + + sentry_close(); + + // Transport should not be called since logs are disabled + TEST_CHECK_INT_EQUAL(called_transport, 0); +} + +SENTRY_TEST(formatted_log_messages) +{ + uint64_t called_transport = 0; + + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_enable_logs(options, true); + + sentry_transport_t *transport + = sentry_transport_new(validate_logs_envelope); + sentry_transport_set_state(transport, &called_transport); + sentry_options_set_transport(options, transport); + + sentry_init(options); + + // Test format specifiers + sentry_log_info("String: %s, Integer: %d, Float: %.2f", "test", 42, 3.14); + sentry_log_warn("Character: %c, Hex: 0x%x", 'A', 255); + sentry_log_error("Pointer: %p", (void *)0x1234); + + sentry_close(); + + // Transport should be called three times + TEST_CHECK_INT_EQUAL(called_transport, 3); +} diff --git a/tests/unit/test_value.c b/tests/unit/test_value.c index 40d0a2ae5..18457c93b 100644 --- a/tests/unit/test_value.c +++ b/tests/unit/test_value.c @@ -71,6 +71,112 @@ SENTRY_TEST(value_int32) TEST_CHECK(sentry_value_refcount(val) == 1); } +SENTRY_TEST(value_int64) +{ + sentry_value_t val = sentry_value_new_int64(42LL); + TEST_CHECK(sentry_value_get_type(val) == SENTRY_VALUE_TYPE_INT64); + TEST_CHECK(sentry_value_as_int64(val) == 42LL); + // We don't convert int64 to double + TEST_CHECK(isnan(sentry_value_as_double(val))); + // We don't convert int64 to int32 + TEST_CHECK(sentry_value_as_int32(val) == 0); + TEST_CHECK(sentry_value_is_true(val)); + TEST_CHECK_JSON_VALUE(val, "42"); + TEST_CHECK(sentry_value_refcount(val) == 1); + TEST_CHECK(sentry_value_is_frozen(val)); + sentry_value_decref(val); + + // Test large positive value + val = sentry_value_new_int64(INT64_MAX); + TEST_CHECK(sentry_value_get_type(val) == SENTRY_VALUE_TYPE_INT64); + TEST_CHECK(sentry_value_as_int64(val) == INT64_MAX); + TEST_CHECK(sentry_value_is_true(val)); + TEST_CHECK_JSON_VALUE(val, "9223372036854775807"); + sentry_value_decref(val); + + // Test large negative value + val = sentry_value_new_int64(INT64_MIN); + TEST_CHECK(sentry_value_get_type(val) == SENTRY_VALUE_TYPE_INT64); + TEST_CHECK(sentry_value_as_int64(val) == INT64_MIN); + TEST_CHECK(sentry_value_is_true(val)); + TEST_CHECK_JSON_VALUE(val, "-9223372036854775808"); + sentry_value_decref(val); + + // Test zero + val = sentry_value_new_int64(0LL); + TEST_CHECK(sentry_value_get_type(val) == SENTRY_VALUE_TYPE_INT64); + TEST_CHECK(sentry_value_as_int64(val) == 0LL); + TEST_CHECK(!sentry_value_is_true(val)); + TEST_CHECK_JSON_VALUE(val, "0"); + TEST_CHECK(sentry_value_refcount(val) == 1); + TEST_CHECK(sentry_value_is_frozen(val)); + sentry_value_decref(val); + + // We do convert int32 to int64 + val = sentry_value_new_int32(42); + TEST_CHECK(sentry_value_as_int64(val) == 42LL); + sentry_value_decref(val); + + // We don't convert uint64 to int64 + val = sentry_value_new_uint64(-42LL); + TEST_CHECK(sentry_value_as_int64(val) == 0); + sentry_value_decref(val); + + // We don't convert double to int64 + val = sentry_value_new_double(42.99); + TEST_CHECK(sentry_value_as_int64(val) == 0); + sentry_value_decref(val); +} + +SENTRY_TEST(value_uint64) +{ + sentry_value_t val = sentry_value_new_uint64(42ULL); + TEST_CHECK(sentry_value_get_type(val) == SENTRY_VALUE_TYPE_UINT64); + TEST_CHECK(sentry_value_as_uint64(val) == 42ULL); + // We don't convert uint64 to double + TEST_CHECK(isnan(sentry_value_as_double(val))); + // We don't convert uint64 to int32 + TEST_CHECK(sentry_value_as_int32(val) == 0); + TEST_CHECK(sentry_value_is_true(val)); + TEST_CHECK_JSON_VALUE(val, "42"); + TEST_CHECK(sentry_value_refcount(val) == 1); + TEST_CHECK(sentry_value_is_frozen(val)); + sentry_value_decref(val); + + // Test large positive value + val = sentry_value_new_uint64(UINT64_MAX); + TEST_CHECK(sentry_value_get_type(val) == SENTRY_VALUE_TYPE_UINT64); + TEST_CHECK(sentry_value_as_uint64(val) == UINT64_MAX); + TEST_CHECK(sentry_value_is_true(val)); + TEST_CHECK_JSON_VALUE(val, "18446744073709551615"); + sentry_value_decref(val); + + // Test zero + val = sentry_value_new_uint64(0ULL); + TEST_CHECK(sentry_value_get_type(val) == SENTRY_VALUE_TYPE_UINT64); + TEST_CHECK(sentry_value_as_uint64(val) == 0ULL); + TEST_CHECK(!sentry_value_is_true(val)); + TEST_CHECK_JSON_VALUE(val, "0"); + TEST_CHECK(sentry_value_refcount(val) == 1); + TEST_CHECK(sentry_value_is_frozen(val)); + sentry_value_decref(val); + + // We don't convert int32 to uint64 + val = sentry_value_new_int32(42); + TEST_CHECK(sentry_value_as_uint64(val) == 0); + sentry_value_decref(val); + + // We don't convert double to uint64 + val = sentry_value_new_double(123.456); + TEST_CHECK(sentry_value_as_uint64(val) == 0); + sentry_value_decref(val); + + // We don't convert int64 to uint64 + val = sentry_value_new_int64(42LL); + TEST_CHECK(sentry_value_as_uint64(val) == 0); + sentry_value_decref(val); +} + SENTRY_TEST(value_double) { sentry_value_t val = sentry_value_new_double(42.05); @@ -452,7 +558,7 @@ SENTRY_TEST(value_json_parsing) "\"surrogates\":\"𐐷\"}"); sentry_value_decref(rv); - // unmatched surrogates don’t parse + // unmatched surrogates don't parse rv = sentry__value_from_json(STRING("\"\\uD801\"")); TEST_CHECK(sentry_value_is_null(rv)); rv = sentry__value_from_json( @@ -521,7 +627,7 @@ SENTRY_TEST(value_json_surrogates) TEST_CHECK_JSON_VALUE(rv, "{\"surrogates\":\"oh 𐐷 hi\"}"); sentry_value_decref(rv); - // unmatched surrogates don’t parse + // unmatched surrogates don't parse rv = sentry__value_from_json(STRING("\"\\uD801\"")); TEST_CHECK(sentry_value_is_null(rv)); rv = sentry__value_from_json( diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 048f554c5..c836832b3 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -11,6 +11,7 @@ XX(basic_http_request_preparation_for_event_with_attachment) XX(basic_http_request_preparation_for_minidump) XX(basic_http_request_preparation_for_transaction) XX(basic_http_request_preparation_for_user_feedback) +XX(basic_logging_functionality) XX(basic_spans) XX(basic_tracing_context) XX(basic_transaction) @@ -51,6 +52,7 @@ XX(dsn_without_project_id_is_invalid) XX(dsn_without_url_scheme_is_invalid) XX(empty_transport) XX(exception_without_type_or_value_still_valid) +XX(formatted_log_messages) XX(fuzz_json) XX(init_failure) XX(internal_uuid_api) @@ -58,6 +60,8 @@ XX(invalid_dsn) XX(invalid_proxy) XX(iso_time) XX(lazy_attachments) +XX(logger_level) +XX(logs_disabled_by_default) XX(message_with_null_text_is_valid) XX(module_addr) XX(module_finder) @@ -145,6 +149,7 @@ XX(value_double) XX(value_freezing) XX(value_get_by_null_key) XX(value_int32) +XX(value_int64) XX(value_json_deeply_nested) XX(value_json_escaping) XX(value_json_invalid_doubles) @@ -162,6 +167,7 @@ XX(value_set_by_null_key) XX(value_set_stacktrace) XX(value_string) XX(value_string_n) +XX(value_uint64) XX(value_unicode) XX(value_user) XX(value_wrong_type)