ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
log.c
Go to the documentation of this file.
1
2/* Storage capacities in elements; text buffers include the terminating NUL. */
3#define LOG_BUFFER_BUFFER_SIZE 8192
4#define LOG_LOG_MSG_BUFFER_SIZE 256
5#define LOG_PARSE_TIMESTAMP_BUFFER_SIZE 64
6#define LOG_LEVEL_BUFFER_SIZE 16
7#define LOG_FILE_PATH_BUFFER_SIZE 256
8#define LOG_FUNC_NAME_BUFFER_SIZE 256
9
10#include <ascii-chat/otel.h>
17
19
21#include <ascii-chat/common.h>
23#include <ascii-chat/options/rcu.h> // For RCU-based options access
24#include <libplatform/compiler.h>
25#include <libplatform/system.h>
26#include <libplatform/terminal.h>
27#include <libplatform/system.h>
32#include <libplatform/lifecycle.h>
33#include <stdarg.h>
34#include <stdio.h>
35#include <stdlib.h>
36#include <string.h>
37#include <time.h>
38#include <fcntl.h>
39#include <sys/stat.h>
40#include <ctype.h>
41
42#include <ascii-chat/log/log.h>
43
44int terminal_choose_log_fd(int level);
47#include <ascii-chat/log/mmap.h>
48#include <ascii-chat/log/grep.h>
49#include <ascii-chat/log/json.h>
50#include <libplatform/terminal.h>
51#include <libplatform/systemd.h>
55#include <libplatform/thread.h>
56#include <libplatform/mutex.h>
60
61// Readers pin the session buffer until unregister has drained their appends.
62static atomic_t g_session_log_readers = {0};
63
64/* Platform-specific log hook (weak, can be overridden by platform implementations) */
65__attribute__((weak)) void platform_log_hook(log_level_t level, const char *message) {
66 (void)level;
67 (void)message;
68 // Default: no-op
69}
70
71/* ============================================================================
72 * Logging System Internal State
73 * ============================================================================ */
74
75/* Log context struct - Lock-free logging with mutex-protected rotation
76 *
77 * Design: Logging itself is lock-free using atomic operations and atomic write() syscalls.
78 * Only log rotation uses a mutex, since it involves multiple file operations that must
79 * be atomic as a group (close, read tail, write temp, rename, reopen).
80 *
81 * This structure consolidates all logging system state into a single, named struct
82 * for better auditability and easier initialization/cleanup.
83 */
84#pragma GCC diagnostic push
85#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
86// Console logs never draw over a live screen, even from its rendering callback.
87static void log_terminal_printf(int fd, const char *format, ...) {
89 return;
90 char buffer[LOG_TERMINAL_BUFFER_SIZE];
91 va_list args;
92 va_start(args, format);
93 int length = vsnprintf(buffer, sizeof(buffer), format, args);
94 va_end(args);
95 if (length > 0) {
96 size_t bytes = (size_t)length < sizeof(buffer) ? (size_t)length : sizeof(buffer) - 1;
97 ui_controller_write(fd, buffer, bytes);
98 }
99}
100
101static struct log_system_t {
102 /* Core logging I/O and state */
103 atomic_t file; /* File descriptor (atomic for safe access) */
104 atomic_t json_file; /* JSON output file descriptor (-1 = disabled) */
105 atomic_t level; /* Log level as int for atomic ops */
106 lifecycle_t lifecycle; /* Initialization state machine */
107 char filename[LOG_MSG_BUFFER_SIZE]; /* Store filename (set once at init) */
108 atomic_t current_size; /* Track current file size */
109
110 /* Terminal output control */
111 atomic_t terminal_output_enabled; /* Control stderr output to terminal */
112 atomic_t force_stderr; /* Force all terminal logs to stderr (client mode) */
113 atomic_t terminal_locked; /* True when a thread has exclusive terminal access */
114 atomic_t terminal_owner_thread; /* Thread that owns terminal output (stored as uint64) */
115
116 /* Log level and formatting */
117 atomic_t level_manually_set; /* Track if level was set manually */
118 atomic_t flush_delay_ms; /* Delay between each buffered log flush (0 = disabled) */
119 log_template_t *format; /* Compiled log format (NULL = use default) */
120 log_template_t *format_console_only; /* Console-only format variant */
121 atomic_t has_custom_format; /* True if format was customized */
122
123 /* Terminal capability detection and color scheme */
124 terminal_capabilities_t terminal_caps; /* Detected terminal capabilities */
125 bool terminal_caps_initialized; /* Whether terminal caps were detected */
126 bool terminal_caps_detecting; /* Guard against recursion during detection */
127
128 /* Shutdown state management */
129 bool shutdown_saved_terminal_output; /* Saved state for log_shutdown_begin/end */
130 bool shutdown_in_progress; /* Track if shutdown phase is active */
131
132 /* Log rotation */
133 mutex_t rotation_mutex; /* Mutex for log rotation only (not for logging!) */
134 lifecycle_t rotation_mutex_lifecycle; /* Rotation mutex initialization state machine */
135
136 /* Session-specific logging (splash/status screens) */
137 atomic_ptr_t session_log_buffer; /* Pointer to registered session log buffer (can be NULL) */
138} g_log = {
139 .file = {0}, /* STDERR_FILENO (fd 2) - fd 0 is STDIN (read-only!) */
140 .json_file = {.impl = (uint64_t)-1}, /* -1 = disabled (atomic_t initialized as uint64_t -1) */
141 .level = {0}, /* DEFAULT_LOG_LEVEL */
142 .lifecycle = LIFECYCLE_INIT,
143 .filename = {0},
144 .current_size = {0},
145 .terminal_output_enabled = {1}, /* true */
146 .force_stderr = {0}, /* false */
147 .terminal_locked = {0}, /* false */
148 .terminal_owner_thread = {0},
149 .level_manually_set = {0}, /* false */
150 .flush_delay_ms = {0},
151 .format = NULL,
152 .format_console_only = NULL,
153 .has_custom_format = {0}, /* false */
154 .terminal_caps = {0},
155 .terminal_caps_initialized = false,
156 .terminal_caps_detecting = false,
157 .shutdown_saved_terminal_output = true,
158 .shutdown_in_progress = false,
159 .rotation_mutex_lifecycle = LIFECYCLE_INIT,
160 .session_log_buffer = {0}, /* NULL */
161};
162#pragma GCC diagnostic pop
163
164/* Level strings are defined in format.c (get_level_string_padded / get_level_string) */
165
166/* ============================================================================
167 * Level Strings
168 * ============================================================================
169 * The actual default log format is defined in lib/log/format.c
170 * and accessed via OPT_LOG_TEMPLATE_DEFAULT macro to support different modes.
171 */
172
184 const char *result;
185 switch (level) {
186 case LOG_INFO:
187 result = "INFO "; // 5 chars: INFO + 1 space
188 break;
189 case LOG_WARN:
190 result = "WARN "; // 5 chars: WARN + 1 space
191 break;
192 case LOG_DEV:
193 result = "DEV "; // 5 chars: DEV + 2 spaces
194 break;
195 case LOG_DEBUG:
196 result = "DEBUG"; // 5 chars: DEBUG (no padding needed)
197 break;
198 case LOG_ERROR:
199 result = "ERROR"; // 5 chars: ERROR (no padding needed)
200 break;
201 case LOG_FATAL:
202 result = "FATAL"; // 5 chars: FATAL (no padding needed)
203 break;
204 default:
205 result = "?????"; // Invalid level - return 5 question marks
206 }
207 // Verify length
208 if (strlen(result) != 5) {
209 log_terminal_printf(STDERR_FILENO, "ERROR: get_level_string_padded() returned non-5-char string: '%s' (len=%zu)\n",
210 result, strlen(result));
211 }
212 return result;
213}
214
215#define LOG_COLOR_COUNT 8 /* DEV, DEBUG, WARN, INFO, ERROR, FATAL, GREY, RESET */
216
217/* Semantic colors are resolved through the shared live palette API. */
218
219/* Internal error macro - uses g_log directly, only used in this file */
220#ifdef NDEBUG
221#define LOGGING_INTERNAL_ERROR(error, message, ...) \
222 do { \
223 errno_set_with_message(error, NULL, 0, NULL, message, ##__VA_ARGS__); \
224 static const char *msg_header = "CRITICAL LOGGING SYSTEM ERROR: "; \
225 log_terminal_printf(STDERR_FILENO, "%s %s\n", colored_string(LOG_COLOR_ERROR, msg_header), message); \
226 int _log_fd = atomic_load_int(&g_log.file); \
227 platform_write(_log_fd, msg_header, strlen(msg_header)); \
228 platform_write(_log_fd, message, strlen(message)); \
229 platform_write(_log_fd, "\n", 1); \
230 backtrace_print_simple(0); \
231 } while (0)
232#else
233#define LOGGING_INTERNAL_ERROR(error, message, ...) \
234 do { \
235 errno_set_with_message(error, __FILE__, __LINE__, __func__, message, ##__VA_ARGS__); \
236 static const char *msg_header = "CRITICAL LOGGING SYSTEM ERROR: "; \
237 log_terminal_printf(STDERR_FILENO, "%s %s\n", colored_string(LOG_COLOR_ERROR, msg_header), message); \
238 int _log_fd = atomic_load_int(&g_log.file); \
239 platform_write(_log_fd, msg_header, strlen(msg_header)); \
240 platform_write(_log_fd, message, strlen(message)); \
241 platform_write(_log_fd, "\n", 1); \
242 backtrace_print_simple(0); \
243 } while (0)
244#endif
245
246size_t get_current_time_formatted(char *time_buf) {
247 /* Get wall-clock time in nanoseconds */
248 uint64_t ts_ns = time_get_realtime_ns();
249 // Extract seconds and nanoseconds from total nanoseconds
250 time_t seconds = (time_t)(ts_ns / NS_PER_SEC_INT);
251 long nanoseconds = (long)(ts_ns % NS_PER_SEC_INT);
252 struct tm tm_info;
253 platform_localtime(&seconds, &tm_info);
254 // Format the time part first
255 // strftime returns 0 on error, not negative (and len is size_t/unsigned)
256 size_t len = strftime(time_buf, 32, "%H:%M:%S", &tm_info);
257 if (len == 0 || len >= 32) {
258 LOGGING_INTERNAL_ERROR(ERROR_INVALID_STATE, "Failed to format time");
259 return 0;
260 }
261
262 // Add microseconds manually (convert nanoseconds to microseconds for display)
263 long microseconds = nanoseconds / 1000;
264 if (microseconds < 0)
265 microseconds = 0;
266 if (microseconds > 999999)
267 microseconds = 999999;
268
269 int result = safe_snprintf(time_buf + len, 32 - len, ".%06ld", microseconds);
270 if (result < 0 || result >= (int)(32 - len)) {
271 LOGGING_INTERNAL_ERROR(ERROR_INVALID_STATE, "Failed to format microseconds");
272 return 0;
273 }
274
275 return len + (size_t)result;
276}
277
278char *format_message(const char *format, va_list args) {
279 if (!format) {
280 return NULL;
281 }
282
283 // First, determine the size needed
284 va_list args_copy;
285 va_copy(args_copy, args);
286 int size = safe_vsnprintf(NULL, 0, format, args_copy);
287 va_end(args_copy);
288
289 if (size < 0) {
290 LOGGING_INTERNAL_ERROR(ERROR_INVALID_STATE, "Failed to format context message");
291 return NULL;
292 }
293
294 // Allocate and format the message
295 char *message = SAFE_MALLOC(size + 1, char *);
296 int result = safe_vsnprintf(message, (size_t)size + 1, format, args);
297 if (result < 0) {
298 SAFE_FREE(message);
299 LOGGING_INTERNAL_ERROR(ERROR_INVALID_STATE, "Failed to format context message");
300 return NULL;
301 }
302
303 return message;
304}
305
316static int truncate_at_whole_line(char *buffer, int current_len, size_t max_len) {
317 if (current_len < 0 || (size_t)current_len <= max_len) {
318 return current_len; // No truncation needed
319 }
320
321 // Search backwards from max_len position for last newline
322 int truncate_pos = (int)max_len - 1;
323 while (truncate_pos > 0 && buffer[truncate_pos] != '\n') {
324 truncate_pos--;
325 }
326
327 // If we found a newline, truncate after it
328 if (truncate_pos > 0 && buffer[truncate_pos] == '\n') {
329 truncate_pos++; // Include the newline
330 // NOLINTNEXTLINE(clang-analyzer-security.ArrayBound) - truncate_pos validated by max_len
331 buffer[truncate_pos] = '\0';
332 return truncate_pos;
333 }
334
335 // No newline found, truncate at max_len to be safe
336 truncate_pos = (int)max_len - 1;
337 buffer[truncate_pos] = '\0';
338 return truncate_pos;
339}
340
348static void validate_log_message_utf8(const char *message, const char *source) {
349 if (!message || !source) {
350 return;
351 }
352
353 if (!utf8_is_valid(message)) {
354 // Use fprintf instead of log_warn to avoid infinite recursion
355 // (this function is called from log_msg, which would create a loop)
356 log_terminal_printf(STDERR_FILENO, "[WARN] Invalid UTF-8 detected in %s\n", source);
357 log_terminal_printf(STDERR_FILENO, "[DEBUG] Invalid UTF-8 data: %s\n", message);
358 }
359}
360
361/* ============================================================================
362 * Logging Implementation
363 * ============================================================================
364 */
365
366/* Parse LOG_LEVEL environment variable */
367static log_level_t parse_log_level_from_env(void) {
368 const char *env_level = SAFE_GETENV("LOG_LEVEL");
369 if (!env_level) {
370 return DEFAULT_LOG_LEVEL; // Default level based on build type
371 }
372
373 // Case-insensitive comparison
374 if (platform_strcasecmp(env_level, "DEV") == 0 || strcmp(env_level, "0") == 0) {
375 return LOG_DEV;
376 }
377 if (platform_strcasecmp(env_level, "DEBUG") == 0 || strcmp(env_level, "1") == 0) {
378 return LOG_DEBUG;
379 }
380 if (platform_strcasecmp(env_level, "INFO") == 0 || strcmp(env_level, "2") == 0) {
381 return LOG_INFO;
382 }
383 if (platform_strcasecmp(env_level, "WARN") == 0 || strcmp(env_level, "3") == 0) {
384 return LOG_WARN;
385 }
386 if (platform_strcasecmp(env_level, "ERROR") == 0 || strcmp(env_level, "4") == 0) {
387 return LOG_ERROR;
388 }
389 if (platform_strcasecmp(env_level, "FATAL") == 0 || strcmp(env_level, "5") == 0) {
390 return LOG_FATAL;
391 }
392
393 // Invalid value - return default
394 log_warn("Invalid LOG_LEVEL: %s", env_level);
395 return DEFAULT_LOG_LEVEL;
396}
397
398/* Log rotation function - keeps the tail (recent entries)
399 * REQUIRES: rotation_mutex must be held by caller
400 * This is the only operation that uses a mutex - regular logging is lock-free.
401 */
402static void rotate_log_locked(void) {
403 int file = atomic_load_int(&g_log.file);
404 size_t current_size = atomic_load_u64(&g_log.current_size);
405
406 if (file < 0 || file == STDERR_FILENO || strlen(g_log.filename) == 0) {
407 return;
408 }
409
410 if (current_size < MAX_LOG_SIZE) {
411 return;
412 }
413
414 platform_close(file);
415 atomic_store_int(&g_log.file, -1);
416
417 /* Open file for reading to get the tail */
418 int read_file = platform_open("log_file_read", g_log.filename, O_RDONLY, 0);
419 if (read_file < 0) {
420 log_error("Failed to open file descriptor for log rotation: fd=%d, path=%s", read_file, g_log.filename);
421 /* Fall back to regular truncation */
422 int fd = platform_open("log_file_write", g_log.filename, O_CREAT | O_RDWR | O_TRUNC, FILE_PERM_PRIVATE);
423 atomic_store_int(&g_log.file, fd);
424 atomic_store_u64(&g_log.current_size, 0);
425 return;
426 }
427
428 /* Seek to position where we want to start keeping data (keep last 2MB) */
429 size_t keep_size = MAX_LOG_SIZE * 2 / 3; /* Keep last 2MB of 3MB file */
430 if (current_size < keep_size) {
431 platform_close(read_file);
432 /* Fall back to truncation since we don't have enough data to rotate */
433 int fd = platform_open("log_file_write", g_log.filename, O_CREAT | O_RDWR | O_TRUNC, FILE_PERM_PRIVATE);
434 atomic_store_int(&g_log.file, fd);
435 atomic_store_u64(&g_log.current_size, 0);
436 return;
437 }
438 if (lseek(read_file, (off_t)(current_size - keep_size), SEEK_SET) == (off_t)-1) {
439 platform_close(read_file);
440 /* Fall back to truncation */
441 int fd = platform_open("log_file_write", g_log.filename, O_CREAT | O_RDWR | O_TRUNC, FILE_PERM_PRIVATE);
442 atomic_store_int(&g_log.file, fd);
443 atomic_store_u64(&g_log.current_size, 0);
444 return;
445 }
446
447 /* Skip to next line boundary to avoid partial lines */
448 char c;
449 while (platform_read(read_file, &c, 1) > 0 && c != '\n') {
450 /* Skip characters until newline */
451 }
452
453 /* Read the tail into a temporary file */
454 char temp_filename[PLATFORM_MAX_PATH_LENGTH];
455 int result = safe_snprintf(temp_filename, sizeof(temp_filename), "%s.tmp", g_log.filename);
456 if (result <= 0 || result >= (int)sizeof(temp_filename)) {
457 LOGGING_INTERNAL_ERROR(ERROR_INVALID_STATE, "Failed to format temp filename");
458 platform_close(read_file);
459 int fd = platform_open("log_file_append", g_log.filename, O_CREAT | O_RDWR | O_APPEND, FILE_PERM_PRIVATE);
460 atomic_store_int(&g_log.file, fd);
461 return;
462 }
463
464 int temp_file = platform_open("log_file_temp", temp_filename, O_CREAT | O_WRONLY | O_TRUNC, FILE_PERM_PRIVATE);
465 if (temp_file < 0) {
466 log_warn("Failed to open temporary file descriptor for log rotation: fd=%d, path=%s", temp_file, temp_filename);
467 platform_close(read_file);
468 /* Fall back to truncation */
469 int fd = platform_open("log_file_write", g_log.filename, O_CREAT | O_RDWR | O_TRUNC, FILE_PERM_PRIVATE);
470 atomic_store_int(&g_log.file, fd);
471 atomic_store_u64(&g_log.current_size, 0);
472 return;
473 }
474
475 /* Copy tail to temp file */
476 char buffer[LOG_BUFFER_BUFFER_SIZE];
477 ssize_t bytes_read;
478 size_t new_size = 0;
479 while ((bytes_read = platform_read(read_file, buffer, sizeof(buffer))) > 0) {
480 ssize_t written = platform_write(temp_file, buffer, (size_t)bytes_read);
481 if (written != bytes_read) {
482 platform_close(read_file);
483 platform_close(temp_file);
484 unlink(temp_filename);
485 /* Fall back to truncation */
486 int fd = platform_open("log_file_write", g_log.filename, O_CREAT | O_RDWR | O_TRUNC, FILE_PERM_PRIVATE);
487 atomic_store_int(&g_log.file, fd);
488 atomic_store_u64(&g_log.current_size, 0);
489 return;
490 }
491 new_size += (size_t)bytes_read;
492 }
493
494 platform_close(read_file);
495 platform_close(temp_file);
496
497 /* Replace original with temp file */
498 if (rename(temp_filename, g_log.filename) != 0) {
499 unlink(temp_filename); /* Clean up temp file */
500 /* Fall back to truncation */
501 int fd = platform_open("log_file_write", g_log.filename, O_CREAT | O_RDWR | O_TRUNC, FILE_PERM_PRIVATE);
502 atomic_store_int(&g_log.file, fd);
503 atomic_store_u64(&g_log.current_size, 0);
504 return;
505 }
506
507 /* Reopen for appending */
508 int new_fd = platform_open("log_file_append", g_log.filename, O_CREAT | O_RDWR | O_APPEND, FILE_PERM_PRIVATE);
509 if (new_fd < 0) {
510 LOGGING_INTERNAL_ERROR(ERROR_INVALID_STATE, "Failed to reopen rotated log file: %s", g_log.filename);
511 atomic_store_int(&g_log.file, STDERR_FILENO);
512 g_log.filename[0] = '\0';
513 atomic_store_u64(&g_log.current_size, 0);
514 return;
515 }
516 atomic_store_int(&g_log.file, new_fd);
517 atomic_store_u64(&g_log.current_size, new_size);
518
519 char time_buf[LOG_TIMESTAMP_BUFFER_SIZE];
521
523 int log_msg_len =
524 safe_snprintf(log_msg, sizeof(log_msg), "[%s] [INFO] Log tail-rotated (kept %zu bytes)\n", time_buf, new_size);
525 if (log_msg_len > 0 && log_msg_len < (int)sizeof(log_msg)) {
526 (void)platform_write(new_fd, log_msg, (size_t)log_msg_len);
527 }
528}
529
530/* Check if rotation is needed and perform it (acquires mutex only if needed) */
531static void maybe_rotate_log(void) {
532 /* Rotation mutex not initialized - skip rotation */
533 if (!lifecycle_is_initialized(&g_log.rotation_mutex_lifecycle)) {
534 return;
535 }
536
537 /* Check mmap path first */
538 if (log_mmap_is_active()) {
539 size_t used = 0, capacity = 0;
540 if (log_mmap_get_usage(&used, &capacity) && capacity > 0) {
541 /* Rotate when 90% full to leave room for writes */
542 if (used > capacity * 9 / 10) {
543 mutex_lock(&g_log.rotation_mutex);
545 mutex_unlock(&g_log.rotation_mutex);
546 }
547 }
548 return;
549 }
550
551 /* File path: quick atomic check - avoid mutex if not needed */
552 size_t current_size = atomic_load_u64(&g_log.current_size);
553 if (current_size < MAX_LOG_SIZE) {
554 return;
555 }
556
557 /* Size exceeded - acquire mutex and rotate */
558 mutex_lock(&g_log.rotation_mutex);
559 rotate_log_locked();
560 mutex_unlock(&g_log.rotation_mutex);
561}
562
563static session_log_buffer_t *g_history;
564
566 return g_history;
567}
568
569void log_init(const char *filename, log_level_t level, bool force_stderr, bool use_mmap) {
570
571 if (!g_history)
572 g_history = session_log_buffer_create();
573
574 // Initialize rotation mutex (only operation that uses a mutex)
575 g_log.rotation_mutex_lifecycle.sync_type = LIFECYCLE_SYNC_MUTEX;
576 g_log.rotation_mutex_lifecycle.sync.mutex = &g_log.rotation_mutex;
577 lifecycle_init(&g_log.rotation_mutex_lifecycle, "log_rotation");
578
579 // Set basic config using atomic stores
580 atomic_store_u64(&g_log.force_stderr, force_stderr);
581 bool preserve_terminal_output = atomic_load_u64(&g_log.terminal_output_enabled);
582
583 // Force logs to stderr if stdout is piped/redirected
584 // This prevents logs from contaminating piped/redirected output in snapshot/batch modes
585 // Per terminal.h: "force stderr when piped to prevent data corruption"
586 if (terminal_is_piped_output()) {
587 atomic_store_bool(&g_log.force_stderr, true);
588 }
589
590 // Close any existing file (atomic load/store)
591 int old_file = atomic_load_int(&g_log.file);
592 if (lifecycle_is_initialized(&g_log.lifecycle) && old_file >= 0 && old_file != STDERR_FILENO) {
593 platform_close(old_file);
594 atomic_store_int(&g_log.file, -1);
595 }
596
597 // Check LOG_LEVEL environment variable
598 const char *env_level_str = SAFE_GETENV("LOG_LEVEL");
599 if (env_level_str) {
600 atomic_store_u64(&g_log.level, (int)parse_log_level_from_env());
601 } else {
602 atomic_store_u64(&g_log.level, (int)level);
603 }
604
605 atomic_store_bool(&g_log.level_manually_set, false);
606 atomic_store_u64(&g_log.current_size, 0);
607
608 if (filename && strcmp(filename, "/dev/null") == 0) {
609 g_log.filename[0] = '\0';
610 atomic_store_int(&g_log.file, -1);
611 atomic_store_bool(&g_log.terminal_output_enabled, false);
612 } else if (filename) {
613 SAFE_STRNCPY(g_log.filename, filename, sizeof(g_log.filename) - 1);
614
615 if (use_mmap) {
616 // Lock-free mmap path - writes go to mmap'd file
617 asciichat_error_t mmap_result = log_mmap_init_simple(filename, 0);
618 if (mmap_result == ASCIICHAT_OK) {
619 atomic_store_int(&g_log.file, -1); // No regular fd - using mmap for file output
620 } else {
621 // Mmap failed - use stderr only (atomic writes, lock-free)
622 if (preserve_terminal_output) {
623 log_terminal_printf(STDERR_FILENO, "Mmap logging failed for %s, using stderr only (lock-free)\n", filename);
624 }
625 atomic_store_int(&g_log.file, STDERR_FILENO);
626 g_log.filename[0] = '\0';
627 }
628 } else {
629 // Lock-free file I/O path - uses atomic write() syscalls
630 int fd = platform_open("log_file", filename, O_CREAT | O_RDWR | O_TRUNC, FILE_PERM_PRIVATE);
631 atomic_store_int(&g_log.file, (fd >= 0) ? fd : STDERR_FILENO);
632 if (fd < 0) {
633 if (preserve_terminal_output) {
634 log_terminal_printf(STDERR_FILENO, "Failed to open log file: %s\n", filename);
635 }
636 g_log.filename[0] = '\0';
637 }
638 }
639 } else {
640 atomic_store_int(&g_log.file, STDERR_FILENO);
641 g_log.filename[0] = '\0';
642 }
643
644 /* Initialize default log format (NULL means use mode-specific default) */
645 log_set_format(NULL, false);
646
647 /* Mark logging system as initialized (state machine transition) */
648 if (!lifecycle_is_initialized(&g_log.lifecycle)) {
649 lifecycle_init(&g_log.lifecycle, "logging");
650 }
651
652 atomic_store_u64(&g_log.terminal_output_enabled, preserve_terminal_output);
653
654 // Reset terminal detection if needed
655 if (g_log.terminal_caps_initialized && !g_log.terminal_caps.detection_reliable) {
656 g_log.terminal_caps_initialized = false;
657 }
658
659 // Detect terminal capabilities
661
662 // NOTE: Color initialization happens separately via log_set_color_scheme()
663 // after options are parsed. Logging works without colors until then.
664 // NOTE: Grep filter initialization happens in main.c after options_init() completes.
665}
666
667void log_destroy(void) {
668 // Destroy mmap logging first (if active)
669 if (log_mmap_is_active()) {
671 }
672
673 // Cleanup grep filter
674 grep_destroy();
675
676 /* Mark logging system as shutdown FIRST (state machine transition)
677 * This signals worker threads to stop logging immediately before we free memory.
678 * This prevents TOCTOU race where a worker thread could see non-NULL format pointer
679 * after we've freed it but before lifecycle_shutdown() is called. */
680 lifecycle_shutdown(&g_log.lifecycle);
681
682 // Cleanup custom format structures: All worker threads have been signaled to stop by this point.
683 // Safe to free templates now without use-after-free concerns.
684 if (g_log.format) {
685 log_template_free(g_log.format);
686 g_log.format = NULL;
687 }
688 if (g_log.format_console_only) {
689 log_template_free(g_log.format_console_only);
690 g_log.format_console_only = NULL;
691 }
692 atomic_store_bool(&g_log.has_custom_format, false);
693
694 // Lock-free cleanup using atomic operations
695 int old_file = atomic_load_int(&g_log.file);
696 if (old_file >= 0 && old_file != STDERR_FILENO) {
697 platform_close(old_file);
698 }
699 atomic_store_int(&g_log.file, -1);
700
701 // Destroy rotation mutex
702 lifecycle_shutdown(&g_log.rotation_mutex_lifecycle);
703}
704
712void log_system_init(void) {
713
714 // Detect terminal capabilities if not already done
716
717 // Initialize color scheme based on terminal capabilities
719}
720
730 // Unregister session log buffer if registered
734 g_history = NULL;
735}
736
738 atomic_store_u64(&g_log.level, (int)level);
739 atomic_store_bool(&g_log.level_manually_set, true);
740}
741
743 return (log_level_t)atomic_load_u64(&g_log.level);
744}
745
747 // Respect --quiet flag: if quiet is set, never enable terminal output
748 // But allow disabling even if options are unavailable (for shutdown cleanup)
749
750 // Only check quiet flag when ENABLING output to avoid deadlock with
751 // concurrent logging from other threads (options_get() can deadlock when
752 // called while other threads are actively logging due to RCU synchronization)
753 if (enabled) {
754 const options_t *opts = options_get();
755 if (opts && opts->quiet) {
756 return; // Silently ignore attempts to enable terminal output when --quiet is set
757 }
758 }
759
760 atomic_store_u64(&g_log.terminal_output_enabled, enabled);
761}
762
764 return atomic_load_u64(&g_log.terminal_output_enabled);
765}
766
768 atomic_store_u64(&g_log.force_stderr, enabled);
769}
770
772 return atomic_load_u64(&g_log.force_stderr);
773}
774
776 atomic_store_int(&g_log.json_file, fd);
777}
778
780 /* Close the current file if it's not stderr */
781 int old_file = atomic_load_int(&g_log.file);
782 if (old_file >= 0 && old_file != STDERR_FILENO) {
783 platform_close(old_file);
784 }
785 /* Redirect file output to stderr */
786 atomic_store_int(&g_log.file, STDERR_FILENO);
787 g_log.filename[0] = '\0';
788}
789
790asciichat_error_t log_set_format(const char *format_str, bool console_only) {
791 /* Free old format if it exists */
792 if (g_log.format) {
793 log_template_free(g_log.format);
794 g_log.format = NULL;
795 }
796 if (g_log.format_console_only) {
797 log_template_free(g_log.format_console_only);
798 g_log.format_console_only = NULL;
799 }
800
801 /* Use default format if NULL or empty string */
802 const char *format_to_use = (format_str && format_str[0] != '\0') ? format_str : OPT_LOG_TEMPLATE_DEFAULT;
803 bool is_custom = (format_str && format_str[0] != '\0');
804
805 /* Parse the format string (always parse, never skip) */
806 log_template_t *parsed_format = log_template_parse(format_to_use, false);
807
808 if (!parsed_format) {
809 log_error("Failed to parse log format: %s", format_to_use);
810 return SET_ERRNO(ERROR_INVALID_STATE, "Invalid log format string");
811 }
812
813 /* If console_only is true, we also need the default format for file output */
814 if (console_only && is_custom) {
816
817 if (!default_format) {
818 log_template_free(parsed_format);
819 log_error("Failed to parse default log format");
820 return SET_ERRNO(ERROR_INVALID_STATE, "Failed to parse default format");
821 }
822 g_log.format = default_format;
823 g_log.format_console_only = parsed_format;
824 } else {
825 g_log.format = parsed_format;
826 g_log.format_console_only = NULL;
827 }
828
829 atomic_store_u64(&g_log.has_custom_format, is_custom);
830 return ASCIICHAT_OK;
831}
832
834 bool expected = atomic_load_bool(&g_log.terminal_locked);
835 while (!atomic_cas_bool(&g_log.terminal_locked, &expected, true)) {
836 // Retry if CAS failed
837 }
838 atomic_store_u64(&g_log.terminal_owner_thread, (uint64_t)thread_self());
839 return expected;
840}
841
842void log_unlock_terminal(bool previous_state) {
843 atomic_store_u64(&g_log.terminal_locked, previous_state);
844 if (!previous_state) {
845 atomic_store_u64(&g_log.terminal_owner_thread, 0);
846 }
847}
848
849void log_set_flush_delay(unsigned int delay_ms) {
850 atomic_store_u64(&g_log.flush_delay_ms, delay_ms);
851}
852
854 // Log rotation is inherently racy without locks - best-effort only
855 // For reliable rotation, use mmap mode or external logrotate
856 int file = atomic_load_int(&g_log.file);
857 if (file >= 0 && file != STDERR_FILENO && strlen(g_log.filename) > 0) {
858 struct stat st;
859 if (fstat(file, &st) == 0 && st.st_size > MAX_LOG_SIZE) {
860 atomic_store_u64(&g_log.current_size, (size_t)st.st_size);
861 }
862 }
863}
864
865/* Helper: Write formatted log entry to file using atomic write() syscall
866 * POSIX guarantees write() is atomic for sizes <= PIPE_BUF (typically 4096 bytes)
867 * Strips ANSI escape codes from the buffer before writing to ensure clean log files.
868 */
869static void write_to_log_file_atomic(const char *buffer, int length, const char *stripped_buffer) {
870 if (length <= 0 || buffer == NULL) {
871 return;
872 }
873
874 if (length > MAX_LOG_SIZE) {
875 return;
876 }
877
878 int file = atomic_load_int(&g_log.file);
879 if (file < 0 || file == STDERR_FILENO) {
880 return;
881 }
882
883 // Use pre-stripped buffer if available (already stripped by caller), otherwise use original
884 const char *write_buf = stripped_buffer ? stripped_buffer : buffer;
885 size_t write_len = stripped_buffer ? strlen(stripped_buffer) : (size_t)length;
886
887 // Single atomic write() call - no locking needed
888 ssize_t written = platform_write(file, write_buf, write_len);
889 if (written > 0) {
890 atomic_fetch_add_u64(&g_log.current_size, (uint64_t)written);
891 maybe_rotate_log();
892 }
893}
894
895/* Helper: Format log message header using the format system
896 * Returns the number of characters written to the buffer
897 */
898static int format_log_header(char *buffer, size_t buffer_size, log_level_t level, const char *timestamp,
899 const char *file, int line, const char *func, bool use_colors, uint64_t time_nanoseconds) {
900 const log_template_t *format = g_log.format;
901 if (!format) {
902 LOGGING_INTERNAL_ERROR(ERROR_INVALID_STATE, "Log format not initialized");
903 return -1;
904 }
905
906 /* Use log_template_apply() to generate the complete formatted output
907 * Safety: reload format immediately before use. Format is never freed to avoid TOCTOU races. */
908 const log_template_t *format_check = g_log.format;
909 if (!format_check) {
910 /* Format not available - return minimal output */
911 return -1;
912 }
913
914 int written = log_template_apply(format_check, buffer, buffer_size, level, timestamp, file, line, func,
915 thread_current_id(), "", use_colors, time_nanoseconds);
916 if (written < 0 || written >= (int)buffer_size) {
917 LOGGING_INTERNAL_ERROR(ERROR_INVALID_STATE, "Failed to format log header");
918 return -1;
919 }
920
921 return written;
922}
923
924/* Note: Terminal buffering removed for lock-free design.
925 * When terminal is locked, other threads' output goes to file only.
926 */
927
928/* Helper: Write colored log entry to terminal (lock-free)
929 * Uses atomic loads for state checks, skips output if terminal is locked by another thread
930 */
931static _Atomic bool g_terminal_log_colors;
932
933static void write_to_terminal_atomic(log_level_t level, const char *timestamp, const char *file, int line,
934 const char *func, const char *fmt, va_list args, uint64_t time_nanoseconds,
935 bool capture_history) {
936 // Prevent logging during shutdown - if logging lifecycle is being destroyed,
937 // bail out to avoid use-after-free of the log template
938 if (!lifecycle_is_initialized(&g_log.lifecycle)) {
939 return; /* Logging is being shut down, skip */
940 }
941
942 // Choose output file descriptor using unified routing logic
943 bool presenting = ui_controller_is_presenting();
944 int fd = presenting ? STDERR_FILENO : terminal_choose_log_fd(level);
945 FILE *output_stream = (fd == STDERR_FILENO) ? stderr : stdout;
946 // Captured session logs still need formatting, but detecting the console
947 // again would block their producer behind the presentation worker's writes.
948 bool use_colors =
949 presenting ? atomic_load(&g_terminal_log_colors) : terminal_should_color_output(fd, options_terminal_settings());
950 if (!presenting)
951 atomic_store(&g_terminal_log_colors, use_colors);
952
953 // Format message first (needed for status screen capture even if terminal output disabled)
954 char msg_buffer[LOG_MSG_BUFFER_SIZE];
955 va_list args_copy;
956 va_copy(args_copy, args);
957 int msg_len = safe_vsnprintf(msg_buffer, sizeof(msg_buffer), fmt, args_copy);
958 va_end(args_copy);
959
960 // Use message buffer directly as clean message
961 // Stack allocation avoids heap corruption during shutdown signal handling
962 const char *clean_msg = msg_buffer;
963
964 // Use format system to generate the complete log line
965 char colored_log_line[LOG_MSG_BUFFER_SIZE + LOG_HEADER_BUFFER_SIZE];
966 char plain_log_line[LOG_MSG_BUFFER_SIZE + LOG_HEADER_BUFFER_SIZE];
967
968 /* Apply format without colors first (plain version for grep matching)
969 * Load format pointer once to prevent TOCTOU race condition where another
970 * thread frees the format between our check and use. */
971 const log_template_t *format = g_log.format_console_only ? g_log.format_console_only : g_log.format;
972 int plain_len = 0;
973 if (format && lifecycle_is_initialized(&g_log.lifecycle)) { /* Double-check lifecycle is still active */
974 plain_len = log_template_apply(format, plain_log_line, sizeof(plain_log_line), level, timestamp, file, line, func,
975 thread_current_id(), clean_msg, false, time_nanoseconds);
976 }
977
978 /* For colored output, apply format with colors enabled */
979 int colored_len = 0;
980 if (use_colors && plain_len > 0) {
981 const log_template_t *format_check = g_log.format_console_only ? g_log.format_console_only : g_log.format;
982 if (format_check && lifecycle_is_initialized(&g_log.lifecycle)) { /* Re-verify lifecycle before use */
983 colored_len = log_template_apply(format_check, colored_log_line, sizeof(colored_log_line), level, timestamp, file,
984 line, func, thread_current_id(), clean_msg, true, time_nanoseconds);
985 }
986 /* If applying with colors failed, fall back to plain text */
987 if (colored_len <= 0) {
988 // Applying with colors failed - use plain text (no colors)
989 colored_len = plain_len;
990 if (plain_len > 0 && plain_len < (int)sizeof(colored_log_line)) {
991 memcpy(colored_log_line, plain_log_line, (size_t)plain_len + 1);
992 }
993 }
994 } else {
995 /* No colors - use plain text */
996 colored_len = plain_len;
997 if (plain_len > 0 && plain_len < (int)sizeof(colored_log_line)) {
998 memcpy(colored_log_line, plain_log_line, (size_t)plain_len + 1);
999 }
1000 }
1001
1002 if (capture_history)
1003 session_log_buffer_append(g_history, colored_len > 0 ? colored_log_line : clean_msg);
1004
1005 // Feed log to registered session log buffer (use colored version if available, otherwise plain)
1006 atomic_fetch_add_u64_impl(&g_session_log_readers, 1);
1007 session_log_buffer_t *buf = (session_log_buffer_t *)atomic_ptr_load(&g_log.session_log_buffer);
1008 if (buf && buf != g_history) {
1009 if (colored_len > 0 && colored_len < (int)sizeof(colored_log_line)) {
1010 session_log_buffer_append(buf, colored_log_line);
1011 } else if (plain_len > 0 && plain_len < (int)sizeof(plain_log_line)) {
1012 session_log_buffer_append(buf, plain_log_line);
1013 }
1014 }
1015
1016 atomic_fetch_sub_u64_impl(&g_session_log_readers, 1);
1017
1018 // The session now owns this log entry. Do not filter or flush a suppressed
1019 // stdio write: fflush can wait on console/stdio activity on another thread.
1020 if (presenting)
1021 return;
1022
1023 // Check if terminal output is enabled (atomic load)
1024 bool is_enabled = atomic_load_u64(&g_log.terminal_output_enabled);
1025 if (!is_enabled) {
1026 // Terminal output disabled - return (status screen already captured)
1027 return;
1028 }
1029
1030 // Check if terminal is locked by another thread
1031 if (atomic_load_u64(&g_log.terminal_locked)) {
1032 uint64_t owner = atomic_load_u64(&g_log.terminal_owner_thread);
1033 if (owner != (uint64_t)thread_self()) {
1034 // Terminal locked by another thread - return
1035 return;
1036 }
1037 }
1038
1039 // Handle message formatting errors for terminal output
1040 if (msg_len <= 0 || msg_len >= (int)sizeof(msg_buffer)) {
1041 // Message formatting failed - skip filtering and try direct output
1042 /* A failed format is omitted; args may already have been consumed. */
1043 log_terminal_printf(output_stream == stdout ? STDOUT_FILENO : STDERR_FILENO, "\n");
1044 (void)fflush(output_stream);
1045 return;
1046 }
1047
1048 // Handle log formatting errors
1049 if (colored_len <= 0 || colored_len >= (int)sizeof(colored_log_line) || plain_len <= 0 ||
1050 plain_len >= (int)sizeof(plain_log_line)) {
1051 // Formatting failed - try to output something
1052 log_terminal_printf(output_stream == stdout ? STDOUT_FILENO : STDERR_FILENO, "%s\n", clean_msg);
1053 (void)fflush(output_stream);
1054 return;
1055 }
1056
1057 // Apply grep filter (terminal output only - file logs are unfiltered)
1058 // Match against PLAIN text (no ANSI codes) so offsets are correct
1059 size_t match_start = 0, match_len = 0;
1060 if (!grep_should_output(plain_log_line, &match_start, &match_len)) {
1061 return; // No match - suppress terminal output
1062 }
1063
1064 // Output with colors
1065 if (use_colors) {
1066 const char **colors = log_get_color_array();
1067
1068 // When grep matches, highlight in the colored line
1069 if (match_len > 0 && colors != NULL) {
1070 // Apply highlighting to the full line, respecting existing ANSI codes
1071 const char *highlighted_line = grep_highlight_colored(colored_log_line, plain_log_line, match_start, match_len);
1072 log_terminal_printf(output_stream == stdout ? STDOUT_FILENO : STDERR_FILENO, "%s\n", highlighted_line);
1073 } else {
1074 // No grep match - output colored line as-is
1075 log_terminal_printf(output_stream == stdout ? STDOUT_FILENO : STDERR_FILENO, "%s\n", colored_log_line);
1076 }
1077 } else {
1078 // No colors - output plain line
1079 log_terminal_printf(output_stream == stdout ? STDOUT_FILENO : STDERR_FILENO, "%s\n", plain_log_line);
1080 }
1081
1082 (void)fflush(output_stream);
1083}
1084
1085static _Thread_local const char *g_notice_text;
1086static _Thread_local ui_notice_severity_t g_notice_severity;
1087static _Thread_local bool g_notice_record_only;
1088static _Thread_local bool g_notice_unfiltered;
1089
1090static void output_notice(log_level_t level, const char *file, int line, const char *func) {
1091 if (g_notice_record_only)
1092 return;
1093 const char *text = g_notice_text;
1094 ui_notice_severity_t severity = g_notice_severity;
1095 g_notice_text = NULL;
1096 atomic_fetch_add_u64_impl(&g_session_log_readers, 1);
1098 session_log_buffer_append(g_history, text);
1099 if (buffer && buffer != g_history)
1100 session_log_buffer_append(buffer, text);
1101 atomic_fetch_sub_u64_impl(&g_session_log_readers, 1);
1102 // Match the same uncolored template as ordinary terminal logs, including metadata.
1103 // Keep the complete body for notices that span multiple file-log records.
1104 size_t capacity = strlen(text) + LOG_MSG_BUFFER_SIZE + LOG_HEADER_BUFFER_SIZE;
1105 char *plain_log_line = SAFE_MALLOC(capacity, char *);
1106 char timestamp[LOG_PARSE_TIMESTAMP_BUFFER_SIZE];
1107 uint64_t time_ns = time_get_realtime_ns();
1108 get_current_time_formatted(timestamp);
1109 const log_template_t *format = g_log.format;
1110 int plain_len = format && plain_log_line
1111 ? log_template_apply(format, plain_log_line, capacity, level, timestamp, file, line, func,
1112 thread_current_id(), text, false, time_ns)
1113 : -1;
1114 size_t start = 0, length = 0;
1115 if (!GET_OPTION(quiet) &&
1116 (log_get_terminal_output() || ui_controller_is_presenting() || severity == UI_NOTICE_FATAL) && plain_len > 0 &&
1117 (size_t)plain_len < capacity && grep_should_output(plain_log_line, &start, &length))
1118#ifdef EMSCRIPTEN_BUILD
1119 log_plain("%s", text);
1120#else
1121 if (ui_notice_present(severity, text) == ERROR_BUFFER_FULL)
1122 log_warn_every(US_PER_SEC_INT, "Notice display queue is full; additional notices remain in the log");
1123#endif
1124 SAFE_FREE(plain_log_line);
1125 g_notice_text = text;
1126}
1127
1128static void notice_logv(ui_notice_severity_t severity, bool unfiltered, const char *file, int line, const char *func,
1129 const char *title, const char *format, va_list args) {
1130 if (!title || !format || !lifecycle_is_initialized(&g_log.lifecycle))
1131 return;
1132 char *body = format_message(format, args);
1133 if (!body)
1134 return;
1135 size_t size = strlen(title) + strlen(body) + 2;
1136 char *message = SAFE_MALLOC(size, char *);
1137 if (!message) {
1138 SAFE_FREE(body);
1139 return;
1140 }
1141 safe_snprintf(message, size, "%s\n%s", title, body);
1142 const char *previous = g_notice_text;
1143 ui_notice_severity_t previous_severity = g_notice_severity;
1144 bool previous_unfiltered = g_notice_unfiltered;
1145 g_notice_text = message;
1146 g_notice_severity = severity;
1147 g_notice_unfiltered = unfiltered;
1148 log_level_t level = severity == UI_NOTICE_FATAL ? LOG_FATAL
1149 : severity == UI_NOTICE_DANGER ? LOG_ERROR
1150 : severity == UI_NOTICE_WARNING ? LOG_WARN
1151 : LOG_INFO;
1152 if (strlen(message) < 3000) {
1153 log_msg(level, file, line, func, "%s", message);
1154 } else {
1155 bool previous_record_only = g_notice_record_only;
1156 g_notice_record_only = true;
1157 const char *cursor = message;
1158 while (*cursor) {
1159 size_t bytes = strlen(cursor);
1160 if (bytes > 3000) {
1161 bytes = 3000;
1162 while (bytes && ((unsigned char)cursor[bytes] & 0xc0) == 0x80)
1163 --bytes;
1164 if (!bytes)
1165 bytes = 3000; // Malformed UTF-8 must still make progress.
1166 }
1167 log_msg(level, file, line, func, "%.*s", (int)bytes, cursor);
1168 cursor += bytes;
1169 }
1170 g_notice_record_only = previous_record_only;
1171 if ((unfiltered || level >= log_get_level()) && atomic_load_int(&g_log.json_file) < 0)
1172 output_notice(level, file, line, func);
1173 }
1174 g_notice_text = previous;
1175 g_notice_severity = previous_severity;
1176 g_notice_unfiltered = previous_unfiltered;
1177 SAFE_FREE(message);
1178 SAFE_FREE(body);
1179}
1180
1181void ui_notice_log(ui_notice_severity_t severity, const char *file, int line, const char *func, const char *title,
1182 const char *format, ...) {
1183 va_list args;
1184 va_start(args, format);
1185 notice_logv(severity, false, file, line, func, title, format, args);
1186 va_end(args);
1187}
1188
1189void ui_notice_announce(const char *file, int line, const char *func, const char *title, const char *format, ...) {
1190 va_list args;
1191 va_start(args, format);
1192 notice_logv(UI_NOTICE_INFO, true, file, line, func, title, format, args);
1193 va_end(args);
1194}
1195
1196void log_msg(log_level_t level, const char *file, int line, const char *func, const char *fmt, ...) {
1197#ifndef __EMSCRIPTEN__
1198 va_list telemetry_args;
1199 va_start(telemetry_args, fmt);
1200 ascii_otel_log((int)level, file, line, func, fmt, telemetry_args);
1201 va_end(telemetry_args);
1202#endif
1203
1204 // All state access uses atomic operations - fully lock-free
1205 if (!lifecycle_is_initialized(&g_log.lifecycle)) {
1206 return;
1207 }
1208
1209 // Safety check: if main format template has been freed during shutdown, skip logging
1210 // This prevents heap-use-after-free when threads call logging after log_destroy() has freed g_log.format
1211 if (!g_log.format) {
1212 return;
1213 }
1214
1215 uint64_t loaded_level = atomic_load_u64(&g_log.level);
1216
1217 if (level < (log_level_t)loaded_level && !(g_notice_text && g_notice_unfiltered)) {
1218 return;
1219 }
1220 /* =========================================================================
1221 * MMAP PATH: When mmap logging is active, writes go to mmap'd file
1222 * ========================================================================= */
1223 bool mmap_active = log_mmap_is_active();
1224
1225 if (mmap_active) {
1226 maybe_rotate_log();
1227
1228 va_list args;
1229 va_start(args, fmt);
1230 char msg_buffer[LOG_MMAP_MSG_BUFFER_SIZE];
1231 int msg_len = safe_vsnprintf(msg_buffer, sizeof(msg_buffer), fmt, args);
1232 va_end(args);
1233
1234 // Truncate at whole line boundaries to avoid UTF-8 issues
1235 if (msg_len > 0) {
1236 msg_len = truncate_at_whole_line(msg_buffer, msg_len, sizeof(msg_buffer));
1237 }
1238
1239 // Validate UTF-8 in formatted message
1240 validate_log_message_utf8(msg_buffer, "mmap log message");
1241
1242 if (!g_notice_text)
1243 session_log_buffer_append(g_history, msg_buffer);
1244 log_mmap_write(level, file, line, func, "%s", msg_buffer);
1245 platform_systemd_journal_log(level, msg_buffer, "ascii-chat", file, line, func);
1246
1247 if (g_notice_text) {
1248 output_notice(level, file, line, func);
1249 return;
1250 }
1251
1252 // Terminal output (check with atomic loads)
1253 if (atomic_load_u64(&g_log.terminal_output_enabled) && !atomic_load_bool(&g_log.terminal_locked) &&
1255 char time_buf[LOG_TIMESTAMP_BUFFER_SIZE];
1256 uint64_t time_ns = time_get_realtime_ns();
1258
1259 // Choose output stream using unified routing logic
1260 int fd = terminal_choose_log_fd(level);
1261 FILE *output_stream = (fd == STDERR_FILENO) ? stderr : stdout;
1262 bool use_colors = terminal_should_color_output(fd, options_terminal_settings());
1263
1264 char header_buffer[LOG_HEADER_BUFFER_SIZE];
1265 int header_len = format_log_header(header_buffer, sizeof(header_buffer), level, time_buf, file, line, func,
1266 use_colors, time_ns);
1267
1268 if (header_len >= 0 && header_len < (int)sizeof(header_buffer)) {
1269 if (use_colors) {
1270 const char *colorized_msg = colorize_log_message(msg_buffer);
1271 const char **colors = log_get_color_array();
1272 if (colors) {
1273 log_terminal_printf(output_stream == stdout ? STDOUT_FILENO : STDERR_FILENO, "%s%s%s%s\n", header_buffer,
1274 colors[LOG_COLOR_RESET], colorized_msg, colors[LOG_COLOR_RESET]);
1275 } else {
1276 log_terminal_printf(output_stream == stdout ? STDOUT_FILENO : STDERR_FILENO, "%s%s\n", header_buffer,
1277 colorized_msg);
1278 }
1279 } else {
1280 log_terminal_printf(output_stream == stdout ? STDOUT_FILENO : STDERR_FILENO, "%s%s\n", header_buffer,
1281 msg_buffer);
1282 }
1283 (void)fflush(output_stream);
1284 }
1285 } else {
1286 }
1287 return;
1288 }
1289 /* =========================================================================
1290 * FILE I/O PATH: Lock-free using atomic write() syscalls
1291 * ========================================================================= */
1292 char time_buf[LOG_TIMESTAMP_BUFFER_SIZE];
1293 uint64_t time_ns = time_get_realtime_ns();
1295 // Format message for file output
1296 char log_buffer[LOG_MSG_BUFFER_SIZE];
1297 va_list args;
1298 va_start(args, fmt);
1299 int header_len = format_log_header(log_buffer, sizeof(log_buffer), level, time_buf, file, line, func, false, time_ns);
1300 if (header_len < 0 || header_len >= (int)sizeof(log_buffer)) {
1301 LOGGING_INTERNAL_ERROR(ERROR_INVALID_STATE, "Failed to format log header");
1302 va_end(args);
1303 return;
1304 }
1305 int msg_len = header_len;
1306 int formatted_len = safe_vsnprintf(log_buffer + header_len, sizeof(log_buffer) - (size_t)header_len, fmt, args);
1307 if (formatted_len < 0) {
1308 LOGGING_INTERNAL_ERROR(ERROR_INVALID_STATE, "Failed to format log message");
1309 va_end(args);
1310 return;
1311 }
1312
1313 msg_len += formatted_len;
1314 // Truncate at whole line boundaries to avoid UTF-8 issues
1315 msg_len = truncate_at_whole_line(log_buffer, msg_len, sizeof(log_buffer));
1316 // Add newline if there's room and message doesn't already end with one
1317 if (msg_len > 0 && msg_len < (int)sizeof(log_buffer) - 1) {
1318 if (log_buffer[msg_len - 1] != '\n') {
1319 log_buffer[msg_len++] = '\n';
1320 log_buffer[msg_len] = '\0';
1321 }
1322 }
1323 va_end(args);
1324 // Validate UTF-8 in formatted message
1325 validate_log_message_utf8(log_buffer, "leveled log message");
1326 platform_systemd_journal_log(level, log_buffer + header_len, "ascii-chat", file, line, func);
1327
1328 // Extract the user message part (without header) for JSON logging
1329 const char *user_message = log_buffer + header_len;
1330
1331 // For JSON output: strip automatic trailing newline (keep explicit ones)
1332 char json_message_buf[LOG_MSG_BUFFER_SIZE];
1333 const char *json_message = user_message;
1334 size_t user_msg_len = strlen(user_message);
1335
1336 // If message ends with newline and it was added automatically (not in original message)
1337 if (user_msg_len > 0 && user_message[user_msg_len - 1] == '\n') {
1338 // The newline was added automatically if the message part didn't end with one
1339 // before we added it on line 1067. We can check this by looking at msg_len - 1
1340 // If msg_len >= header_len + 2, then msg_len - 2 would be the char before the newline
1341 int msg_content_len = msg_len - header_len;
1342 // NOLINTNEXTLINE(clang-analyzer-security.ArrayBound) - msg_content_len > 1 bounds check above
1343 if (msg_content_len > 1 && log_buffer[msg_len - 2] != '\n') {
1344 // Newline was added automatically - strip it for JSON
1345 if (user_msg_len - 1 < sizeof(json_message_buf)) {
1346 memcpy(json_message_buf, user_message, user_msg_len - 1);
1347 json_message_buf[user_msg_len - 1] = '\0';
1348 json_message = json_message_buf;
1349 }
1350 }
1351 }
1352
1353 // Check if JSON format is enabled
1354 int json_fd = atomic_load_int(&g_log.json_file);
1355 bool json_format_enabled = (json_fd >= 0);
1356
1357 // If JSON format is enabled, output ONLY JSON (skip text output)
1358 if (json_format_enabled) {
1359 session_log_buffer_append(g_history, log_buffer);
1360 // Output JSON to the JSON file descriptor
1361 log_json_write(json_fd, level, time_ns, file, line, func, json_message);
1362 // Also output JSON to console using unified routing logic, respecting quiet flag
1363 if (atomic_load_u64(&g_log.terminal_output_enabled)) {
1364 int console_fd = terminal_choose_log_fd(level);
1365 log_json_write(console_fd, level, time_ns, file, line, func, json_message);
1366 }
1367 } else {
1368 // Text format: output to file and terminal
1369 // No heap allocation - use log_buffer directly
1370 // Write to file (atomic write syscall) - use original buffer (with ANSI codes)
1371 int file_fd = (int)atomic_load_u64(&g_log.file);
1372 if (file_fd >= 0 && file_fd != STDERR_FILENO) {
1373 write_to_log_file_atomic(log_buffer, msg_len, NULL);
1374 }
1375
1376 if (g_notice_text) {
1377 output_notice(level, file, line, func);
1378 return;
1379 }
1380
1381 // Write to terminal (atomic state checks)
1382 va_list args_terminal;
1383 va_start(args_terminal, fmt);
1384 write_to_terminal_atomic(level, time_buf, file, line, func, fmt, args_terminal, time_ns, true);
1385 va_end(args_terminal);
1386 }
1387}
1388
1389void log_terminal_msg(log_level_t level, const char *file, int line, const char *func, const char *fmt, ...) {
1390 // Lock-free: only uses atomic loads, no mutex
1391 if (!lifecycle_is_initialized(&g_log.lifecycle)) {
1392 return;
1393 }
1394
1395 if (level < (log_level_t)atomic_load_u64(&g_log.level)) {
1396 return;
1397 }
1398
1399 // Terminal output only - no file/mmap writing
1400 char time_buf[LOG_TIMESTAMP_BUFFER_SIZE];
1401 uint64_t time_ns = time_get_realtime_ns();
1403
1404 va_list args;
1405 va_start(args, fmt);
1406 write_to_terminal_atomic(level, time_buf, file, line, func, fmt, args, time_ns, true);
1407 va_end(args);
1408}
1409
1410static void log_colored_msg_internal(log_level_t level, const char *file, int line, const char *func,
1411 const char *colored_message, const char *plain_message, bool persist) {
1412 if (!colored_message || !plain_message || !lifecycle_is_initialized(&g_log.lifecycle)) {
1413 return;
1414 }
1415 if (level < (log_level_t)atomic_load_u64(&g_log.level)) {
1416 return;
1417 }
1418
1419 bool presenting = ui_controller_is_presenting();
1420 int fd = presenting ? STDERR_FILENO : terminal_choose_log_fd(level);
1421 bool use_colors =
1422 presenting ? atomic_load(&g_terminal_log_colors) : terminal_should_color_output(fd, options_terminal_settings());
1423 if (!presenting) {
1424 atomic_store(&g_terminal_log_colors, use_colors);
1425 }
1426
1427 char time_buf[LOG_TIMESTAMP_BUFFER_SIZE];
1428 uint64_t time_ns = time_get_realtime_ns();
1430
1431 char colored_header[LOG_HEADER_BUFFER_SIZE] = {0};
1432 char plain_header[LOG_HEADER_BUFFER_SIZE] = {0};
1433 if (format_log_header(colored_header, sizeof(colored_header), level, time_buf, file, line, func, use_colors,
1434 time_ns) < 0) {
1435 colored_header[0] = '\0';
1436 }
1437 if (format_log_header(plain_header, sizeof(plain_header), level, time_buf, file, line, func, false, time_ns) < 0) {
1438 plain_header[0] = '\0';
1439 }
1440
1441 char colored_entry[LOG_TERMINAL_ENTRY_BUFFER_SIZE];
1442 char plain_entry[LOG_TERMINAL_ENTRY_BUFFER_SIZE];
1443 safe_snprintf(colored_entry, sizeof(colored_entry), "%s%s", colored_header, colored_message);
1444 safe_snprintf(plain_entry, sizeof(plain_entry), "%s%s", plain_header, plain_message);
1445
1446 int json_fd = atomic_load_int(&g_log.json_file);
1447 if (persist && json_fd >= 0) {
1448 log_json_write(json_fd, level, time_ns, file, line, func, plain_message);
1449 } else if (persist && log_mmap_is_active()) {
1450 log_mmap_write(level, file, line, func, "%s", plain_message);
1451 } else if (persist) {
1452 int file_fd = atomic_load_int(&g_log.file);
1453 if (file_fd >= 0 && file_fd != STDERR_FILENO) {
1454 write_to_log_file_atomic(plain_entry, (int)strlen(plain_entry), NULL);
1455 }
1456 }
1457
1458 const char *terminal_message = use_colors ? colored_entry : plain_entry;
1459 session_log_buffer_append(g_history, json_fd >= 0 ? plain_message : terminal_message);
1460 atomic_fetch_add_u64_impl(&g_session_log_readers, 1);
1461 session_log_buffer_t *buffer = (session_log_buffer_t *)atomic_ptr_load(&g_log.session_log_buffer);
1462 if (buffer && buffer != g_history) {
1463 session_log_buffer_append(buffer, json_fd >= 0 ? plain_message : terminal_message);
1464 }
1465 atomic_fetch_sub_u64_impl(&g_session_log_readers, 1);
1466
1467 if (presenting || !atomic_load_u64(&g_log.terminal_output_enabled)) {
1468 return;
1469 }
1470 if (atomic_load_u64(&g_log.terminal_locked)) {
1471 uint64_t owner = atomic_load_u64(&g_log.terminal_owner_thread);
1472 if (owner != (uint64_t)thread_self()) {
1473 return;
1474 }
1475 }
1476
1477 if (json_fd >= 0) {
1478 log_json_write(fd, level, time_ns, file, line, func, plain_message);
1479 return;
1480 }
1481
1482 size_t match_start = 0;
1483 size_t match_len = 0;
1484 if (!grep_should_output(plain_entry, &match_start, &match_len)) {
1485 return;
1486 }
1487
1488 const char *output = terminal_message;
1489 if (use_colors && match_len > 0 && log_get_color_array() != NULL) {
1490 output = grep_highlight_colored(colored_entry, plain_entry, match_start, match_len);
1491 }
1492 size_t output_len = strlen(output);
1493 ui_controller_write(fd, output, output_len);
1494 if (output_len == 0 || output[output_len - 1] != '\n') {
1495 ui_controller_write(fd, "\n", 1);
1496 }
1497}
1498
1499void log_colored_msg_impl(log_level_t level, const char *file, int line, const char *func, const char *colored_message,
1500 const char *plain_message) {
1501 log_colored_msg_internal(level, file, line, func, colored_message, plain_message, true);
1502}
1503
1504void log_terminal_colored_msg_impl(log_level_t level, const char *file, int line, const char *func,
1505 const char *colored_message, const char *plain_message) {
1506 log_colored_msg_internal(level, file, line, func, colored_message, plain_message, false);
1507}
1508
1509void log_plain_msg(const char *fmt, ...) {
1510 if (!lifecycle_is_initialized(&g_log.lifecycle)) {
1511 return;
1512 }
1513
1514 char log_buffer[LOG_MSG_BUFFER_SIZE];
1515 va_list args;
1516 va_start(args, fmt);
1517 int msg_len = safe_vsnprintf(log_buffer, sizeof(log_buffer), fmt, args);
1518 va_end(args);
1519
1520 if (msg_len <= 0) {
1521 return;
1522 }
1523
1524 // Truncate at whole line boundaries to avoid UTF-8 issues
1525 msg_len = truncate_at_whole_line(log_buffer, msg_len, sizeof(log_buffer));
1526
1527 // Validate UTF-8 in formatted message
1528 validate_log_message_utf8(log_buffer, "plain text log");
1529
1530 session_log_buffer_append(g_history, log_buffer);
1531
1532 // Write to mmap if active
1533 if (log_mmap_is_active()) {
1534 log_mmap_write(LOG_INFO, NULL, 0, NULL, "%s", log_buffer);
1535 } else {
1536 // Write to file with headers (atomic write syscall)
1537 int file_fd = atomic_load_u64(&g_log.file);
1538 if (file_fd >= 0 && file_fd != STDERR_FILENO) {
1539 // Add header with timestamp and log level to file output
1540 char time_buf[LOG_TIMESTAMP_BUFFER_SIZE];
1541 uint64_t time_ns = time_get_realtime_ns();
1543
1544 char header_buffer[LOG_HEADER_BUFFER_SIZE];
1545 int header_len = format_log_header(header_buffer, sizeof(header_buffer), LOG_INFO, time_buf, "lib/log/logging.c",
1546 0, "log_plain_msg", false, time_ns);
1547
1548 if (header_len > 0) {
1549 write_to_log_file_atomic(header_buffer, header_len, NULL);
1550 }
1551 write_to_log_file_atomic(log_buffer, msg_len, NULL);
1552 write_to_log_file_atomic("\n", 1, NULL);
1553 }
1554 }
1555
1556 // Preserve diagnostics in the log file during shutdown without repainting the terminal.
1558 return;
1559
1560 // Terminal output (atomic state checks)
1561 if (!atomic_load_u64(&g_log.terminal_output_enabled)) {
1562 return;
1563 }
1564 if (atomic_load_u64(&g_log.terminal_locked)) {
1565 uint64_t owner = atomic_load_u64(&g_log.terminal_owner_thread);
1566 if (owner != (uint64_t)thread_self()) {
1567 return;
1568 }
1569 }
1570
1571 // Check if JSON format is enabled
1572 int json_fd = atomic_load_int(&g_log.json_file);
1573 bool json_format_enabled = (json_fd >= 0);
1574
1575 if (json_format_enabled) {
1576 // Output JSON for plain messages too
1577 uint64_t time_ns = time_get_realtime_ns();
1578 int console_fd = terminal_choose_log_fd(LOG_INFO);
1579 log_json_write(console_fd, LOG_INFO, time_ns, __FILE__, __LINE__, "log_plain_msg", log_buffer);
1580 } else {
1581 // Choose output stream using unified routing logic (LOG_INFO level)
1583 FILE *output_stream = (fd == STDERR_FILENO) ? stderr : stdout;
1584
1585 // Apply colorization for TTY output
1586 if (terminal_should_color_output(fd, options_terminal_settings())) {
1587 const char *colorized_msg = colorize_log_message(log_buffer);
1588 log_terminal_printf(output_stream == stdout ? STDOUT_FILENO : STDERR_FILENO, "%s\n", colorized_msg);
1589 } else {
1590 log_terminal_printf(output_stream == stdout ? STDOUT_FILENO : STDERR_FILENO, "%s\n", log_buffer);
1591 }
1592 (void)fflush(output_stream);
1593 }
1594}
1595
1596// Helper for log_plain_stderr variants (lock-free)
1597static void log_plain_stderr_internal_atomic(const char *fmt, va_list args, bool add_newline) {
1598 char log_buffer[LOG_MSG_BUFFER_SIZE];
1599 int msg_len = safe_vsnprintf(log_buffer, sizeof(log_buffer), fmt, args);
1600
1601 if (msg_len <= 0) {
1602 return;
1603 }
1604
1605 // Truncate at whole line boundaries to avoid UTF-8 issues
1606 msg_len = truncate_at_whole_line(log_buffer, msg_len, sizeof(log_buffer));
1607
1608 // Validate UTF-8 in formatted message
1609 validate_log_message_utf8(log_buffer, "stderr log");
1610 session_log_buffer_append(g_history, log_buffer);
1611
1612 // Write to mmap if active
1613 if (log_mmap_is_active()) {
1614 log_mmap_write(LOG_INFO, NULL, 0, NULL, "%s", log_buffer);
1615 } else {
1616 // Write to file with headers (atomic write syscall)
1617 int file_fd = atomic_load_u64(&g_log.file);
1618 if (file_fd >= 0 && file_fd != STDERR_FILENO) {
1619 // Add header with timestamp and log level to file output
1620 char time_buf[LOG_TIMESTAMP_BUFFER_SIZE];
1621 uint64_t time_ns = time_get_realtime_ns();
1623
1624 char header_buffer[LOG_HEADER_BUFFER_SIZE];
1625 int header_len = format_log_header(header_buffer, sizeof(header_buffer), LOG_INFO, time_buf, "lib/log/logging.c",
1626 0, "log_plain_stderr_msg", false, time_ns);
1627
1628 if (header_len > 0) {
1629 write_to_log_file_atomic(header_buffer, header_len, NULL);
1630 }
1631 write_to_log_file_atomic(log_buffer, msg_len, NULL);
1632 if (add_newline) {
1633 write_to_log_file_atomic("\n", 1, NULL);
1634 }
1635 }
1636 }
1637
1638 // Terminal output (atomic state checks)
1639 if (!atomic_load_u64(&g_log.terminal_output_enabled)) {
1640 return;
1641 }
1642 if (atomic_load_u64(&g_log.terminal_locked)) {
1643 uint64_t owner = atomic_load_u64(&g_log.terminal_owner_thread);
1644 if (owner != (uint64_t)thread_self()) {
1645 return;
1646 }
1647 }
1648
1649 // Write to stderr without colorization (plain output)
1650 if (add_newline) {
1651 log_terminal_printf(STDERR_FILENO, "%s\n", log_buffer);
1652 } else {
1653 log_terminal_printf(STDERR_FILENO, "%s", log_buffer);
1654 }
1655 (void)fflush(stderr);
1656}
1657
1658void log_plain_stderr_msg(const char *fmt, ...) {
1659 if (!lifecycle_is_initialized(&g_log.lifecycle)) {
1660 return;
1661 }
1662 if (shutdown_is_requested()) {
1663 return;
1664 }
1665
1666 va_list args;
1667 va_start(args, fmt);
1668 log_plain_stderr_internal_atomic(fmt, args, true);
1669 va_end(args);
1670}
1671
1672void log_plain_stderr_nonewline_msg(const char *fmt, ...) {
1673 if (!lifecycle_is_initialized(&g_log.lifecycle)) {
1674 return;
1675 }
1676 if (shutdown_is_requested()) {
1677 return;
1678 }
1679
1680 va_list args;
1681 va_start(args, fmt);
1682 log_plain_stderr_internal_atomic(fmt, args, false);
1683 va_end(args);
1684}
1685
1691static void log_plain_stdout_internal_atomic(const char *fmt, va_list args, bool add_newline) {
1692 char log_buffer[LOG_MSG_BUFFER_SIZE];
1693 int msg_len = safe_vsnprintf(log_buffer, sizeof(log_buffer), fmt, args);
1694
1695 if (msg_len <= 0) {
1696 return;
1697 }
1698
1699 // Truncate at whole line boundaries to avoid UTF-8 issues
1700 msg_len = truncate_at_whole_line(log_buffer, msg_len, sizeof(log_buffer));
1701
1702 // Validate UTF-8 in formatted message
1703 validate_log_message_utf8(log_buffer, "stdout log");
1704 session_log_buffer_append(g_history, log_buffer);
1705
1706 // Write to mmap if active
1707 if (log_mmap_is_active()) {
1708 log_mmap_write(LOG_INFO, NULL, 0, NULL, "%s", log_buffer);
1709 } else {
1710 // Write to file with headers (atomic write syscall)
1711 int file_fd = atomic_load_u64(&g_log.file);
1712 if (file_fd >= 0 && file_fd != STDOUT_FILENO) {
1713 // Add header with timestamp and log level to file output
1714 char time_buf[LOG_TIMESTAMP_BUFFER_SIZE];
1715 uint64_t time_ns = time_get_realtime_ns();
1717
1718 char header_buffer[LOG_HEADER_BUFFER_SIZE];
1719 int header_len = format_log_header(header_buffer, sizeof(header_buffer), LOG_INFO, time_buf, "lib/log/logging.c",
1720 0, "log_plain_stdout_msg", false, time_ns);
1721
1722 if (header_len > 0) {
1723 write_to_log_file_atomic(header_buffer, header_len, NULL);
1724 }
1725 write_to_log_file_atomic(log_buffer, msg_len, NULL);
1726 if (add_newline) {
1727 write_to_log_file_atomic("\n", 1, NULL);
1728 }
1729 }
1730 }
1731
1732 // Terminal output (atomic state checks)
1733 if (!atomic_load_u64(&g_log.terminal_output_enabled)) {
1734 return;
1735 }
1736 if (atomic_load_u64(&g_log.terminal_locked)) {
1737 uint64_t owner = atomic_load_u64(&g_log.terminal_owner_thread);
1738 if (owner != (uint64_t)thread_self()) {
1739 return;
1740 }
1741 }
1742
1743 // Write to stdout without colorization (plain output)
1744 if (add_newline) {
1745 log_terminal_printf(STDOUT_FILENO, "%s\n", log_buffer);
1746 } else {
1747 log_terminal_printf(STDOUT_FILENO, "%s", log_buffer);
1748 }
1749}
1750
1751void log_plain_stdout_msg(const char *fmt, ...) {
1752 if (!lifecycle_is_initialized(&g_log.lifecycle)) {
1753 return;
1754 }
1755 if (shutdown_is_requested()) {
1756 return;
1757 }
1758
1759 va_list args;
1760 va_start(args, fmt);
1761 log_plain_stdout_internal_atomic(fmt, args, true);
1762 va_end(args);
1763}
1764
1765void log_file_msg(const char *fmt, ...) {
1766 if (!lifecycle_is_initialized(&g_log.lifecycle)) {
1767 return;
1768 }
1769
1770 char log_buffer[LOG_MSG_BUFFER_SIZE];
1771 va_list args;
1772 va_start(args, fmt);
1773 int msg_len = safe_vsnprintf(log_buffer, sizeof(log_buffer), fmt, args);
1774 va_end(args);
1775
1776 if (msg_len <= 0) {
1777 return;
1778 }
1779
1780 // Truncate at whole line boundaries to avoid UTF-8 issues
1781 msg_len = truncate_at_whole_line(log_buffer, msg_len, sizeof(log_buffer));
1782
1783 // Validate UTF-8 in formatted message
1784 validate_log_message_utf8(log_buffer, "file-only log");
1785 session_log_buffer_append(g_history, log_buffer);
1786
1787 // Write to mmap if active, else to file
1788 if (log_mmap_is_active()) {
1789 log_mmap_write(LOG_INFO, NULL, 0, NULL, "%s", log_buffer);
1790 } else {
1791 int file_fd = atomic_load_u64(&g_log.file);
1792 if (file_fd >= 0 && file_fd != STDERR_FILENO) {
1793 write_to_log_file_atomic(log_buffer, msg_len, NULL);
1794 write_to_log_file_atomic("\n", 1, NULL);
1795 }
1796 }
1797}
1798
1799static const char *log_network_direction_label(remote_log_direction_t direction) {
1800 switch (direction) {
1802 return "server→client";
1804 return "client→server";
1805 default:
1806 return "network";
1807 }
1808}
1809
1811 const char *file, int line, const char *func, const char *fmt, ...) {
1812 if (!fmt) {
1813 return SET_ERRNO(ERROR_INVALID_PARAM, "Format string is NULL");
1814 }
1815
1816 va_list args_copy;
1817 va_start(args_copy, fmt);
1819 char *formatted = format_message(fmt, args_copy);
1820 va_end(args_copy);
1821
1822 if (!formatted) {
1824 asciichat_error_t current_error = errno_peek_since(format_scope, &current) ? current.code : ASCIICHAT_OK;
1825 if (current_error == ASCIICHAT_OK) {
1826 current_error = SET_ERRNO(ERROR_MEMORY, "Failed to format network log message");
1827 }
1828 return current_error;
1829 }
1830
1831 asciichat_error_t send_result = ASCIICHAT_OK;
1832 if (!transport) {
1833 send_result = SET_ERRNO(ERROR_INVALID_PARAM, "Missing log transport");
1834 log_msg(LOG_WARN, file, line, func, "Skipping remote log message: transport unavailable");
1835 } else {
1836 send_result = acip_send_remote_log(transport, level, direction, formatted);
1837 if (send_result != ASCIICHAT_OK) {
1838 log_msg(LOG_WARN, file, line, func, "Failed to send remote log message: %s", asciichat_error_string(send_result));
1839 }
1840 }
1841
1842 const char *direction_label = log_network_direction_label(direction);
1843 log_msg(level, file, line, func, "[NET %s] %s", direction_label, formatted);
1844
1845 SAFE_FREE(formatted);
1846 return send_result;
1847}
1848
1849/* ============================================================================
1850 * Color Helper Functions
1851 * ============================================================================ */
1852
1853/* Initialize terminal capabilities if not already done */
1854static void init_terminal_capabilities(void) {
1855 // Guard against recursion - this can be called indirectly via log_get_color_array()
1856 // from detect_terminal_capabilities() which uses logging
1857 if (g_log.terminal_caps_detecting) {
1858 return;
1859 }
1860
1861 // Terminal capabilities should already be initialized via log_system_init()
1862 // This function is now just a safety guard for any edge cases before init
1863 if (!g_log.terminal_caps_initialized) {
1864 // Set detecting flag to prevent recursion
1865 g_log.terminal_caps_detecting = true;
1866
1867 // Use safe fallback during very early initialization to avoid recursion
1868 g_log.terminal_caps.color_level = TERM_COLOR_16;
1869 g_log.terminal_caps.capabilities = TERM_CAP_COLOR_16;
1870 g_log.terminal_caps.color_count = 16;
1871 g_log.terminal_caps.detection_reliable = false;
1872 g_log.terminal_caps_initialized = true;
1873
1874 // Clear detecting flag
1875 g_log.terminal_caps_detecting = false;
1876 }
1877}
1878
1879/* Re-detect terminal capabilities after logging is initialized */
1881
1882 // Guard against recursion
1883 if (g_log.terminal_caps_detecting) {
1884 return;
1885 }
1886
1887 // Detect if not initialized, or if we're using defaults (not reliably detected)
1888 // This ensures we get proper detection after logging is ready, replacing any defaults
1889 // Once we have reliable detection, never re-detect to keep colors consistent
1890 if (!g_log.terminal_caps_initialized || !g_log.terminal_caps.detection_reliable) {
1891 g_log.terminal_caps_detecting = true;
1892 detect_terminal_capabilities_into(options_terminal_settings(), &g_log.terminal_caps);
1893 g_log.terminal_caps_detecting = false;
1894 g_log.terminal_caps_initialized = true;
1895
1896 // Now log the capabilities AFTER colors are set, so this log uses the correct colors
1897 log_debug("Terminal capabilities: color_level=%d, capabilities=0x%x, utf8=%s, fps=%d",
1898 g_log.terminal_caps.color_level, g_log.terminal_caps.capabilities,
1899 g_log.terminal_caps.utf8_support ? "yes" : "no", g_log.terminal_caps.desired_fps);
1900
1901 // Now that we've detected once with reliable results, keep these colors consistent for all future logs
1902 } else {
1903 }
1904 // Once initialized with reliable detection, never re-detect to keep colors consistent
1905}
1906
1907/* Get the appropriate color array based on terminal capabilities */
1908static _Thread_local colorscheme_palette_t live_log_palette;
1909static _Thread_local const char *live_log_codes[COLORSCHEME_LOG_COLOR_COUNT];
1910
1911const char **log_get_color_array(void) {
1912 init_terminal_capabilities();
1913 colorscheme_current_palette(&live_log_palette, g_log.terminal_caps.color_level);
1914 for (int i = 0; i < 8; ++i)
1915 live_log_codes[i] = live_log_palette.codes[i];
1916 return live_log_codes;
1917}
1918
1920 init_terminal_capabilities();
1921 return g_log.terminal_caps.color_level;
1922}
1923
1924const char *log_get_foreground_color(void) {
1926 return live_log_palette.foreground;
1927}
1928
1929const char *log_level_color(log_color_t color) {
1930 if (color == LOG_COLOR_WHITE)
1931 return "\033[37m";
1932 const char **colors = log_get_color_array();
1933 if (colors == NULL) {
1934 return ""; /* Return empty string if colors not available */
1935 }
1936 if (color >= 0 && color <= LOG_COLOR_RESET) {
1937 return colors[color];
1938 }
1939 return colors[LOG_COLOR_RESET]; /* Return reset color if invalid */
1940}
1941
1943 if (!enabled)
1944 return "";
1945 if (color == LOG_COLOR_WHITE)
1946 return "\033[37m";
1947 static _Thread_local colorscheme_palette_t palette;
1948 // The caller already resolved whether this destination accepts color. Do not
1949 // re-check stdout here: reports can target a terminal independent of stdout.
1950 terminal_color_mode_t mode = GET_OPTION(color_mode);
1951 if (mode == TERM_COLOR_AUTO)
1952 mode = (terminal_color_mode_t)log_get_color_mode();
1953 if (mode == TERM_COLOR_NONE && GET_OPTION(color_mode) == TERM_COLOR_AUTO)
1954 mode = TERM_COLOR_16;
1955 colorscheme_current_palette(&palette, mode);
1956 return palette.codes[color >= 0 && color <= LOG_COLOR_RESET ? color : LOG_COLOR_RESET];
1957}
1958
1959/* ============================================================================
1960 * Color Scheme Management
1961 * ============================================================================ */
1962
1964 if (g_log.terminal_caps_detecting || !lifecycle_is_initialized(&g_log.lifecycle))
1965 return;
1967}
1968
1970 colorscheme_apply(scheme);
1971}
1972
1973/* ============================================================================
1974 * Lock-Free MMAP Logging Integration
1975 * ============================================================================ */
1976
1977asciichat_error_t log_enable_mmap(const char *log_path) {
1978 return log_enable_mmap_sized(log_path, 0); /* Use default size */
1979}
1980
1981asciichat_error_t log_enable_mmap_sized(const char *log_path, size_t max_size) {
1982 if (!log_path) {
1983 return SET_ERRNO(ERROR_INVALID_PARAM, "log_path is required");
1984 }
1985
1986 // Initialize mmap logging - text is written directly to the mmap'd file
1987 asciichat_error_t result = log_mmap_init_simple(log_path, max_size);
1988 if (result != ASCIICHAT_OK) {
1989 return result;
1990 }
1991
1992 log_info("Lock-free mmap logging enabled: %s", log_path);
1993 return ASCIICHAT_OK;
1994}
1995
1997 if (log_mmap_is_active()) {
1999 log_info("Lock-free mmap logging disabled");
2000 }
2001}
2002
2003/* ============================================================================
2004 * Shutdown Logging Control
2005 * ============================================================================ */
2006
2008 if (g_log.shutdown_in_progress) {
2009 return; /* Already in shutdown phase */
2010 }
2011
2012 /* Save current terminal output state and disable console output */
2013 g_log.shutdown_saved_terminal_output = atomic_load_u64(&g_log.terminal_output_enabled);
2014 atomic_store_bool(&g_log.terminal_output_enabled, false);
2015 g_log.shutdown_in_progress = true;
2016}
2017
2019 if (!g_log.shutdown_in_progress) {
2020 return; /* Not in shutdown phase, skip terminal state restoration */
2021 }
2022
2023 /* Restore previous terminal output state */
2024 atomic_store_u64(&g_log.terminal_output_enabled, g_log.shutdown_saved_terminal_output);
2025 g_log.shutdown_in_progress = false;
2026}
2027
2035
2036/* ============================================================================
2037 * Session Log Buffer Registration
2038 * ============================================================================ */
2039
2049 atomic_ptr_store(&g_log.session_log_buffer, (void *)buf);
2050}
2051
2058 atomic_ptr_store(&g_log.session_log_buffer, NULL);
2059 // An atomic pointer swap alone does not protect a writer that loaded the old
2060 // buffer. Wait for those writers before the UI releases its storage.
2061 while (atomic_load_u64_impl(&g_session_log_readers))
2062 platform_sleep_us(100);
2063}
2064
2072 return (session_log_buffer_t *)atomic_ptr_load(&g_log.session_log_buffer);
2073}
2074
2075/* ============================================================================
2076 * Log Recoloring for Plain Text Entries
2077 * ============================================================================ */
2078
2085size_t log_recolor_plain_entry(const char *plain_line, char *colored_buf, size_t buf_size) {
2086 if (!plain_line || !colored_buf || buf_size < 128) {
2087 return 0;
2088 }
2089
2090 static char work_buffer[LOG_MSG_BUFFER_SIZE + 1024];
2091
2092 // Parse format FIRST, regardless of color availability
2093 // Parse format: [TIMESTAMP] [LEVEL] [tid:THREAD_ID] FILE:LINE in FUNC(): MESSAGE
2094 const char *p = plain_line;
2095
2096 // Extract timestamp [TIMESTAMP]
2097 if (*p != '[') {
2098 return 0; // Invalid format
2099 }
2100 p++;
2101 const char *timestamp_start = p;
2102
2103 // Find the closing ] for timestamp - look for pattern that ends with proper timestamp format
2104 // Valid timestamp: HH:MM:SS.UUUUUU (time with microseconds, no date)
2105 while (*p && *p != ']') {
2106 p++;
2107 }
2108 if (*p != ']') {
2109 return 0; // Malformed - no closing bracket
2110 }
2111
2112 size_t timestamp_len = p - timestamp_start;
2113 char timestamp[LOG_PARSE_TIMESTAMP_BUFFER_SIZE];
2114 if (timestamp_len >= sizeof(timestamp) || timestamp_len == 0) {
2115 return 0; // Invalid timestamp - empty or too long
2116 }
2117 SAFE_STRNCPY(timestamp, timestamp_start, timestamp_len);
2118 timestamp[timestamp_len] = '\0';
2119 p++; // Skip ]
2120
2121 // Skip whitespace after timestamp
2122 while (*p && *p == ' ') {
2123 p++;
2124 }
2125
2126 // Extract level [LEVEL]
2127 if (*p != '[') {
2128 return 0; // Missing opening bracket for level
2129 }
2130 p++;
2131 const char *level_start = p;
2132 while (*p && *p != ']') {
2133 p++;
2134 }
2135 if (*p != ']') {
2136 return 0; // Missing closing bracket for level
2137 }
2138 size_t level_len = p - level_start;
2139 char level_str[LOG_LEVEL_BUFFER_SIZE];
2140 if (level_len >= sizeof(level_str) || level_len == 0) {
2141 return 0; // Level string too long or empty
2142 }
2143 memcpy(level_str, level_start, level_len);
2144 level_str[level_len] = '\0';
2145
2146 // Determine log level for color selection
2147 log_level_t level = LOG_INFO; // Default
2148 if (strstr(level_str, "DEV") || strstr(level_str, "DEBUG")) {
2149 level = LOG_DEBUG;
2150 } else if (strstr(level_str, "INFO")) {
2151 level = LOG_INFO;
2152 } else if (strstr(level_str, "WARN")) {
2153 level = LOG_WARN;
2154 } else if (strstr(level_str, "ERROR")) {
2155 level = LOG_ERROR;
2156 } else if (strstr(level_str, "FATAL")) {
2157 level = LOG_FATAL;
2158 }
2159
2160 p++; // Skip ]
2161
2162 // Skip whitespace after level
2163 while (*p && *p == ' ') {
2164 p++;
2165 }
2166
2167 // Extract thread ID [tid:THREAD_ID] - optional field
2168 uint64_t tid = 0;
2169 if (*p == '[' && strncmp(p, "[tid:", 5) == 0) {
2170 p += 5;
2171 char *tid_end = NULL;
2172 tid = strtoull(p, &tid_end, 10);
2173 if (!tid_end || *tid_end != ']') {
2174 // tid parsing failed, but continue anyway (might still recover)
2175 // Try to find the closing bracket
2176 while (*p && *p != ']') {
2177 p++;
2178 }
2179 if (*p == ']') {
2180 p++;
2181 }
2182 } else {
2183 p = tid_end + 1; // Skip past the ]
2184 }
2185 // Skip whitespace after tid
2186 while (*p && *p == ' ') {
2187 p++;
2188 }
2189 }
2190 // tid is optional - continue parsing if not present
2191
2192 // Extract file path (everything up to :LINE)
2193 const char *file_start = p;
2194 while (*p && *p != ':') {
2195 p++;
2196 }
2197 if (*p != ':') {
2198 return 0; // Malformed
2199 }
2200 size_t file_len = p - file_start;
2202 if (file_len >= sizeof(file_path)) {
2203 return 0;
2204 }
2205 memcpy(file_path, file_start, file_len);
2206 file_path[file_len] = '\0';
2207 p++; // Skip :
2208
2209 // Extract line number (digits only)
2210 int line_num = 0;
2211 const char *line_start = p;
2212 while (*p && isdigit((unsigned char)*p)) {
2213 p++;
2214 }
2215 if (p == line_start) {
2216 return 0; // No line number
2217 }
2218 line_num = (int)strtol(line_start, NULL, 10);
2219
2220 // Skip whitespace and find "in" keyword (be very lenient)
2221 while (*p && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) {
2222 p++;
2223 }
2224
2225 // Try to find "in " - if not found, might still be valid, just harder to parse
2226 if (strncmp(p, "in ", 3) == 0) {
2227 p += 3; // Skip "in "
2228 } else if (*p == 'i' && *(p + 1) == 'n' && (*(p + 2) == ' ' || *(p + 2) == '\t')) {
2229 // Allow tab after "in"
2230 p += 2; // Skip "in"
2231 while (*p && (*p == ' ' || *p == '\t')) {
2232 p++;
2233 }
2234 } else {
2235 // Missing "in" keyword - this is a format error
2236 return 0;
2237 }
2238
2239 // Extract function name (everything up to "()")
2240 const char *func_start = p;
2241 while (*p && *p != '(') {
2242 p++;
2243 }
2244 if (*p != '(' || func_start == p) {
2245 return 0; // Missing function name or parentheses
2246 }
2247 size_t func_len = p - func_start;
2248
2249 // Trim trailing whitespace from function name
2250 while (func_len > 0 && (func_start[func_len - 1] == ' ' || func_start[func_len - 1] == '\t')) {
2251 func_len--;
2252 }
2253
2254 char func_name[LOG_FUNC_NAME_BUFFER_SIZE];
2255 if (func_len >= sizeof(func_name)) {
2256 func_len = sizeof(func_name) - 1;
2257 }
2258 if (func_len > 0) {
2259 memcpy(func_name, func_start, func_len);
2260 }
2261 func_name[func_len] = '\0';
2262
2263 // Skip "(" and ")" - be lenient about what's between them
2264 if (*p == '(') {
2265 p++;
2266 while (*p && *p != ')') {
2267 p++;
2268 }
2269 if (*p == ')') {
2270 p++;
2271 }
2272 }
2273
2274 // Skip whitespace and optional colon(s) and other separators
2275 while (*p && (*p == ' ' || *p == ':' || *p == '\t')) {
2276 p++;
2277 }
2278
2279 // Remaining is the message
2280 const char *message = p;
2281
2282 // Format is valid, get colors from logging system
2283 const char **colors = log_get_color_array();
2284 if (!colors) {
2285 // No colors available, return plain text
2286 static bool warned_once = false;
2287 if (!warned_once) {
2288 log_debug("Colors not initialized - returning plain text");
2289 warned_once = true;
2290 }
2291 size_t len = strlen(plain_line);
2292 if (len >= buf_size) {
2293 return 0;
2294 }
2295 SAFE_STRNCPY(colored_buf, plain_line, buf_size - 1);
2296 colored_buf[buf_size - 1] = '\0';
2297 return len;
2298 }
2299
2300 // Build colored output
2301 const char *level_color = colors[level];
2302 const char *reset = colors[LOG_COLOR_RESET];
2303 const char *file_color = colors[1]; // DEBUG/Cyan
2304 const char *line_color = colors[6]; // GREY (matching tid)
2305 const char *func_color = colors[0]; // DEV/Orange
2306 const char *tid_color = colors[6]; // GREY
2307
2308 int len = safe_snprintf(work_buffer, sizeof(work_buffer),
2309 "[%s%s%s] [%s%s%s] [tid:%s%llu%s] %s%s%s:%s%d%s in %s%s%s(): %s", level_color, timestamp,
2310 reset, level_color, level_str, reset, tid_color, (unsigned long long)tid, reset, file_color,
2311 file_path, reset, line_color, line_num, reset, func_color, func_name, reset, message);
2312
2313 if (len <= 0 || len >= (int)sizeof(work_buffer)) {
2314 return 0;
2315 }
2316
2317 // Colorize the message part
2318 const char *colorized_msg = colorize_log_message(message);
2319 len = safe_snprintf(work_buffer, sizeof(work_buffer),
2320 "[%s%s%s] [%s%s%s] [tid:%s%llu%s] %s%s%s:%s%d%s in %s%s%s(): %s", level_color, timestamp, reset,
2321 level_color, level_str, reset, tid_color, (unsigned long long)tid, reset, file_color, file_path,
2322 reset, line_color, line_num, reset, func_color, func_name, reset, colorized_msg);
2323
2324 if (len <= 0 || len >= (int)sizeof(work_buffer) || len >= (int)buf_size) {
2325 return 0;
2326 }
2327
2328 SAFE_STRNCPY(colored_buf, work_buffer, buf_size - 1);
2329 colored_buf[buf_size - 1] = '\0';
2330 return (size_t)len;
2331}
2332
2333// Internal implementation - don't use directly, use log_console_impl macro
2334void log_console_impl(log_level_t level, const char *file, int line, const char *func, const char *message) {
2335 if (!message) {
2336 return;
2337 }
2338
2339 int fd = terminal_choose_log_fd(level);
2340 if (fd < 0) {
2341 return;
2342 }
2343
2344 // Presentation owns terminal output, but never changes the caller's logging preference.
2345 if (ui_controller_is_presenting() && (ui_controller_is_owner() || platform_isatty(fd))) {
2346 return;
2347 }
2348
2349 // Check if JSON output is enabled (json_file >= 0 means enabled)
2350 int json_fd = atomic_load_int(&g_log.json_file);
2351 bool use_json = (json_fd >= 0);
2352
2353 if (use_json) {
2354 // Use async-safe JSON formatter (safe for signal handlers)
2355 log_json_async_safe(fd, level, file, line, func, message);
2356 } else {
2357 // Text output to console using platform_write_all to handle partial writes
2358 size_t msg_len = strlen(message);
2359 ui_controller_write(fd, message, msg_len);
2360 if (msg_len == 0 || message[msg_len - 1] != '\n') {
2361 ui_controller_write(fd, "\n", 1);
2362 }
2363 }
2364}
2365
2375void *log_get_template(void) {
2376 return (void *)g_log.format;
2377}
action_args_t args
Definition actions.c:78
ANSI escape sequence utilities and fast color code generation.
void platform_log_hook(log_level_t level, const char *message)
Definition apple.c:55
bool errno_peek_since(asciichat_errno_scope_t scope, asciichat_error_context_t *out)
asciichat_errno_scope_t errno_checkpoint(void)
#define SET_ERRNO(code,...)
const char * colorize_log_message(const char *message)
Colorize a log message for terminal output.
Definition colorize.c:536
Log message colorization for terminal output.
void colorscheme_apply(const color_scheme_t *scheme)
const color_scheme_t * colorscheme_get_active_scheme(void)
Get currently active color scheme.
void colorscheme_current_palette(colorscheme_palette_t *palette, terminal_color_mode_t mode)
#define COLORSCHEME_LOG_COLOR_COUNT
Definition colorscheme.h:40
⚙️ Common definitions, error codes, macros, and types shared throughout the application
int color_mode
Definition fft.c:25
unsigned char colors[(512 *256 *2) *3]
Definition fft.c:22
audio_visualization_source_t source
Definition fft.c:24
void grep_destroy(void)
Clean up filter resources.
Definition grep.c:1005
bool grep_should_output(const char *log_line, size_t *match_start, size_t *match_len)
Check if log line should be output (matches filter).
Definition grep.c:512
bool enabled
Is filtering active?
Definition grep.c:62
int buffer_size
Size of circular buffer.
Definition grep.c:68
const char * grep_highlight_colored(const char *colored_text, const char *plain_text, size_t match_start, size_t match_len)
Highlight matches in colored text while preserving original colors.
Definition grep.c:670
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:423
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_MALLOC(size, cast)
Definition common.h:265
#define SAFE_GETENV(name)
Definition common.h:449
#define PLATFORM_MAX_PATH_LENGTH
Definition common.h:95
unsigned long long uint64_t
Definition common.h:63
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ERROR_INVALID_STATE
@ ERROR_BUFFER_FULL
@ ERROR_MEMORY
Definition error_codes.h:51
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_INVALID_PARAM
void log_truncate_if_large(void)
Manually truncate large log files.
Definition log.c:853
void log_plain_stderr_nonewline_msg(const char *fmt,...)
Plain logging to stderr without trailing newline.
Definition log.c:1672
void log_plain_msg(const char *fmt,...)
Plain logging without timestamps or levels.
Definition log.c:1509
#define log_warn(...)
Log a WARN message.
Definition log/log.h:621
bool log_get_terminal_output(void)
Get current terminal output setting.
Definition log.c:763
asciichat_error_t log_enable_mmap(const char *log_path)
Enable lock-free mmap-based logging.
Definition log.c:1977
void log_init_colors(void)
Initialize logging color system with current terminal capabilities.
Definition log.c:1963
void log_system_init(void)
Initialize the logging system internal state (system-level init).
Definition log.c:712
void log_msg(log_level_t level, const char *file, int line, const char *func, const char *fmt,...)
Log a message at a specific level.
Definition log.c:1196
void log_shutdown_end(void)
End shutdown phase - restore previous logging settings.
Definition log.c:2018
asciichat_error_t log_net_message(struct acip_transport *transport, log_level_t level, remote_log_direction_t direction, const char *file, int line, const char *func, const char *fmt,...)
Log a message to all destinations (network, file, and terminal).
Definition log.c:1810
void log_destroy(void)
Destroy the logging system and close log file.
Definition log.c:667
void log_colored_msg_impl(log_level_t level, const char *file, int line, const char *func, const char *colored_message, const char *plain_message)
Write semantically colored terminal text and an ANSI-free log equivalent.
Definition log.c:1499
void log_set_force_stderr(bool enabled)
Force all terminal log output to stderr.
Definition log.c:767
enum remote_log_direction remote_log_direction_t
Remote log packet direction enumeration.
log_level_t log_get_level(void)
Get the current minimum log level.
Definition log.c:742
const char ** log_get_color_array(void)
Get the appropriate color array based on terminal capabilities.
Definition log.c:1911
const char * log_level_color_for_output(log_color_t color, bool enabled)
Definition log.c:1942
const char * log_level_color(log_color_t color)
Get color string for a given color enum.
Definition log.c:1929
int log_template_apply(const log_template_t *format, char *buf, size_t buf_size, log_level_t level, const char *timestamp, const char *file, int line, const char *func, uint64_t tid, const char *message, bool use_colors, uint64_t time_nanoseconds)
Apply format to a log entry and write result to buffer.
Definition log/format.c:498
struct session_log_buffer session_log_buffer_t
Thread-safe fixed-size history ring for session-screen log lines.
void log_set_flush_delay(unsigned int delay_ms)
Set the delay between flushing buffered log entries.
Definition log.c:849
const char * log_get_foreground_color(void)
Definition log.c:1924
session_log_buffer_t * log_get_session_log_buffer(void)
Get the currently registered session log buffer.
Definition log.c:2071
#define LOG_MSG_BUFFER_SIZE
Maximum size of a single log message (including formatting).
Definition log/log.h:92
char * format_message(const char *format, va_list args)
Format a message using va_list.
Definition log.c:278
log_color_t
Color enum for logging - indexes into color arrays.
Definition log/log.h:147
void log_cleanup_colors(void)
Clean up compiled color scheme.
Definition log.c:2034
#define LOG_HEADER_BUFFER_SIZE
Maximum size of a log header (timestamp, level, file:line:func).
Definition log/log.h:98
void log_shutdown_begin(void)
Begin shutdown phase - disable console logging but keep file logging.
Definition log.c:2007
void log_disable_file_output(void)
Disable file output and use stderr instead.
Definition log.c:779
#define log_error(...)
Log an ERROR message.
Definition log/log.h:634
bool log_get_force_stderr(void)
Get current force_stderr setting.
Definition log.c:771
void log_set_level(log_level_t level)
Set the minimum log level.
Definition log.c:737
size_t log_recolor_plain_entry(const char *plain_line, char *colored_buf, size_t buf_size)
Recolor a plain (non-colored) log line with proper ANSI codes.
Definition log.c:2085
void log_clear_session_log_buffer(void)
Unregister the session log buffer.
Definition log.c:2057
void log_plain_stdout_msg(const char *fmt,...)
Plain logging to stdout with newline.
Definition log.c:1751
void log_set_session_log_buffer(session_log_buffer_t *buf)
Register a session log buffer with the logger.
Definition log.c:2048
void log_system_destroy(void)
Shutdown the logging system internal state (system-level cleanup).
Definition log.c:729
void log_init(const char *filename, log_level_t level, bool force_stderr, bool use_mmap)
Initialize the logging system.
Definition log.c:569
#define LOG_TERMINAL_BUFFER_SIZE
Definition log/log.h:104
bool log_lock_terminal(void)
Lock terminal output for exclusive access by the calling thread.
Definition log.c:833
log_level_t
Logging levels enumeration.
Definition types.h:29
void log_template_free(log_template_t *format)
Free compiled format structure.
Definition log/format.c:332
#define log_info(...)
Log an INFO message.
Definition log/log.h:608
asciichat_error_t log_set_format(const char *format_str, bool console_only)
Set a custom log format string.
Definition log.c:790
#define LOG_TERMINAL_ENTRY_BUFFER_SIZE
Definition log/log.h:107
log_template_t * log_template_parse(const char *format_str, bool console_only)
Parse a format string into compiled format structure.
Definition log/format.c:328
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:595
size_t get_current_time_formatted(char *time_buf)
Get current time as formatted string.
Definition log.c:246
asciichat_error_t log_enable_mmap_sized(const char *log_path, size_t max_size)
Enable lock-free mmap logging with custom file size.
Definition log.c:1981
#define MAX_LOG_SIZE
Maximum log file size in bytes (3MB) before rotation.
Definition log/log.h:76
void log_file_msg(const char *fmt,...)
Log to file only, no stderr output.
Definition log.c:1765
void log_disable_mmap(void)
Disable mmap logging and return to mutex-based logging.
Definition log.c:1996
#define LOG_TIMESTAMP_BUFFER_SIZE
Maximum size of a timestamp string.
Definition log/log.h:101
void log_set_terminal_output(bool enabled)
Control stderr output to terminal.
Definition log.c:746
void * log_get_template(void)
Get the current log format template.
Definition log.c:2375
#define log_plain(...)
Plain logging - writes to both log file and stderr without timestamps or log levels.
Definition log/log.h:665
int log_get_color_mode(void)
Definition log.c:1919
#define LOG_MMAP_MSG_BUFFER_SIZE
Maximum size of a log message in mmap mode.
Definition log/log.h:95
void log_set_color_scheme(const color_scheme_t *scheme)
Set the color scheme for logging output.
Definition log.c:1969
void log_plain_stderr_msg(const char *fmt,...)
Plain logging to stderr with newline.
Definition log.c:1658
void log_redetect_terminal_capabilities(void)
Re-detect terminal capabilities after logging is initialized.
Definition log.c:1880
void log_unlock_terminal(bool previous_state)
Release terminal lock and flush buffered messages.
Definition log.c:842
#define DEFAULT_LOG_LEVEL
Default log level for debug builds (DEBUG and above).
Definition log/log.h:69
void log_terminal_colored_msg_impl(log_level_t level, const char *file, int line, const char *func, const char *colored_message, const char *plain_message)
Write semantically colored terminal text without persistent logging.
Definition log.c:1504
void log_terminal_msg(log_level_t level, const char *file, int line, const char *func, const char *fmt,...)
Log a message to terminal only (no file output).
Definition log.c:1389
@ REMOTE_LOG_DIRECTION_SERVER_TO_CLIENT
Definition network/log.h:22
@ REMOTE_LOG_DIRECTION_CLIENT_TO_SERVER
Definition network/log.h:23
@ LOG_COLOR_RESET
Definition log/log.h:155
@ LOG_COLOR_WHITE
Definition log/log.h:156
int32_t vec_t __attribute__((vector_size(4 *4)))
SESSION_CREATED (PACKET_TYPE_ACIP_SESSION_CREATED) - Session created response.
Definition vector.h:42
#define NS_PER_SEC_INT
Definition time.h:169
#define US_PER_SEC_INT
Definition time.h:173
#define GET_OPTION(field)
Safely get a specific option field (lock-free read).
Definition options.h:1336
struct options_state options_t
Immutable, RCU-published snapshot of the application's runtime configuration.
const options_t * options_get(void)
Get pointer to current options struct (lock-free, thread-safe).
Definition rcu.c:511
#define OPT_LOG_TEMPLATE_DEFAULT
Default log template string (selected based on build mode).
Definition options.h:391
#define options_terminal_settings()
Definition options.h:1296
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Platform-safe snprintf wrapper.
int safe_vsnprintf(char *buffer, size_t buffer_size, const char *format, va_list ap)
Platform-safe vsnprintf wrapper.
asciichat_error_t platform_localtime(const time_t *timer, struct tm *result)
Platform-safe localtime wrapper.
ssize_t platform_write(int fd, const void *buf, size_t count)
Platform-safe write function.
bool shutdown_is_requested(void)
Check if shutdown has been requested.
Definition common.c:86
bool utf8_is_valid(const char *str)
Check if a string is valid UTF-8.
Definition utf8.c:128
📝 JSON structured logging output
void log_json_write(int fd, log_level_t level, uint64_t time_nanoseconds, const char *file, int line, const char *func, const char *message)
Write a log entry as a JSON object to the json output fd.
Definition log/json.c:104
void log_json_async_safe(int fd, log_level_t level, const char *file, int line, const char *func, const char *message)
Async-safe JSON logging for signal handlers.
Definition log/json.c:273
📝 Logging API with multiple log levels and terminal output control
#define log_warn_every(interval_us, fmt,...)
Rate-limited WARN logging.
Definition log/log.h:755
Lock-free memory-mapped text logging with crash safety.
session_log_buffer_t * log_get_history_buffer(void)
Definition log.c:565
#define LOG_LEVEL_BUFFER_SIZE
Definition log.c:6
#define LOG_FILE_PATH_BUFFER_SIZE
Definition log.c:7
int terminal_choose_log_fd(int level)
Definition log/terminal.c:7
#define LOG_BUFFER_BUFFER_SIZE
Definition log.c:3
#define LOG_PARSE_TIMESTAMP_BUFFER_SIZE
Definition log.c:5
#define LOG_LOG_MSG_BUFFER_SIZE
Definition log.c:4
void log_set_json_output(int fd)
Set the JSON output file descriptor.
Definition log.c:775
#define LOGGING_INTERNAL_ERROR(error, message,...)
Definition log.c:233
const char * get_level_string_padded(log_level_t level)
Get padded level string for consistent alignment.
Definition log.c:183
void log_console_impl(log_level_t level, const char *file, int line, const char *func, const char *message)
Definition log.c:2334
void ui_notice_announce(const char *file, int line, const char *func, const char *title, const char *format,...)
Definition log.c:1189
void ui_notice_log(ui_notice_severity_t severity, const char *file, int line, const char *func, const char *title, const char *format,...)
Definition log.c:1181
#define LOG_FUNC_NAME_BUFFER_SIZE
Definition log.c:8
void log_mmap_rotate(void)
Rotate the mmap log (tail-keeping rotation).
Definition mmap.c:426
bool log_mmap_is_active(void)
Check if mmap logging is active.
Definition mmap.c:393
void log_mmap_write(int level, const char *file, int line, const char *func, const char *fmt,...)
Write a log entry directly to the mmap'd file (lock-free).
Definition mmap.c:315
bool log_mmap_get_usage(size_t *used, size_t *capacity)
Get current mmap log usage.
Definition mmap.c:412
char file_path[4096]
Definition mmap.c:42
void log_mmap_destroy(void)
Shutdown mmap logging.
Definition mmap.c:263
asciichat_error_t log_mmap_init_simple(const char *log_path, size_t max_size)
Initialize mmap logging with simple parameters.
Definition mmap.c:255
asciichat_error_t ui_notice_present(ui_notice_severity_t severity, const char *text)
Definition notice.c:371
ui_notice_severity_t
Definition notice.h:9
@ UI_NOTICE_WARNING
Definition notice.h:11
@ UI_NOTICE_INFO
Definition notice.h:10
@ UI_NOTICE_DANGER
Definition notice.h:12
@ UI_NOTICE_FATAL
Definition notice.h:13
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
void ascii_otel_log(int level, const char *file, int line, const char *function, const char *format, va_list args)
Definition otel.c:700
📂 Path Manipulation Utilities
void log_search_destroy(void)
Clean up interactive grep subsystem.
Definition search.c:203
Interactive search filtering for terminal screens.
asciichat_error_t acip_send_remote_log(acip_transport_t *transport, uint8_t log_level, uint8_t direction, const char *message)
Send remote log packet.
Definition send.c:459
ACIP shared/bidirectional packet sending functions.
session_log_buffer_t * session_log_buffer_create(void)
Create a new session log buffer instance.
void session_log_buffer_append(session_log_buffer_t *buf, const char *message)
Append a log message to the buffer.
void session_log_buffer_destroy(session_log_buffer_t *buf)
Cleanup session log buffer instance.
Thread-safe circular log buffer for session screens (status, splash).
🔤 String Manipulation and Shell Escaping Utilities
When code needs to work with asciichat errno snapshot, it keeps total, copied here instead of scatter...
Color scheme definition.
Definition colorscheme.h:76
Compiled log format ready for use in log_template_apply().
Definition log/format.h:72
bool quiet
Quiet mode (suppress logs).
Definition options.h:1147
⏱️ High-precision timing utilities using sokol_time.h and uthash
bool ui_controller_is_presenting(void)
bool ui_controller_is_owner(void)
asciichat_error_t ui_controller_write(int fd, const char *data, size_t len)
#define LOG_DEV
Definition types.h:38
#define LOG_DEBUG
Definition types.h:39
#define LOG_FATAL
Definition types.h:43
#define LOG_ERROR
Definition types.h:42
#define LOG_WARN
Definition types.h:41
#define LOG_INFO
Definition types.h:40
🔤 UTF-8 Encoding and Decoding Utilities