Skip to content

Commit c18e451

Browse files
feat(logs): batching (#1338)
* initial queue attempt * add timer * prototype double buffer approach * update logs unit tests for batching * replace timer with bgworker * add first integration tests * update example.c with correct log thread amounts * cleanup * add wait for 'adding' logs in flush logic * go back to single queue for performance testing * add time checks * add ToDos + cleanup sentry_value_t cloning * initial attempt * cond_wait for timer + 'adding' spinlock * add sleep for tests * add sleep for tests * force flush before attempting timer_worker shutdown * add proper cond_wait for 'adding' counter * revert to manual flush on shutdown instead of timer thread flush * add separate timer_stop atomic * cleanup + replace 'adding' cond_wait by pure spinlock * change bgworker for simpler thread implementation * cleanup * fix memleak * fixes * cleanup * cleanup * windows fixes * update shutdown order * fixes * explicit check to stop timer task * windows cleanup * loosen threaded test assertion for CI - too much variability in thread scheduling, so we can expect pretty much anything * add continue for unexpected logs flush trigger instead of attempting flush * Windows re-add condition variable trigger case * feat(logs): add `before_send_log` (#1355) * add `before_send_log` callback * add `before_send_log` callback tests * (temporary) add debug for calling sentry_options_free * remove early return * add late return * cleanup * fix ownership issues in single buffer batching (#1362) * let the producer thread sleep for 1ms between logs * fix two missing NULL checks in the json serializer * clean up logging and early exits in `enqueue_log_single()` * clean up ownerships in logs * eliminate clones (we expect that everything outlives the logs being sent except local construction) * use incref everywhere where we ref global state. this was the cause of the UAF, partially solved with the clones but a few were missing. no reason to clone if we do not want to disconnect for a particular object graph * clarify that add_attribute expects ownership * minimize scope_mut by moving os_context out * raise that log output in throughput tests add to variability (stdout logging should be turned off when running a limit) * log error in case we weren't able to start the log batching worker * fix clang-cl warning * ci: fix failing mingw build (#1361) * ci: fix failing mingw build * split `ASM_MASM_COMPILER` and `_FLAGS` * add `ASM_MASM_FLAGS` in `mingw` install step * specify the `CMAKE_ASM_MASM_COMPILER` as a `FILEPATH` * clean up CMAKE_DEFINES construction so it is easier to diff in the future * fix `LLVM_MINGW_INSTALL_PATH` to be referenced locally rather than $env (cherry picked from commit 519554f) * use UNREACHABLE macro to fix anal warnings * batching double buffered (#1365) * first attempt at double buffered * remove the sleep from the windows thread func * clean up thread waiting in the example * adapt the double buffer to use retries, fix remaining issues, clean up and write inline docs * return early in example on sentry_init error. * fix formatting via shorter name for thread gate atomic * improve inline docs of log_buffer_t members * fetch os_context from scope * move scope/options data retrieval into separate function + add expected keys to test * update logs API to return status code * cleanup * add log-event trace connection test * remove duplicate test * specify macOS SDKROOT --------- Co-authored-by: JoshuaMoelans <[email protected]> * add flush retry for missed flush requests * move flush retry into flush function * add docs --------- Co-authored-by: Mischan Toosarani-Hausberger <[email protected]>
1 parent 8f0fa12 commit c18e451

File tree

15 files changed

+877
-126
lines changed

15 files changed

+877
-126
lines changed

.github/workflows/ci.yml

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,8 @@ jobs:
150150
os: windows-latest
151151
TEST_MINGW: 1
152152
MINGW_PKG_PREFIX: x86_64-w64-mingw32
153-
MINGW_ASM_MASM_COMPILER: llvm-ml;-m64
153+
MINGW_ASM_MASM_COMPILER: llvm-ml
154+
MINGW_ASM_MASM_FLAGS: -m64
154155
# The Android emulator is currently only available on macos, see:
155156
# https://docs.microsoft.com/en-us/azure/devops/pipelines/ecosystems/android?view=azure-devops#test-on-the-android-emulator
156157
# TODO: switch to reactivecircus/android-emulator-runner, concurrently running emulators continuously fail now.
@@ -244,12 +245,32 @@ jobs:
244245
if: ${{ runner.os == 'macOS' && matrix.os == 'macos-15-large' && matrix.RUN_ANALYZER == 'asan,llvm-cov' }}
245246
run: echo $(brew --prefix llvm@18)/bin >> $GITHUB_PATH
246247

248+
- name: Set macOS SDKROOT
249+
if: ${{ runner.os == 'macOS' }}
250+
run: echo "SDKROOT=macosx" >> "$GITHUB_ENV"
251+
252+
- name: Remove Strawberry Perl from PATH
253+
if: ${{ runner.os == 'Windows' }}
254+
shell: powershell
255+
run: |
256+
$strawberryBins = @(
257+
'C:\Strawberry\c\bin',
258+
'C:\Strawberry\perl\site\bin',
259+
'C:\Strawberry\perl\bin'
260+
)
261+
262+
$cleanedPaths = $env:Path -split ';' | Where-Object { $_ -and ($strawberryBins -notcontains $_) }
263+
$newPath = ($cleanedPaths -join ';')
264+
265+
"PATH=$newPath" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
266+
247267
- name: Installing LLVM-MINGW Dependencies
248268
if: ${{ runner.os == 'Windows' && env['TEST_MINGW'] }}
249269
shell: powershell
250270
env:
251271
MINGW_PKG_PREFIX: ${{ matrix.MINGW_PKG_PREFIX }}
252272
MINGW_ASM_MASM_COMPILER: ${{ matrix.MINGW_ASM_MASM_COMPILER }}
273+
MINGW_ASM_MASM_FLAGS: ${{ matrix.MINGW_ASM_MASM_FLAGS }}
253274
run: . "scripts\install-llvm-mingw.ps1"
254275

255276
- name: Set up zlib for Windows

examples/example.c

Lines changed: 107 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
# define sleep_s(SECONDS) Sleep((SECONDS) * 1000)
2525
#else
2626

27+
# include <pthread.h>
2728
# include <signal.h>
2829
# include <unistd.h>
2930

@@ -58,6 +59,8 @@ get_current_thread_id()
5859
}
5960
#endif
6061

62+
#define NUM_THREADS 50
63+
6164
static double
6265
traces_sampler_callback(const sentry_transaction_context_t *transaction_ctx,
6366
sentry_value_t custom_sampling_ctx, const int *parent_sampled)
@@ -155,6 +158,33 @@ discarding_before_transaction_callback(sentry_value_t tx, void *user_data)
155158
return tx;
156159
}
157160

