Skip to content

Commit 2fa5677

Browse files
committed
feat: add per-thread component/stream fields to log output (#314)
Log lines from long-running worker threads now carry a [component] and, where applicable, a [stream] field between the level and the message: [timestamp] [LEVEL] [component] [stream] message ← stream threads [timestamp] [LEVEL] [component] message ← non-stream threads [timestamp] [LEVEL] message ← unchanged (API, etc.) Implementation ────────────── * Two __thread (TLS) char buffers are kept in logger.c. * log_set_thread_context(component, stream) copies into them; no mutex is needed — TLS is inherently per-thread. * log_message_v() builds a ctx_prefix string from the TLS values and prepends it to every text log line. When both buffers are empty (the default) ctx_prefix is '' so existing behaviour is preserved. * write_json_log() calls the new log_get_thread_component() / log_get_thread_stream() getters and conditionally adds 'component' and 'stream' fields to the JSON object, keeping the JSON schema backward-compatible. Public API additions in logger.h ───────────────────────────────── void log_set_thread_context(component, stream_name) void log_clear_thread_context(void) const char *log_get_thread_component(void) const char *log_get_thread_stream(void) Thread contexts wired up ──────────────────────── MP4Writer mp4_writer_rtsp_thread (+ stream name) MP4Recorder mp4_recording_thread (+ stream name) HLSWriter hls_unified_thread_func (+ stream name) Detection unified_detection_thread_func (+ stream name) HLSWatchdog hls_watchdog_thread_func StreamScheduler schedule_monitor_func go2rtc unified_health_monitor_thread RecordingSync sync_thread_func MQTT ha_snapshot/motion/cleanup threads HealthCheck health_check_thread_func StorageManager unified_storage_controller_func Closes #314
1 parent 8ff26f0 commit 2fa5677

13 files changed

Lines changed: 119 additions & 5 deletions

include/core/logger.h

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,4 +145,49 @@ int is_syslog_enabled(void);
145145
*/
146146
int is_logger_available(void);
147147

148+
/* -----------------------------------------------------------------------
149+
* Per-thread logging context
150+
*
151+
* Each long-running thread may call log_set_thread_context() once at
152+
* startup so that every subsequent log_* call from that thread
153+
* automatically includes a [component] and, when applicable, a
154+
* [stream_name] field in the log line:
155+
*
156+
* [timestamp] [LEVEL] [component] [stream] message
157+
* [timestamp] [LEVEL] [component] message <- no stream set
158+
* [timestamp] [LEVEL] message <- no context set
159+
*
160+
* The implementation uses __thread storage; no mutex is required.
161+
* ----------------------------------------------------------------------- */
162+
163+
/**
164+
* Set the logging context for the current thread.
165+
*
166+
* @param component Short label for the subsystem, e.g. "MP4Writer"
167+
* (max 63 chars, copied into thread-local storage).
168+
* Pass NULL or "" to clear.
169+
* @param stream_name Name of the stream this thread is handling, e.g.
170+
* "front_door" (max 127 chars, copied).
171+
* Pass NULL or "" when the thread is not stream-specific.
172+
*/
173+
void log_set_thread_context(const char *component, const char *stream_name);
174+
175+
/**
176+
* Clear the logging context for the current thread.
177+
* After this call log_* calls from the thread omit the context prefix.
178+
*/
179+
void log_clear_thread_context(void);
180+
181+
/**
182+
* Return the component label stored for the current thread.
183+
* Returns "" when no context has been set.
184+
*/
185+
const char *log_get_thread_component(void);
186+
187+
/**
188+
* Return the stream name stored for the current thread.
189+
* Returns "" when no stream context has been set.
190+
*/
191+
const char *log_get_thread_stream(void);
192+
148193
#endif // LIGHTNVR_LOGGER_H

src/core/logger.c

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,29 @@ static struct {
4141
extern __attribute__((weak)) int init_json_logger(const char *filename);
4242
extern __attribute__((weak)) int write_json_log(log_level_t level, const char *timestamp, const char *message);
4343

44+
// -----------------------------------------------------------------------
45+
// Per-thread logging context (thread-local storage)
46+
// Each worker thread calls log_set_thread_context() once at startup.
47+
// log_message_v() reads these to prepend [component] [stream] to lines.
48+
// -----------------------------------------------------------------------
49+
static __thread char tls_log_component[64] = {0};
50+
static __thread char tls_log_stream[128] = {0};
51+
52+
void log_set_thread_context(const char *component, const char *stream_name) {
53+
strncpy(tls_log_component, component ? component : "", sizeof(tls_log_component) - 1);
54+
tls_log_component[sizeof(tls_log_component) - 1] = '\0';
55+
strncpy(tls_log_stream, stream_name ? stream_name : "", sizeof(tls_log_stream) - 1);
56+
tls_log_stream[sizeof(tls_log_stream) - 1] = '\0';
57+
}
58+
59+
void log_clear_thread_context(void) {
60+
tls_log_component[0] = '\0';
61+
tls_log_stream[0] = '\0';
62+
}
63+
64+
const char *log_get_thread_component(void) { return tls_log_component; }
65+
const char *log_get_thread_stream(void) { return tls_log_stream; }
66+
4467
// Log level strings
4568
static const char *log_level_strings[] = {
4669
"ERROR",
@@ -353,8 +376,17 @@ void log_message_v(log_level_t level, const char *format, va_list args) {
353376
tm_info = localtime_r(&now, &tm_buf);
354377
strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", tm_info);
355378

379+
char shutdown_prefix[224] = {0};
380+
if (tls_log_component[0] != '\0') {
381+
if (tls_log_stream[0] != '\0') {
382+
snprintf(shutdown_prefix, sizeof(shutdown_prefix), "[%s] [%s] ",
383+
tls_log_component, tls_log_stream);
384+
} else {
385+
snprintf(shutdown_prefix, sizeof(shutdown_prefix), "[%s] ", tls_log_component);
386+
}
387+
}
356388
FILE *console = (level == LOG_LEVEL_ERROR) ? stderr : stdout;
357-
fprintf(console, "[%s] [%s] %s\n", timestamp, log_level_strings[level], message);
389+
fprintf(console, "[%s] [%s] %s%s\n", timestamp, log_level_strings[level], shutdown_prefix, message);
358390
fflush(console);
359391
return;
360392
}
@@ -387,10 +419,23 @@ void log_message_v(log_level_t level, const char *format, va_list args) {
387419
vsnprintf(message, sizeof(message), format, args_copy); // NOLINT(clang-analyzer-valist.Uninitialized)
388420
va_end(args_copy);
389421

422+
// Build optional [component] [stream] prefix from per-thread context.
423+
// If the thread has not called log_set_thread_context() both strings are
424+
// empty and ctx_prefix is "" (no prefix — fully backward compatible).
425+
char ctx_prefix[224] = {0};
426+
if (tls_log_component[0] != '\0') {
427+
if (tls_log_stream[0] != '\0') {
428+
snprintf(ctx_prefix, sizeof(ctx_prefix), "[%s] [%s] ",
429+
tls_log_component, tls_log_stream);
430+
} else {
431+
snprintf(ctx_prefix, sizeof(ctx_prefix), "[%s] ", tls_log_component);
432+
}
433+
}
434+
390435
// Double-check shutdown flag before acquiring mutex
391436
if (logger.shutdown) {
392437
FILE *console = (level == LOG_LEVEL_ERROR) ? stderr : stdout;
393-
fprintf(console, "[%s] [%s] %s\n", timestamp, log_level_strings[level], message);
438+
fprintf(console, "[%s] [%s] %s%s\n", timestamp, log_level_strings[level], ctx_prefix, message);
394439
fflush(console);
395440
return;
396441
}
@@ -399,14 +444,14 @@ void log_message_v(log_level_t level, const char *format, va_list args) {
399444

400445
// Write to log file if available
401446
if (logger.log_file && logger.log_file != stdout && logger.log_file != stderr) {
402-
fprintf(logger.log_file, "[%s] [%s] %s\n", timestamp, log_level_strings[level], message);
447+
fprintf(logger.log_file, "[%s] [%s] %s%s\n", timestamp, log_level_strings[level], ctx_prefix, message);
403448
fflush(logger.log_file);
404449
}
405450

406451
// Always write to console (tee behavior)
407452
// Use stderr for errors, stdout for other levels
408453
FILE *console = (level == LOG_LEVEL_ERROR) ? stderr : stdout;
409-
fprintf(console, "[%s] [%s] %s\n", timestamp, log_level_strings[level], message);
454+
fprintf(console, "[%s] [%s] %s%s\n", timestamp, log_level_strings[level], ctx_prefix, message);
410455
fflush(console);
411456

412457
// Write to syslog if enabled

src/core/logger_json.c

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,9 +180,20 @@ int write_json_log(log_level_t level, const char *timestamp, const char *message
180180
return -1;
181181
}
182182

183-
// Add timestamp, level, and message
183+
// Add timestamp, level, optional context fields, and message.
184+
// Component and stream are read from per-thread storage (log_get_thread_*);
185+
// they are only added to the JSON object when non-empty so that log
186+
// entries from threads without context remain unchanged.
184187
cJSON_AddStringToObject(log_entry, "timestamp", timestamp);
185188
cJSON_AddStringToObject(log_entry, "level", json_log_level_strings[level]);
189+
const char *component = log_get_thread_component();
190+
const char *stream_name = log_get_thread_stream();
191+
if (component && component[0] != '\0') {
192+
cJSON_AddStringToObject(log_entry, "component", component);
193+
}
194+
if (stream_name && stream_name[0] != '\0') {
195+
cJSON_AddStringToObject(log_entry, "stream", stream_name);
196+
}
186197
cJSON_AddStringToObject(log_entry, "message", message);
187198

188199
// Convert to string

src/core/mqtt_client.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,7 @@ void mqtt_set_motion_state(const char *stream_name, const detection_result_t *re
861861
*/
862862
static void *ha_snapshot_thread_func(void *arg) {
863863
(void)arg;
864+
log_set_thread_context("MQTT", NULL);
864865
log_info("MQTT HA: Snapshot publishing thread started (interval=%ds)",
865866
mqtt_config->mqtt_ha_snapshot_interval);
866867

@@ -912,6 +913,7 @@ static void *ha_snapshot_thread_func(void *arg) {
912913
*/
913914
static void *ha_motion_thread_func(void *arg) {
914915
(void)arg;
916+
log_set_thread_context("MQTT", NULL);
915917
log_info("MQTT HA: Motion timeout thread started");
916918

917919
while (ha_services_running) {
@@ -1049,6 +1051,7 @@ typedef struct {
10491051
* Thread function to run blocking mosquitto operations with timeout capability
10501052
*/
10511053
static void *mqtt_cleanup_thread(void *arg) {
1054+
log_set_thread_context("MQTT", NULL);
10521055
mqtt_cleanup_arg_t *cleanup_arg = (mqtt_cleanup_arg_t *)arg;
10531056
volatile int *done_flag = cleanup_arg->done_flag;
10541057

src/database/db_recordings_sync.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ static int sync_recordings_needing_size_update(void) {
207207
* Sync thread function
208208
*/
209209
static void *sync_thread_func(void *arg) {
210+
log_set_thread_context("RecordingSync", NULL);
210211
log_info("Recording sync thread started with interval: %d seconds (syncing recordings since %ld)",
211212
sync_thread.interval_seconds, (long)sync_thread.startup_time);
212213

src/storage/storage_manager.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -963,6 +963,7 @@ static void deep_maintenance_cycle(void) {
963963
*/
964964
static void* unified_storage_controller_func(void *arg) {
965965
(void)arg;
966+
log_set_thread_context("StorageManager", NULL);
966967
log_info("Unified storage controller started");
967968
log_info(" Heartbeat interval: %d seconds", HEARTBEAT_INTERVAL_SEC);
968969
log_info(" Standard cleanup interval: %d seconds", STANDARD_INTERVAL_SEC);

src/video/go2rtc/go2rtc_integration.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -760,6 +760,7 @@ static bool restart_go2rtc_process(void) {
760760
static void *unified_health_monitor_thread(void *arg) {
761761
(void)arg;
762762

763+
log_set_thread_context("go2rtc", NULL);
763764
log_info("Unified go2rtc health monitor thread started");
764765

765766
bool process_restarted = false;

src/video/hls/hls_unified_thread.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -942,6 +942,7 @@ void *hls_unified_thread_func(void *arg) {
942942
strncpy(stream_name, ctx->stream_name, MAX_STREAM_NAME - 1);
943943
stream_name[MAX_STREAM_NAME - 1] = '\0';
944944

945+
log_set_thread_context("HLSWriter", stream_name);
945946
log_info("Starting unified HLS thread for stream %s", stream_name);
946947

947948
// Check if we're still running before proceeding
@@ -3258,6 +3259,7 @@ static bool is_thread_running(hls_unified_thread_ctx_t *ctx) {
32583259
* @return NULL
32593260
*/
32603261
static void *hls_watchdog_thread_func(void *arg) {
3262+
log_set_thread_context("HLSWatchdog", NULL);
32613263
log_info("HLS watchdog thread started");
32623264

32633265
while (atomic_load(&watchdog_running)) {

src/video/mp4_recording_core.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ static void *mp4_recording_thread(void *arg) {
108108
strncpy(stream_name, ctx->config.name, MAX_STREAM_NAME - 1);
109109
stream_name[MAX_STREAM_NAME - 1] = '\0';
110110

111+
log_set_thread_context("MP4Recorder", stream_name);
111112
log_info("Starting MP4 recording thread for stream %s", stream_name);
112113

113114
// Check if we're still running (might have been stopped during initialization)

src/video/mp4_writer_thread.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ static void *mp4_writer_rtsp_thread(void *arg) {
121121
strncpy(stream_name, "unknown", MAX_STREAM_NAME - 1);
122122
}
123123

124+
log_set_thread_context("MP4Writer", stream_name);
124125
log_info("Starting RTSP reading thread for stream %s", stream_name);
125126

126127
// Defer DB creation until the first keyframe is seen so start_time aligns to a playable frame.

0 commit comments

Comments
 (0)