161+
static sentry_value_t
162+
before_send_log_callback(sentry_value_t log, void *user_data)
163+
{
164+
(void)user_data;
165+
sentry_value_t attribute = sentry_value_new_object();
166+
sentry_value_set_by_key(
167+
attribute, "value", sentry_value_new_string("little"));
168+
sentry_value_set_by_key(
169+
attribute, "type", sentry_value_new_string("string"));
170+
sentry_value_set_by_key(sentry_value_get_by_key(log, "attributes"),
171+
"coffeepot.size", attribute);
172+
return log;
173+
}
174+
175+
static sentry_value_t
176+
discarding_before_send_log_callback(sentry_value_t log, void *user_data)
177+
{
178+
(void)user_data;
179+
if (sentry_value_is_null(
180+
sentry_value_get_by_key(sentry_value_get_by_key(log, "attributes"),
181+
"sentry.message.template"))) {
182+
sentry_value_decref(log);
183+
return sentry_value_new_null();
184+
}
185+
return log;
186+
}
187+
158188
static void
159189
print_envelope(sentry_envelope_t *envelope, void *unused_state)
160190
{
@@ -272,6 +302,32 @@ create_debug_crumb(const char *message)
272302
return debug_crumb;
273303
}
274304

305+
#ifdef SENTRY_PLATFORM_WINDOWS
306+
DWORD WINAPI
307+
log_thread_func(LPVOID lpParam)
308+
{
309+
(void)lpParam;
310+
int LOG_COUNT = 100;
311+
for (int i = 0; i < LOG_COUNT; i++) {
312+
sentry_log_debug(
313+
"thread log %d on thread %lu", i, get_current_thread_id());
314+
}
315+
return 0;
316+
}
317+
#else
318+
void *
319+
log_thread_func(void *arg)
320+
{
321+
(void)arg;
322+
int LOG_COUNT = 100;
323+
for (int i = 0; i < LOG_COUNT; i++) {
324+
sentry_log_debug(
325+
"thread log %d on thread %llu", i, get_current_thread_id());
326+
}
327+
return NULL;
328+
}
329+
#endif
330+
275331
int
276332
main(int argc, char **argv)
277333
{
@@ -345,6 +401,16 @@ main(int argc, char **argv)
345401
options, discarding_before_transaction_callback, NULL);
346402
}
347403

404+
if (has_arg(argc, argv, "before-send-log")) {
405+
sentry_options_set_before_send_log(
406+
options, before_send_log_callback, NULL);
407+
}
408+
409+
if (has_arg(argc, argv, "discarding-before-send-log")) {
410+
sentry_options_set_before_send_log(
411+
options, discarding_before_send_log_callback, NULL);
412+
}
413+
348414
if (has_arg(argc, argv, "traces-sampler")) {
349415
sentry_options_set_traces_sampler(options, traces_sampler_callback);
350416
}
@@ -385,7 +451,9 @@ main(int argc, char **argv)
385451
sentry_options_set_enable_logs(options, true);
386452
}
387453

388-
sentry_init(options);
454+
if (0 != sentry_init(options)) {
455+
return EXIT_FAILURE;
456+
}
389457

390458
if (has_arg(argc, argv, "attachment")) {
391459
sentry_attachment_t *bytes
@@ -395,28 +463,42 @@ main(int argc, char **argv)
395463

396464
// TODO incorporate into test
397465
if (sentry_options_get_enable_logs(options)) {
398-
sentry_log_warn(
399-
"This is a big number %" PRIu64, 18446744073709551615ULL);
400-
sentry_log_warn("This is a medium number as unsigned %" PRIu64,
401-
9007199254740991ULL);
402-
sentry_log_warn(
403-
"This is a medium number as signed %" PRId64, 9007199254740991LL);
404-
sentry_log_trace("We log it up %i%%, %s style", 100, "trace");
405-
sentry_log_debug("We log it up %i%%, %s style", 100, "debug");
406-
sentry_log_info("We log it up %i%%, %s style", 100, "info");
407-
sentry_log_warn("We log it up %i%%, %s style", 100, "warn");
408-
sentry_log_error("We log it up %i%%, %s style", 100, "error");
409-
sentry_log_fatal("We log it up %i%%, %s style", 100, "fatal");
410-
411-
// Test the logger with various parameter types
412-
sentry_log_info(
413-
"API call to %s completed in %d ms with %f success rate",
414-
"/api/products", 2500, 0.95);
415-
416-
sentry_log_warn("Processing %d items, found %u errors, pointer: %p",
417-
100, 5u, (void *)0x12345678);
418-
419-
sentry_log_error("Character '%c' is invalid", 'X');
466+
if (has_arg(argc, argv, "capture-log")) {
467+
sentry_log_debug("I'm a log message!");
468+
}
469+
if (has_arg(argc, argv, "logs-timer")) {
470+
for (int i = 0; i < 10; i++) {
471+
sentry_log_info("Informational log nr.%d", i);
472+
}
473+
// sleep >5s to trigger logs timer
474+
sleep_s(6);
475+
// we should see two envelopes make its way to Sentry
476+
sentry_log_debug("post-sleep log");
477+
}
478+
if (has_arg(argc, argv, "logs-threads")) {
479+
// Spawn multiple threads to test concurrent logging
480+
#ifdef SENTRY_PLATFORM_WINDOWS
481+
HANDLE threads[NUM_THREADS];
482+
for (int t = 0; t < NUM_THREADS; t++) {
483+
threads[t]
484+
= CreateThread(NULL, 0, log_thread_func, NULL, 0, NULL);
485+
}
486+
487+
WaitForMultipleObjects(NUM_THREADS, threads, TRUE, INFINITE);
488+
489+
for (int t = 0; t < NUM_THREADS; t++) {
490+
CloseHandle(threads[t]);
491+
}
492+
#else
493+
pthread_t threads[NUM_THREADS];
494+
for (int t = 0; t < NUM_THREADS; t++) {
495+
pthread_create(&threads[t], NULL, log_thread_func, NULL);
496+
}
497+
for (int t = 0; t < NUM_THREADS; t++) {
498+
pthread_join(threads[t], NULL);
499+
}
500+
#endif
501+
}
420502
}
421503

422504
if (!has_arg(argc, argv, "no-setup")) {
@@ -681,4 +763,6 @@ main(int argc, char **argv)
681763
if (has_arg(argc, argv, "crash-after-shutdown")) {
682764
trigger_crash();
683765
}
766+
767+
return EXIT_SUCCESS;
684768
}

include/sentry.h

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1876,12 +1876,41 @@ SENTRY_EXPERIMENTAL_API void sentry_options_set_enable_logs(
18761876
SENTRY_EXPERIMENTAL_API int sentry_options_get_enable_logs(
18771877
const sentry_options_t *opts);
18781878

1879-
SENTRY_EXPERIMENTAL_API void sentry_log_trace(const char *message, ...);
1880-
SENTRY_EXPERIMENTAL_API void sentry_log_debug(const char *message, ...);
1881-
SENTRY_EXPERIMENTAL_API void sentry_log_info(const char *message, ...);
1882-
SENTRY_EXPERIMENTAL_API void sentry_log_warn(const char *message, ...);
1883-
SENTRY_EXPERIMENTAL_API void sentry_log_error(const char *message, ...);
1884-
SENTRY_EXPERIMENTAL_API void sentry_log_fatal(const char *message, ...);
1879+
typedef enum {
1880+
SENTRY_LOG_RETURN_SUCCESS = 0,
1881+
SENTRY_LOG_RETURN_DISCARD = 1,
1882+
SENTRY_LOG_RETURN_FAILED = 2,
1883+
SENTRY_LOG_RETURN_DISABLED = 3
1884+
} log_return_value_t;
1885+
SENTRY_EXPERIMENTAL_API log_return_value_t sentry_log_trace(
1886+
const char *message, ...);
1887+
SENTRY_EXPERIMENTAL_API log_return_value_t sentry_log_debug(
1888+
const char *message, ...);
1889+
SENTRY_EXPERIMENTAL_API log_return_value_t sentry_log_info(
1890+
const char *message, ...);
1891+
SENTRY_EXPERIMENTAL_API log_return_value_t sentry_log_warn(
1892+
const char *message, ...);
1893+
SENTRY_EXPERIMENTAL_API log_return_value_t sentry_log_error(
1894+
const char *message, ...);
1895+
SENTRY_EXPERIMENTAL_API log_return_value_t sentry_log_fatal(
1896+
const char *message, ...);
1897+
1898+
/**
1899+
* Type of the `before_send_log` callback.
1900+
*
1901+
* The callback takes ownership of the `log`, and should usually return
1902+
* that same log. In case the log should be discarded, the
1903+
* callback needs to call `sentry_value_decref` on the provided log, and
1904+
* return a `sentry_value_new_null()` instead.
1905+
*/
1906+
typedef sentry_value_t (*sentry_before_send_log_function_t)(
1907+
sentry_value_t log, void *user_data);
1908+
1909+
/**
1910+
* Sets the `before_send_log` callback.
1911+
*/
1912+
SENTRY_EXPERIMENTAL_API void sentry_options_set_before_send_log(
1913+
sentry_options_t *opts, sentry_before_send_log_function_t func, void *data);
18851914

18861915
#ifdef SENTRY_PLATFORM_LINUX
18871916

scripts/install-llvm-mingw.ps1

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,19 @@ Expand-Archive -LiteralPath "${NINJA_DL_PATH}" -DestinationPath "${NINJA_INSTALL
5555
"PATH=${NINJA_INSTALL_PATH}; $env:PATH" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
5656

5757
# export CMAKE_DEFINES to the runner environment
58-
"CMAKE_DEFINES=${env:CMAKE_DEFINES} -DCMAKE_C_COMPILER=${env:MINGW_PKG_PREFIX}-gcc -DCMAKE_CXX_COMPILER=${env:MINGW_PKG_PREFIX}-g++ -DCMAKE_RC_COMPILER=${env:MINGW_PKG_PREFIX}-windres -DCMAKE_ASM_MASM_COMPILER=${env:MINGW_ASM_MASM_COMPILER}" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
58+
$cmakeDefines = @()
59+
60+
if ($env:CMAKE_DEFINES) {
61+
$cmakeDefines += $env:CMAKE_DEFINES
62+
}
63+
64+
$cmakeDefines += @(
65+
"-DCMAKE_C_COMPILER=$($env:MINGW_PKG_PREFIX)-gcc"
66+
"-DCMAKE_CXX_COMPILER=$($env:MINGW_PKG_PREFIX)-g++"
67+
"-DCMAKE_RC_COMPILER=$($env:MINGW_PKG_PREFIX)-windres"
68+
"-DCMAKE_ASM_MASM_COMPILER:FILEPATH=$($LLVM_MINGW_INSTALL_PATH -replace '\\','/')/bin/$($env:MINGW_ASM_MASM_COMPILER).exe"
69+
"-DCMAKE_ASM_MASM_FLAGS=$env:MINGW_ASM_MASM_FLAGS"
70+
)
71+
72+
"CMAKE_DEFINES=$($cmakeDefines -join ' ')" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
73+

src/sentry_core.c

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include "sentry_core.h"
99
#include "sentry_database.h"
1010
#include "sentry_envelope.h"
11+
#include "sentry_logs.h"
1112
#include "sentry_options.h"
1213
#include "sentry_os.h"
1314
#include "sentry_path.h"
@@ -284,6 +285,10 @@ sentry_init(sentry_options_t *options)
284285
sentry_start_session();
285286
}
286287

288+
if (options->enable_logs) {
289+
sentry__logs_startup();
290+
}
291+
287292
sentry__mutex_unlock(&g_options_lock);
288293
return 0;
289294

@@ -310,6 +315,16 @@ sentry_flush(uint64_t timeout)
310315
int
311316
sentry_close(void)
312317
{
318+
// Shutdown logs system before locking options to ensure logs are flushed
319+
// TODO is this the only way? we got a potential deadlock on the options
320+
// otherwise
321+
// (for envelope creation, see sentry__envelope_new)
322+
SENTRY_WITH_OPTIONS (options) {
323+
if (options->enable_logs) {
324+
sentry__logs_shutdown(options->shutdown_timeout);
325+
}
326+
}
327+
313328
SENTRY__MUTEX_INIT_DYN_ONCE(g_options_lock);
314329
// this function is to be called only once, so we do not allow more than one
315330
// caller

src/sentry_envelope.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,9 +420,12 @@ sentry__envelope_add_logs(sentry_envelope_t *envelope, sentry_value_t logs)
420420
sentry__envelope_item_set_header(
421421
item, "type", sentry_value_new_string("log"));
422422
sentry__envelope_item_set_header(item, "item_count",
423-
sentry_value_new_int32((int32_t)sentry_value_get_length(logs)));
423+
sentry_value_new_int32((int32_t)sentry_value_get_length(
424+
sentry_value_get_by_key(logs, "items"))));
424425
sentry__envelope_item_set_header(item, "content_type",
425426
sentry_value_new_string("application/vnd.sentry.items.log+json"));
427+
sentry_value_t length = sentry_value_new_int32((int32_t)item->payload_len);
428+
sentry__envelope_item_set_header(item, "length", length);
426429

427430
return item;
428431
}

0 commit comments

Comments
 (0)