ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
log/logging.c
Go to the documentation of this file.
1
7#include <ascii-chat/common.h>
8#include <ascii-chat/options/options.h>
9#include <ascii-chat/options/rcu.h> // For RCU-based options access
10#include <ascii-chat/platform/abstraction.h>
11#include <ascii-chat/platform/system.h>
12#include <ascii-chat/util/path.h>
13#include <ascii-chat/util/string.h>
14#include <ascii-chat/util/time.h>
15#include <ascii-chat/util/utf8.h>
16#include <stdarg.h>
17#include <stdio.h>
18#include <stdlib.h>
19#include <string.h>
20#include <time.h>
21#include <fcntl.h>
22#include <sys/stat.h>
23#include <ctype.h>
24
25#include <ascii-chat/log/logging.h>
26#include <ascii-chat/log/format.h>
27#include <ascii-chat/log/colorize.h>
28#include <ascii-chat/log/mmap.h>
29#include <ascii-chat/log/grep.h>
30#include <ascii-chat/log/json.h>
31#include <ascii-chat/platform/terminal.h>
32#include <ascii-chat/options/colorscheme.h>
33#include <ascii-chat/platform/thread.h>
34#include <ascii-chat/platform/mutex.h>
35#include <ascii-chat/network/packet.h>
36#include <ascii-chat/video/ansi.h>
37
38/* Platform-specific log hook (weak, can be overridden by platform implementations) */
39__attribute__((weak)) void platform_log_hook(log_level_t level, const char *message) {
40 (void)level;
41 (void)message;
42 // Default: no-op
43}
44
45/* ============================================================================
46 * Logging System Internal State
47 * ============================================================================ */
48
49/* Log context struct - Lock-free logging with mutex-protected rotation
50 *
51 * Design: Logging itself is lock-free using atomic operations and atomic write() syscalls.
52 * Only log rotation uses a mutex, since it involves multiple file operations that must
53 * be atomic as a group (close, read tail, write temp, rename, reopen).
54 */
55#pragma GCC diagnostic push
56#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
57static struct log_context_t {
58 _Atomic int file; /* File descriptor (atomic for safe access) */
59 _Atomic int json_file; /* JSON output file descriptor (-1 = disabled) */
60 _Atomic int level; /* Log level as int for atomic ops */
61 _Atomic bool initialized; /* Initialization flag */
62 char filename[LOG_MSG_BUFFER_SIZE]; /* Store filename (set once at init) */
63 _Atomic size_t current_size; /* Track current file size */
64 _Atomic bool terminal_output_enabled; /* Control stderr output to terminal */
65 _Atomic bool level_manually_set; /* Track if level was set manually */
66 _Atomic bool force_stderr; /* Force all terminal logs to stderr (client mode) */
67 _Atomic bool terminal_locked; /* True when a thread has exclusive terminal access */
68 _Atomic uint64_t terminal_owner_thread; /* Thread that owns terminal output (stored as uint64) */
69 _Atomic unsigned int flush_delay_ms; /* Delay between each buffered log flush (0 = disabled) */
70 mutex_t rotation_mutex; /* Mutex for log rotation only (not for logging!) */
71 _Atomic bool rotation_mutex_initialized; /* Track if rotation mutex is ready */
72 log_template_t *format; /* Compiled log format (NULL = use default) */
73 log_template_t *format_console_only; /* Console-only format variant */
74 _Atomic bool has_custom_format; /* True if format was customized */
75} g_log = {
76 .file = 2, /* STDERR_FILENO - fd 0 is STDIN (read-only!) */
77 .json_file = -1,
78 .level = DEFAULT_LOG_LEVEL,
79 .initialized = false,
80 .filename = {0},
81 .current_size = 0,
82 .terminal_output_enabled = true,
83 .level_manually_set = false,
84 .force_stderr = false,
85 .terminal_locked = false,
86 .terminal_owner_thread = 0,
87 .flush_delay_ms = 0,
88 .rotation_mutex_initialized = false,
89 .format = NULL,
90 .format_console_only = NULL,
91 .has_custom_format = false,
92};
93#pragma GCC diagnostic pop
94
95/* Level strings are defined in format.c (get_level_string_padded / get_level_string) */
96
97/* ============================================================================
98 * Level Strings
99 * ============================================================================
100 * The actual default log format is defined in lib/log/format.c
101 * and accessed via OPT_LOG_TEMPLATE_DEFAULT macro to support different modes.
102 */
103
114const char *get_level_string_padded(log_level_t level) {
115 const char *result;
116 switch (level) {
117 case LOG_INFO:
118 result = "INFO "; // 5 chars: INFO + 1 space
119 break;
120 case LOG_WARN:
121 result = "WARN "; // 5 chars: WARN + 1 space
122 break;
123 case LOG_DEV:
124 result = "DEV "; // 5 chars: DEV + 2 spaces
125 break;
126 case LOG_DEBUG:
127 result = "DEBUG"; // 5 chars: DEBUG (no padding needed)
128 break;
129 case LOG_ERROR:
130 result = "ERROR"; // 5 chars: ERROR (no padding needed)
131 break;
132 case LOG_FATAL:
133 result = "FATAL"; // 5 chars: FATAL (no padding needed)
134 break;
135 default:
136 result = "?????"; // Invalid level - return 5 question marks
137 }
138 // Verify length
139 if (strlen(result) != 5) {
140 fprintf(stderr, "ERROR: get_level_string_padded() returned non-5-char string: '%s' (len=%zu)\n", result,
141 strlen(result));
142 }
143 return result;
144}
145
146#define LOG_COLOR_COUNT 8 /* DEV, DEBUG, WARN, INFO, ERROR, FATAL, GREY, RESET */
147
148/* NOTE: Color arrays are now generated by the color scheme system in lib/ui/colors.c
149 * and stored in g_compiled_colors. The log_get_color_array() function returns pointers
150 * to g_compiled_colors.codes_16/256/truecolor based on terminal capabilities. */
151
152/* Internal error macro - uses g_log directly, only used in this file */
153#ifdef NDEBUG
154#define LOGGING_INTERNAL_ERROR(error, message, ...) \
155 do { \
156 asciichat_set_errno_with_message(error, NULL, 0, NULL, message, ##__VA_ARGS__); \
157 static const char *msg_header = "CRITICAL LOGGING SYSTEM ERROR: "; \
158 safe_fprintf(stderr, "%s %s\n", colored_string(LOG_COLOR_ERROR, msg_header), message); \
159 platform_write(g_log.file, msg_header, strlen(msg_header)); \
160 platform_write(g_log.file, message, strlen(message)); \
161 platform_write(g_log.file, "\n", 1); \
162 platform_print_backtrace(0); \
163 } while (0)
164#else
165#define LOGGING_INTERNAL_ERROR(error, message, ...) \
166 do { \
167 asciichat_set_errno_with_message(error, __FILE__, __LINE__, __func__, message, ##__VA_ARGS__); \
168 static const char *msg_header = "CRITICAL LOGGING SYSTEM ERROR: "; \
169 safe_fprintf(stderr, "%s %s\n", colored_string(LOG_COLOR_ERROR, msg_header), message); \
170 platform_write(g_log.file, msg_header, strlen(msg_header)); \
171 platform_write(g_log.file, message, strlen(message)); \
172 platform_write(g_log.file, "\n", 1); \
173 platform_print_backtrace(0); \
174 } while (0)
175#endif
176
177/* Terminal capabilities cache */
178static terminal_capabilities_t g_terminal_caps = {0};
179static bool g_terminal_caps_initialized = false;
180static bool g_terminal_caps_detecting = false; /* Guard against recursion */
181
182/* Color scheme management - logging-specific state */
183static compiled_color_scheme_t g_compiled_colors = {0};
184static bool g_log_colorscheme_initialized = false;
185
186/* Note: g_colorscheme_mutex is defined in lib/ui/colors.c and declared in lib/ui/colors.h */
187
188/* Shutdown logging state */
189static bool g_shutdown_saved_terminal_output = true; /* Saved state for log_shutdown_begin/end */
190static bool g_shutdown_in_progress = false; /* Track if shutdown phase is active */
191
192size_t get_current_time_formatted(char *time_buf) {
193 /* Get wall-clock time in nanoseconds */
194 uint64_t ts_ns = time_get_realtime_ns();
195 // Extract seconds and nanoseconds from total nanoseconds
196 time_t seconds = (time_t)(ts_ns / NS_PER_SEC_INT);
197 long nanoseconds = (long)(ts_ns % NS_PER_SEC_INT);
198 struct tm tm_info;
199 platform_localtime(&seconds, &tm_info);
200 // Format the time part first
201 // strftime returns 0 on error, not negative (and len is size_t/unsigned)
202 size_t len = strftime(time_buf, 32, "%H:%M:%S", &tm_info);
203 if (len == 0 || len >= 32) {
204 LOGGING_INTERNAL_ERROR(ERROR_INVALID_STATE, "Failed to format time");
205 return 0;
206 }
207
208 // Add microseconds manually (convert nanoseconds to microseconds for display)
209 long microseconds = nanoseconds / 1000;
210 if (microseconds < 0)
211 microseconds = 0;
212 if (microseconds > 999999)
213 microseconds = 999999;
214
215 int result = safe_snprintf(time_buf + len, 32 - len, ".%06ld", microseconds);
216 if (result < 0 || result >= (int)(32 - len)) {
217 LOGGING_INTERNAL_ERROR(ERROR_INVALID_STATE, "Failed to format microseconds");
218 return 0;
219 }
220
221 return len + (size_t)result;
222}
223
224char *format_message(const char *format, va_list args) {
225 if (!format) {
226 return NULL;
227 }
228
229 // First, determine the size needed
230 va_list args_copy;
231 va_copy(args_copy, args);
232 int size = safe_vsnprintf(NULL, 0, format, args_copy);
233 va_end(args_copy);
234
235 if (size < 0) {
236 LOGGING_INTERNAL_ERROR(ERROR_INVALID_STATE, "Failed to format context message");
237 return NULL;
238 }
239
240 // Allocate and format the message
241 char *message = SAFE_MALLOC(size + 1, char *);
242 int result = safe_vsnprintf(message, (size_t)size + 1, format, args);
243 if (result < 0) {
244 SAFE_FREE(message);
245 LOGGING_INTERNAL_ERROR(ERROR_INVALID_STATE, "Failed to format context message");
246 return NULL;
247 }
248
249 return message;
250}
251
262static int truncate_at_whole_line(char *buffer, int current_len, size_t max_len) {
263 if (current_len < 0 || (size_t)current_len <= max_len) {
264 return current_len; // No truncation needed
265 }
266
267 // Search backwards from max_len position for last newline
268 int truncate_pos = (int)max_len - 1;
269 while (truncate_pos > 0 && buffer[truncate_pos] != '\n') {
270 truncate_pos--;
271 }
272
273 // If we found a newline, truncate after it
274 if (truncate_pos > 0 && buffer[truncate_pos] == '\n') {
275 truncate_pos++; // Include the newline
276 buffer[truncate_pos] = '\0';
277 return truncate_pos;
278 }
279
280 // No newline found, truncate at max_len to be safe
281 truncate_pos = (int)max_len - 1;
282 buffer[truncate_pos] = '\0';
283 return truncate_pos;
284}
285
293static void validate_log_message_utf8(const char *message, const char *source) {
294 if (!message || !source) {
295 return;
296 }
297
298 if (!utf8_is_valid(message)) {
299 // Use fprintf instead of log_warn to avoid infinite recursion
300 // (this function is called from log_msg, which would create a loop)
301 safe_fprintf(stderr, "[WARN] Invalid UTF-8 detected in %s\n", source);
302 safe_fprintf(stderr, "[DEBUG] Invalid UTF-8 data: %s\n", message);
303 }
304}
305
306/* ============================================================================
307 * Logging Implementation
308 * ============================================================================
309 */
310
311/* Parse LOG_LEVEL environment variable */
312static log_level_t parse_log_level_from_env(void) {
313 const char *env_level = SAFE_GETENV("LOG_LEVEL");
314 if (!env_level) {
315 return DEFAULT_LOG_LEVEL; // Default level based on build type
316 }
317
318 // Case-insensitive comparison
319 if (platform_strcasecmp(env_level, "DEV") == 0 || strcmp(env_level, "0") == 0) {
320 return LOG_DEV;
321 }
322 if (platform_strcasecmp(env_level, "DEBUG") == 0 || strcmp(env_level, "1") == 0) {
323 return LOG_DEBUG;
324 }
325 if (platform_strcasecmp(env_level, "INFO") == 0 || strcmp(env_level, "2") == 0) {
326 return LOG_INFO;
327 }
328 if (platform_strcasecmp(env_level, "WARN") == 0 || strcmp(env_level, "3") == 0) {
329 return LOG_WARN;
330 }
331 if (platform_strcasecmp(env_level, "ERROR") == 0 || strcmp(env_level, "4") == 0) {
332 return LOG_ERROR;
333 }
334 if (platform_strcasecmp(env_level, "FATAL") == 0 || strcmp(env_level, "5") == 0) {
335 return LOG_FATAL;
336 }
337
338 // Invalid value - return default
339 log_warn("Invalid LOG_LEVEL: %s", env_level);
340 return DEFAULT_LOG_LEVEL;
341}
342
343/* Log rotation function - keeps the tail (recent entries)
344 * REQUIRES: rotation_mutex must be held by caller
345 * This is the only operation that uses a mutex - regular logging is lock-free.
346 */
347static void rotate_log_locked(void) {
348 int file = atomic_load(&g_log.file);
349 size_t current_size = atomic_load(&g_log.current_size);
350
351 if (file < 0 || file == STDERR_FILENO || strlen(g_log.filename) == 0) {
352 return;
353 }
354
355 if (current_size < MAX_LOG_SIZE) {
356 return;
357 }
358
359 platform_close(file);
360 atomic_store(&g_log.file, -1);
361
362 /* Open file for reading to get the tail */
363 int read_file = platform_open(g_log.filename, O_RDONLY, 0);
364 if (read_file < 0) {
365 safe_fprintf(stderr, "Failed to open log file for tail rotation: %s\n", g_log.filename);
366 /* Fall back to regular truncation */
367 int fd = platform_open(g_log.filename, O_CREAT | O_RDWR | O_TRUNC, FILE_PERM_PRIVATE);
368 atomic_store(&g_log.file, fd);
369 atomic_store(&g_log.current_size, 0);
370 return;
371 }
372
373 /* Seek to position where we want to start keeping data (keep last 2MB) */
374 size_t keep_size = MAX_LOG_SIZE * 2 / 3; /* Keep last 2MB of 3MB file */
375 if (current_size < keep_size) {
376 platform_close(read_file);
377 /* Fall back to truncation since we don't have enough data to rotate */
378 int fd = platform_open(g_log.filename, O_CREAT | O_RDWR | O_TRUNC, FILE_PERM_PRIVATE);
379 atomic_store(&g_log.file, fd);
380 atomic_store(&g_log.current_size, 0);
381 return;
382 }
383 if (lseek(read_file, (off_t)(current_size - keep_size), SEEK_SET) == (off_t)-1) {
384 platform_close(read_file);
385 /* Fall back to truncation */
386 int fd = platform_open(g_log.filename, O_CREAT | O_RDWR | O_TRUNC, FILE_PERM_PRIVATE);
387 atomic_store(&g_log.file, fd);
388 atomic_store(&g_log.current_size, 0);
389 return;
390 }
391
392 /* Skip to next line boundary to avoid partial lines */
393 char c;
394 while (platform_read(read_file, &c, 1) > 0 && c != '\n') {
395 /* Skip characters until newline */
396 }
397
398 /* Read the tail into a temporary file */
399 char temp_filename[PLATFORM_MAX_PATH_LENGTH];
400 int result = safe_snprintf(temp_filename, sizeof(temp_filename), "%s.tmp", g_log.filename);
401 if (result <= 0 || result >= (int)sizeof(temp_filename)) {
402 LOGGING_INTERNAL_ERROR(ERROR_INVALID_STATE, "Failed to format temp filename");
403 platform_close(read_file);
404 int fd = platform_open(g_log.filename, O_CREAT | O_RDWR | O_APPEND, FILE_PERM_PRIVATE);
405 atomic_store(&g_log.file, fd);
406 return;
407 }
408
409 int temp_file = platform_open(temp_filename, O_CREAT | O_WRONLY | O_TRUNC, FILE_PERM_PRIVATE);
410 if (temp_file < 0) {
411 platform_close(read_file);
412 /* Fall back to truncation */
413 int fd = platform_open(g_log.filename, O_CREAT | O_RDWR | O_TRUNC, FILE_PERM_PRIVATE);
414 atomic_store(&g_log.file, fd);
415 atomic_store(&g_log.current_size, 0);
416 return;
417 }
418
419 /* Copy tail to temp file */
420 char buffer[8192];
421 ssize_t bytes_read;
422 size_t new_size = 0;
423 while ((bytes_read = platform_read(read_file, buffer, sizeof(buffer))) > 0) {
424 ssize_t written = platform_write(temp_file, buffer, (size_t)bytes_read);
425 if (written != bytes_read) {
426 platform_close(read_file);
427 platform_close(temp_file);
428 unlink(temp_filename);
429 /* Fall back to truncation */
430 int fd = platform_open(g_log.filename, O_CREAT | O_RDWR | O_TRUNC, FILE_PERM_PRIVATE);
431 atomic_store(&g_log.file, fd);
432 atomic_store(&g_log.current_size, 0);
433 return;
434 }
435 new_size += (size_t)bytes_read;
436 }
437
438 platform_close(read_file);
439 platform_close(temp_file);
440
441 /* Replace original with temp file */
442 if (rename(temp_filename, g_log.filename) != 0) {
443 unlink(temp_filename); /* Clean up temp file */
444 /* Fall back to truncation */
445 int fd = platform_open(g_log.filename, O_CREAT | O_RDWR | O_TRUNC, FILE_PERM_PRIVATE);
446 atomic_store(&g_log.file, fd);
447 atomic_store(&g_log.current_size, 0);
448 return;
449 }
450
451 /* Reopen for appending */
452 int new_fd = platform_open(g_log.filename, O_CREAT | O_RDWR | O_APPEND, FILE_PERM_PRIVATE);
453 if (new_fd < 0) {
454 LOGGING_INTERNAL_ERROR(ERROR_INVALID_STATE, "Failed to reopen rotated log file: %s", g_log.filename);
455 atomic_store(&g_log.file, STDERR_FILENO);
456 g_log.filename[0] = '\0';
457 atomic_store(&g_log.current_size, 0);
458 return;
459 }
460 atomic_store(&g_log.file, new_fd);
461 atomic_store(&g_log.current_size, new_size);
462
463 char time_buf[LOG_TIMESTAMP_BUFFER_SIZE];
465
466 char log_msg[256];
467 int log_msg_len =
468 safe_snprintf(log_msg, sizeof(log_msg), "[%s] [INFO] Log tail-rotated (kept %zu bytes)\n", time_buf, new_size);
469 if (log_msg_len > 0 && log_msg_len < (int)sizeof(log_msg)) {
470 (void)platform_write(new_fd, log_msg, (size_t)log_msg_len);
471 }
472}
473
474/* Check if rotation is needed and perform it (acquires mutex only if needed) */
475static void maybe_rotate_log(void) {
476 /* Rotation mutex not initialized - skip rotation */
477 if (!atomic_load(&g_log.rotation_mutex_initialized)) {
478 return;
479 }
480
481 /* Check mmap path first */
482 if (log_mmap_is_active()) {
483 size_t used = 0, capacity = 0;
484 if (log_mmap_get_usage(&used, &capacity) && capacity > 0) {
485 /* Rotate when 90% full to leave room for writes */
486 if (used > capacity * 9 / 10) {
487 mutex_lock(&g_log.rotation_mutex);
489 mutex_unlock(&g_log.rotation_mutex);
490 }
491 }
492 return;
493 }
494
495 /* File path: quick atomic check - avoid mutex if not needed */
496 size_t current_size = atomic_load(&g_log.current_size);
497 if (current_size < MAX_LOG_SIZE) {
498 return;
499 }
500
501 /* Size exceeded - acquire mutex and rotate */
502 mutex_lock(&g_log.rotation_mutex);
503 rotate_log_locked();
504 mutex_unlock(&g_log.rotation_mutex);
505}
506
507void log_init(const char *filename, log_level_t level, bool force_stderr, bool use_mmap) {
508 // Initialize rotation mutex (only operation that uses a mutex)
509 if (!atomic_load(&g_log.rotation_mutex_initialized)) {
510 mutex_init(&g_log.rotation_mutex);
511 atomic_store(&g_log.rotation_mutex_initialized, true);
512 }
513
514 // Set basic config using atomic stores
515 atomic_store(&g_log.force_stderr, force_stderr);
516 bool preserve_terminal_output = atomic_load(&g_log.terminal_output_enabled);
517
518 // Close any existing file (atomic load/store)
519 int old_file = atomic_load(&g_log.file);
520 if (atomic_load(&g_log.initialized) && old_file >= 0 && old_file != STDERR_FILENO) {
521 platform_close(old_file);
522 atomic_store(&g_log.file, -1);
523 }
524
525 // Check LOG_LEVEL environment variable
526 const char *env_level_str = SAFE_GETENV("LOG_LEVEL");
527 if (env_level_str) {
528 atomic_store(&g_log.level, (int)parse_log_level_from_env());
529 } else {
530 atomic_store(&g_log.level, (int)level);
531 }
532
533 atomic_store(&g_log.level_manually_set, false);
534 atomic_store(&g_log.current_size, 0);
535
536 if (filename) {
537 SAFE_STRNCPY(g_log.filename, filename, sizeof(g_log.filename) - 1);
538
539 if (use_mmap) {
540 // Lock-free mmap path - writes go to mmap'd file
541 asciichat_error_t mmap_result = log_mmap_init_simple(filename, 0);
542 if (mmap_result == ASCIICHAT_OK) {
543 atomic_store(&g_log.file, -1); // No regular fd - using mmap for file output
544 } else {
545 // Mmap failed - use stderr only (atomic writes, lock-free)
546 if (preserve_terminal_output) {
547 safe_fprintf(stderr, "Mmap logging failed for %s, using stderr only (lock-free)\n", filename);
548 }
549 atomic_store(&g_log.file, STDERR_FILENO);
550 g_log.filename[0] = '\0';
551 }
552 } else {
553 // Lock-free file I/O path - uses atomic write() syscalls
554 int fd = platform_open(filename, O_CREAT | O_RDWR | O_TRUNC, FILE_PERM_PRIVATE);
555 atomic_store(&g_log.file, (fd >= 0) ? fd : STDERR_FILENO);
556 if (fd < 0) {
557 if (preserve_terminal_output) {
558 safe_fprintf(stderr, "Failed to open log file: %s\n", filename);
559 }
560 g_log.filename[0] = '\0';
561 }
562 }
563 } else {
564 atomic_store(&g_log.file, STDERR_FILENO);
565 g_log.filename[0] = '\0';
566 }
567
568 /* Initialize default log format (NULL means use mode-specific default) */
569 log_set_format(NULL, false);
570
571 atomic_store(&g_log.initialized, true);
572 atomic_store(&g_log.terminal_output_enabled, preserve_terminal_output);
573
574 // Reset terminal detection if needed
575 if (g_terminal_caps_initialized && !g_terminal_caps.detection_reliable) {
576 g_terminal_caps_initialized = false;
577 }
578
579 // Detect terminal capabilities
581
582 // NOTE: Color initialization happens separately via log_set_color_scheme()
583 // after options are parsed. Logging works without colors until then.
584 // NOTE: Grep filter initialization happens in main.c after options_init() completes.
585}
586
587void log_destroy(void) {
588 // Destroy mmap logging first (if active)
589 if (log_mmap_is_active()) {
591 }
592
593 // Cleanup grep filter
594 grep_destroy();
595
596 // Cleanup custom format structures
597 if (g_log.format) {
598 log_template_free(g_log.format);
599 g_log.format = NULL;
600 }
601 if (g_log.format_console_only) {
602 log_template_free(g_log.format_console_only);
603 g_log.format_console_only = NULL;
604 }
605 atomic_store(&g_log.has_custom_format, false);
606
607 // Lock-free cleanup using atomic operations
608 int old_file = atomic_load(&g_log.file);
609 if (old_file >= 0 && old_file != STDERR_FILENO) {
610 platform_close(old_file);
611 }
612 atomic_store(&g_log.file, -1);
613 atomic_store(&g_log.initialized, false);
614
615 // Destroy rotation mutex
616 if (atomic_load(&g_log.rotation_mutex_initialized)) {
617 mutex_destroy(&g_log.rotation_mutex);
618 atomic_store(&g_log.rotation_mutex_initialized, false);
619 }
620}
621
622void log_set_level(log_level_t level) {
623 atomic_store(&g_log.level, (int)level);
624 atomic_store(&g_log.level_manually_set, true);
625}
626
627log_level_t log_get_level(void) {
628 return (log_level_t)atomic_load(&g_log.level);
629}
630
632 // Respect --quiet flag: if quiet is set, never enable terminal output
633 // But allow disabling even if options are unavailable (for shutdown cleanup)
634 const options_t *opts = options_get();
635
636 if (enabled && opts && opts->quiet) {
637 return; // Silently ignore attempts to enable terminal output when --quiet is set
638 }
639 atomic_store(&g_log.terminal_output_enabled, enabled);
640}
641
643 return atomic_load(&g_log.terminal_output_enabled);
644}
645
647 atomic_store(&g_log.force_stderr, enabled);
648}
649
651 return atomic_load(&g_log.force_stderr);
652}
653
655 atomic_store(&g_log.json_file, fd);
656}
657
659 /* Close the current file if it's not stderr */
660 int old_file = atomic_load(&g_log.file);
661 if (old_file >= 0 && old_file != STDERR_FILENO) {
662 platform_close(old_file);
663 }
664 /* Redirect file output to stderr */
665 atomic_store(&g_log.file, STDERR_FILENO);
666 g_log.filename[0] = '\0';
667}
668
669asciichat_error_t log_set_format(const char *format_str, bool console_only) {
670 /* Free old format if it exists */
671 if (g_log.format) {
672 log_template_free(g_log.format);
673 g_log.format = NULL;
674 }
675 if (g_log.format_console_only) {
676 log_template_free(g_log.format_console_only);
677 g_log.format_console_only = NULL;
678 }
679
680 /* Use default format if NULL or empty string */
681 const char *format_to_use = (format_str && format_str[0] != '\0') ? format_str : OPT_LOG_TEMPLATE_DEFAULT;
682 bool is_custom = (format_str && format_str[0] != '\0');
683
684 /* Parse the format string (always parse, never skip) */
685 log_template_t *parsed_format = log_template_parse(format_to_use, false);
686 if (!parsed_format) {
687 log_error("Failed to parse log format: %s", format_to_use);
688 return SET_ERRNO(ERROR_INVALID_STATE, "Invalid log format string");
689 }
690
691 /* If console_only is true, we also need the default format for file output */
692 if (console_only && is_custom) {
693 log_template_t *default_format = log_template_parse(OPT_LOG_TEMPLATE_DEFAULT, false);
694 if (!default_format) {
695 log_template_free(parsed_format);
696 log_error("Failed to parse default log format");
697 return SET_ERRNO(ERROR_INVALID_STATE, "Failed to parse default format");
698 }
699 g_log.format = default_format;
700 g_log.format_console_only = parsed_format;
701 } else {
702 g_log.format = parsed_format;
703 g_log.format_console_only = NULL;
704 }
705
706 atomic_store(&g_log.has_custom_format, is_custom);
707 return ASCIICHAT_OK;
708}
709
711 bool previous_state = atomic_exchange(&g_log.terminal_locked, true);
712 atomic_store(&g_log.terminal_owner_thread, (uint64_t)asciichat_thread_self());
713 return previous_state;
714}
715
716void log_unlock_terminal(bool previous_state) {
717 atomic_store(&g_log.terminal_locked, previous_state);
718 if (!previous_state) {
719 atomic_store(&g_log.terminal_owner_thread, 0);
720 }
721}
722
723void log_set_flush_delay(unsigned int delay_ms) {
724 atomic_store(&g_log.flush_delay_ms, delay_ms);
725}
726
728 // Log rotation is inherently racy without locks - best-effort only
729 // For reliable rotation, use mmap mode or external logrotate
730 int file = atomic_load(&g_log.file);
731 if (file >= 0 && file != STDERR_FILENO && strlen(g_log.filename) > 0) {
732 struct stat st;
733 if (fstat(file, &st) == 0 && st.st_size > MAX_LOG_SIZE) {
734 atomic_store(&g_log.current_size, (size_t)st.st_size);
735 }
736 }
737}
738
739/* Helper: Write formatted log entry to file using atomic write() syscall
740 * POSIX guarantees write() is atomic for sizes <= PIPE_BUF (typically 4096 bytes)
741 * Strips ANSI escape codes from the buffer before writing to ensure clean log files.
742 */
743static void write_to_log_file_atomic(const char *buffer, int length) {
744 if (length <= 0 || buffer == NULL) {
745 return;
746 }
747
748 if (length > MAX_LOG_SIZE) {
749 return;
750 }
751
752 int file = atomic_load(&g_log.file);
753 if (file < 0 || file == STDERR_FILENO) {
754 return;
755 }
756
757 // Strip ANSI escape codes from the buffer before writing to file
758 char *stripped = ansi_strip_escapes(buffer, (size_t)length);
759 const char *write_buf = stripped ? stripped : buffer;
760 size_t write_len = stripped ? strlen(stripped) : (size_t)length;
761
762 // Single atomic write() call - no locking needed
763 ssize_t written = platform_write(file, write_buf, write_len);
764 if (written > 0) {
765 atomic_fetch_add(&g_log.current_size, (size_t)written);
766 maybe_rotate_log();
767 }
768
769 if (stripped) {
770 SAFE_FREE(stripped);
771 }
772}
773
774/* Helper: Format log message header using the format system
775 * Returns the number of characters written to the buffer
776 */
777static int format_log_header(char *buffer, size_t buffer_size, log_level_t level, const char *timestamp,
778 const char *file, int line, const char *func, bool use_colors, uint64_t time_nanoseconds) {
779 const log_template_t *format = g_log.format;
780 if (!format) {
781 LOGGING_INTERNAL_ERROR(ERROR_INVALID_STATE, "Log format not initialized");
782 return -1;
783 }
784
785 /* Use log_template_apply() to generate the complete formatted output */
786 int written = log_template_apply(format, buffer, buffer_size, level, timestamp, file, line, func,
787 asciichat_thread_current_id(), "", use_colors, time_nanoseconds);
788 if (written < 0 || written >= (int)buffer_size) {
789 LOGGING_INTERNAL_ERROR(ERROR_INVALID_STATE, "Failed to format log header");
790 return -1;
791 }
792
793 return written;
794}
795
796/* Note: Terminal buffering removed for lock-free design.
797 * When terminal is locked, other threads' output goes to file only.
798 */
799
800/* Helper: Write colored log entry to terminal (lock-free)
801 * Uses atomic loads for state checks, skips output if terminal is locked by another thread
802 */
803static void write_to_terminal_atomic(log_level_t level, const char *timestamp, const char *file, int line,
804 const char *func, const char *fmt, va_list args, uint64_t time_nanoseconds) {
805 // Choose output file descriptor using unified routing logic
806 int fd = terminal_choose_log_fd(level);
807 FILE *output_stream = (fd == STDERR_FILENO) ? stderr : stdout;
808 bool use_colors = terminal_should_color_output(fd);
809
810 // Format message first (needed for status screen capture even if terminal output disabled)
811 char msg_buffer[LOG_MSG_BUFFER_SIZE];
812 va_list args_copy;
813 va_copy(args_copy, args);
814 int msg_len = safe_vsnprintf(msg_buffer, sizeof(msg_buffer), fmt, args_copy);
815 va_end(args_copy);
816
817 // Strip ANSI codes from message for accurate grep matching and status screen capture
818 char *stripped_msg = NULL;
819 const char *clean_msg = msg_buffer;
820
821 if (msg_len > 0 && msg_len < (int)sizeof(msg_buffer)) {
822 stripped_msg = ansi_strip_escapes(msg_buffer, (size_t)msg_len);
823 clean_msg = stripped_msg ? stripped_msg : msg_buffer;
824 }
825
826 // Use format system to generate the complete log line
827 char colored_log_line[LOG_MSG_BUFFER_SIZE + 512];
828 char plain_log_line[LOG_MSG_BUFFER_SIZE + 512];
829
830 /* Apply format without colors first (plain version for grep matching) */
831 int plain_len = log_template_apply(g_log.format, plain_log_line, sizeof(plain_log_line), level, timestamp, file, line,
832 func, asciichat_thread_current_id(), clean_msg, false, time_nanoseconds);
833
834 /* For colored output, apply format with colors enabled */
835 int colored_len = 0;
836 if (use_colors && plain_len > 0) {
837 colored_len = log_template_apply(g_log.format, colored_log_line, sizeof(colored_log_line), level, timestamp, file,
838 line, func, asciichat_thread_current_id(), clean_msg, true, time_nanoseconds);
839 /* If applying with colors failed, fall back to plain text */
840 if (colored_len <= 0) {
841 // Applying with colors failed - use plain text (no colors)
842 colored_len = plain_len;
843 if (plain_len > 0 && plain_len < (int)sizeof(colored_log_line)) {
844 memcpy(colored_log_line, plain_log_line, (size_t)plain_len + 1);
845 }
846 }
847 } else {
848 /* No colors - use plain text */
849 colored_len = plain_len;
850 if (plain_len > 0 && plain_len < (int)sizeof(colored_log_line)) {
851 memcpy(colored_log_line, plain_log_line, (size_t)plain_len + 1);
852 }
853 }
854
855 // Feed log to status screen buffer (use colored version if available, otherwise plain)
856 if (colored_len > 0 && colored_len < (int)sizeof(colored_log_line)) {
857 extern void server_status_log_append(const char *message);
858 server_status_log_append(colored_log_line);
859 } else if (plain_len > 0 && plain_len < (int)sizeof(plain_log_line)) {
860 extern void server_status_log_append(const char *message);
861 server_status_log_append(plain_log_line);
862 }
863
864 // Check if terminal output is enabled (atomic load)
865 bool is_enabled = atomic_load(&g_log.terminal_output_enabled);
866 if (!is_enabled) {
867 // Terminal output disabled - cleanup and return (status screen already captured)
868 if (stripped_msg) {
869 SAFE_FREE(stripped_msg);
870 }
871 return;
872 }
873
874 // Check if terminal is locked by another thread
875 if (atomic_load(&g_log.terminal_locked)) {
876 uint64_t owner = atomic_load(&g_log.terminal_owner_thread);
877 if (owner != (uint64_t)asciichat_thread_self()) {
878 // Terminal locked by another thread - cleanup and return
879 if (stripped_msg) {
880 SAFE_FREE(stripped_msg);
881 }
882 return;
883 }
884 }
885
886 // Handle message formatting errors for terminal output
887 if (msg_len <= 0 || msg_len >= (int)sizeof(msg_buffer)) {
888 // Message formatting failed - skip filtering and try direct output
889 (void)vfprintf(output_stream, fmt, args);
890 safe_fprintf(output_stream, "\n");
891 (void)fflush(output_stream);
892 if (stripped_msg) {
893 SAFE_FREE(stripped_msg);
894 }
895 return;
896 }
897
898 // Handle log formatting errors
899 if (colored_len <= 0 || colored_len >= (int)sizeof(colored_log_line) || plain_len <= 0 ||
900 plain_len >= (int)sizeof(plain_log_line)) {
901 // Formatting failed - try to output something
902 safe_fprintf(output_stream, "%s\n", clean_msg);
903 (void)fflush(output_stream);
904 if (stripped_msg) {
905 SAFE_FREE(stripped_msg);
906 }
907 return;
908 }
909
910 // Apply grep filter (terminal output only - file logs are unfiltered)
911 // Match against PLAIN text (no ANSI codes) so offsets are correct
912 size_t match_start = 0, match_len = 0;
913 if (!grep_should_output(plain_log_line, &match_start, &match_len)) {
914 if (stripped_msg) {
915 SAFE_FREE(stripped_msg);
916 }
917 return; // No match - suppress terminal output
918 }
919
920 // Output with colors
921 if (use_colors) {
922 const char **colors = log_get_color_array();
923
924 // When grep matches, highlight in the colored line
925 if (match_len > 0 && colors != NULL) {
926 // Apply highlighting to the full line, respecting existing ANSI codes
927 const char *highlighted_line = grep_highlight_colored(colored_log_line, plain_log_line, match_start, match_len);
928 safe_fprintf(output_stream, "%s\n", highlighted_line);
929 } else {
930 // No grep match - output colored line as-is
931 safe_fprintf(output_stream, "%s\n", colored_log_line);
932 }
933 } else {
934 // No colors - output plain line
935 safe_fprintf(output_stream, "%s\n", plain_log_line);
936 }
937
938 // Clean up stripped message
939 if (stripped_msg) {
940 SAFE_FREE(stripped_msg);
941 }
942
943 (void)fflush(output_stream);
944}
945
946void log_msg(log_level_t level, const char *file, int line, const char *func, const char *fmt, ...) {
947 // All state access uses atomic operations - fully lock-free
948 if (!atomic_load(&g_log.initialized)) {
949 return;
950 }
951 if (level < (log_level_t)atomic_load(&g_log.level)) {
952 return;
953 }
954 /* =========================================================================
955 * MMAP PATH: When mmap logging is active, writes go to mmap'd file
956 * ========================================================================= */
957 bool mmap_active = log_mmap_is_active();
958 if (mmap_active) {
959 maybe_rotate_log();
960
961 va_list args;
962 va_start(args, fmt);
963 char msg_buffer[LOG_MMAP_MSG_BUFFER_SIZE];
964 int msg_len = safe_vsnprintf(msg_buffer, sizeof(msg_buffer), fmt, args);
965 va_end(args);
966
967 // Truncate at whole line boundaries to avoid UTF-8 issues
968 if (msg_len > 0) {
969 msg_len = truncate_at_whole_line(msg_buffer, msg_len, sizeof(msg_buffer));
970 }
971
972 // Validate UTF-8 in formatted message
973 validate_log_message_utf8(msg_buffer, "mmap log message");
974
975 log_mmap_write(level, file, line, func, "%s", msg_buffer);
976
977 // Terminal output (check with atomic loads)
978 if (atomic_load(&g_log.terminal_output_enabled) && !atomic_load(&g_log.terminal_locked)) {
979 char time_buf[LOG_TIMESTAMP_BUFFER_SIZE];
980 uint64_t time_ns = time_get_realtime_ns();
982
983 // Choose output stream using unified routing logic
984 int fd = terminal_choose_log_fd(level);
985 FILE *output_stream = (fd == STDERR_FILENO) ? stderr : stdout;
986 // Check if colors should be used
987 // Priority 1: If --color was explicitly passed, force colors
988 extern bool g_color_flag_passed;
989 extern bool g_color_flag_value;
990 bool use_colors = true; // Default: enable colors
992 use_colors = false; // --color=false explicitly disables colors
993 }
994 // Priority 2: If --color NOT explicitly passed, enable colors by default
995
996 char header_buffer[512];
997 int header_len = format_log_header(header_buffer, sizeof(header_buffer), level, time_buf, file, line, func,
998 use_colors, time_ns);
999
1000 if (header_len >= 0 && header_len < (int)sizeof(header_buffer)) {
1001 // Platform-specific log hook (e.g., for WASM browser console)
1002 platform_log_hook(level, msg_buffer);
1003
1004 if (use_colors) {
1005 const char *colorized_msg = colorize_log_message(msg_buffer);
1006 const char **colors = log_get_color_array();
1007 if (colors) {
1008 safe_fprintf(output_stream, "%s%s%s%s\n", header_buffer, colors[LOG_COLOR_RESET], colorized_msg,
1009 colors[LOG_COLOR_RESET]);
1010 } else {
1011 safe_fprintf(output_stream, "%s%s\n", header_buffer, colorized_msg);
1012 }
1013 } else {
1014 safe_fprintf(output_stream, "%s%s\n", header_buffer, msg_buffer);
1015 }
1016 (void)fflush(output_stream);
1017 }
1018 }
1019 return;
1020 }
1021 /* =========================================================================
1022 * FILE I/O PATH: Lock-free using atomic write() syscalls
1023 * ========================================================================= */
1024 char time_buf[LOG_TIMESTAMP_BUFFER_SIZE];
1025 uint64_t time_ns = time_get_realtime_ns();
1027 // Format message for file output
1028 char log_buffer[LOG_MSG_BUFFER_SIZE];
1029 va_list args;
1030 va_start(args, fmt);
1031 int header_len = format_log_header(log_buffer, sizeof(log_buffer), level, time_buf, file, line, func, false, time_ns);
1032 if (header_len < 0 || header_len >= (int)sizeof(log_buffer)) {
1033 LOGGING_INTERNAL_ERROR(ERROR_INVALID_STATE, "Failed to format log header");
1034 va_end(args);
1035 return;
1036 }
1037 int msg_len = header_len;
1038 int formatted_len = safe_vsnprintf(log_buffer + header_len, sizeof(log_buffer) - (size_t)header_len, fmt, args);
1039 if (formatted_len < 0) {
1040 LOGGING_INTERNAL_ERROR(ERROR_INVALID_STATE, "Failed to format log message");
1041 va_end(args);
1042 return;
1043 }
1044
1045 msg_len += formatted_len;
1046 // Truncate at whole line boundaries to avoid UTF-8 issues
1047 msg_len = truncate_at_whole_line(log_buffer, msg_len, sizeof(log_buffer));
1048 // Add newline if there's room and message doesn't already end with one
1049 if (msg_len > 0 && msg_len < (int)sizeof(log_buffer) - 1) {
1050 if (log_buffer[msg_len - 1] != '\n') {
1051 log_buffer[msg_len++] = '\n';
1052 log_buffer[msg_len] = '\0';
1053 }
1054 }
1055 va_end(args);
1056 // Validate UTF-8 in formatted message
1057 validate_log_message_utf8(log_buffer, "leveled log message");
1058
1059 // Extract the user message part (without header) for JSON logging
1060 const char *user_message = log_buffer + header_len;
1061
1062 // For JSON output: strip automatic trailing newline (keep explicit ones)
1063 char json_message_buf[LOG_MSG_BUFFER_SIZE];
1064 const char *json_message = user_message;
1065 size_t user_msg_len = strlen(user_message);
1066
1067 // If message ends with newline and it was added automatically (not in original message)
1068 if (user_msg_len > 0 && user_message[user_msg_len - 1] == '\n') {
1069 // The newline was added automatically if the message part didn't end with one
1070 // before we added it on line 1067. We can check this by looking at msg_len - 1
1071 // If msg_len >= header_len + 2, then msg_len - 2 would be the char before the newline
1072 int msg_content_len = msg_len - header_len;
1073 if (msg_content_len > 1 && log_buffer[msg_len - 2] != '\n') {
1074 // Newline was added automatically - strip it for JSON
1075 if (user_msg_len - 1 < sizeof(json_message_buf)) {
1076 memcpy(json_message_buf, user_message, user_msg_len - 1);
1077 json_message_buf[user_msg_len - 1] = '\0';
1078 json_message = json_message_buf;
1079 }
1080 }
1081 }
1082
1083 // Check if JSON format is enabled
1084 int json_fd = atomic_load(&g_log.json_file);
1085 bool json_format_enabled = (json_fd >= 0);
1086
1087 // If JSON format is enabled, output ONLY JSON (skip text output)
1088 if (json_format_enabled) {
1089 // Output JSON to the JSON file descriptor
1090 log_json_write(json_fd, level, time_ns, file, line, func, json_message);
1091 // Also output JSON to console using unified routing logic, respecting quiet flag
1092 if (atomic_load(&g_log.terminal_output_enabled)) {
1093 int console_fd = terminal_choose_log_fd(level);
1094 log_json_write(console_fd, level, time_ns, file, line, func, json_message);
1095 }
1096 } else {
1097 // Text format: output to file and terminal
1098 // Write to file (atomic write syscall)
1099 int file_fd = atomic_load(&g_log.file);
1100 if (file_fd >= 0 && file_fd != STDERR_FILENO) {
1101 write_to_log_file_atomic(log_buffer, msg_len);
1102 }
1103
1104 // Write to terminal (atomic state checks)
1105 va_list args_terminal;
1106 va_start(args_terminal, fmt);
1107 write_to_terminal_atomic(level, time_buf, file, line, func, fmt, args_terminal, time_ns);
1108 va_end(args_terminal);
1109 }
1110}
1111
1112void log_terminal_msg(log_level_t level, const char *file, int line, const char *func, const char *fmt, ...) {
1113 // Lock-free: only uses atomic loads, no mutex
1114 if (!atomic_load(&g_log.initialized)) {
1115 return;
1116 }
1117
1118 if (level < (log_level_t)atomic_load(&g_log.level)) {
1119 return;
1120 }
1121
1122 // Terminal output only - no file/mmap writing
1123 char time_buf[LOG_TIMESTAMP_BUFFER_SIZE];
1124 uint64_t time_ns = time_get_realtime_ns();
1126
1127 va_list args;
1128 va_start(args, fmt);
1129 write_to_terminal_atomic(level, time_buf, file, line, func, fmt, args, time_ns);
1130 va_end(args);
1131}
1132
1133void log_plain_msg(const char *fmt, ...) {
1134 if (!atomic_load(&g_log.initialized)) {
1135 return;
1136 }
1137
1138 if (shutdown_is_requested()) {
1139 return;
1140 }
1141
1142 char log_buffer[LOG_MSG_BUFFER_SIZE];
1143 va_list args;
1144 va_start(args, fmt);
1145 int msg_len = safe_vsnprintf(log_buffer, sizeof(log_buffer), fmt, args);
1146 va_end(args);
1147
1148 if (msg_len <= 0) {
1149 return;
1150 }
1151
1152 // Truncate at whole line boundaries to avoid UTF-8 issues
1153 msg_len = truncate_at_whole_line(log_buffer, msg_len, sizeof(log_buffer));
1154
1155 // Validate UTF-8 in formatted message
1156 validate_log_message_utf8(log_buffer, "plain text log");
1157
1158 // Write to mmap if active
1159 if (log_mmap_is_active()) {
1160 log_mmap_write(LOG_INFO, NULL, 0, NULL, "%s", log_buffer);
1161 } else {
1162 // Write to file with headers (atomic write syscall)
1163 int file_fd = atomic_load(&g_log.file);
1164 if (file_fd >= 0 && file_fd != STDERR_FILENO) {
1165 // Add header with timestamp and log level to file output
1166 char time_buf[LOG_TIMESTAMP_BUFFER_SIZE];
1167 uint64_t time_ns = time_get_realtime_ns();
1169
1170 char header_buffer[512];
1171 int header_len = format_log_header(header_buffer, sizeof(header_buffer), LOG_INFO, time_buf, "lib/log/logging.c",
1172 0, "log_plain_msg", false, time_ns);
1173
1174 if (header_len > 0) {
1175 write_to_log_file_atomic(header_buffer, header_len);
1176 }
1177 write_to_log_file_atomic(log_buffer, msg_len);
1178 write_to_log_file_atomic("\n", 1);
1179 }
1180 }
1181
1182 // Terminal output (atomic state checks)
1183 if (!atomic_load(&g_log.terminal_output_enabled)) {
1184 return;
1185 }
1186 if (atomic_load(&g_log.terminal_locked)) {
1187 uint64_t owner = atomic_load(&g_log.terminal_owner_thread);
1188 if (owner != (uint64_t)asciichat_thread_self()) {
1189 return;
1190 }
1191 }
1192
1193 // Check if JSON format is enabled
1194 int json_fd = atomic_load(&g_log.json_file);
1195 bool json_format_enabled = (json_fd >= 0);
1196
1197 if (json_format_enabled) {
1198 // Output JSON for plain messages too
1199 uint64_t time_ns = time_get_realtime_ns();
1200 int console_fd = terminal_choose_log_fd(LOG_INFO);
1201 log_json_write(console_fd, LOG_INFO, time_ns, __FILE__, __LINE__, "log_plain_msg", log_buffer);
1202 } else {
1203 // Choose output stream using unified routing logic (LOG_INFO level)
1204 int fd = terminal_choose_log_fd(LOG_INFO);
1205 FILE *output_stream = (fd == STDERR_FILENO) ? stderr : stdout;
1206
1207 // Apply colorization for TTY output
1209 const char *colorized_msg = colorize_log_message(log_buffer);
1210 safe_fprintf(output_stream, "%s\n", colorized_msg);
1211 } else {
1212 safe_fprintf(output_stream, "%s\n", log_buffer);
1213 }
1214 (void)fflush(output_stream);
1215 }
1216}
1217
1218// Helper for log_plain_stderr variants (lock-free)
1219static void log_plain_stderr_internal_atomic(const char *fmt, va_list args, bool add_newline) {
1220 char log_buffer[LOG_MSG_BUFFER_SIZE];
1221 int msg_len = safe_vsnprintf(log_buffer, sizeof(log_buffer), fmt, args);
1222
1223 if (msg_len <= 0) {
1224 return;
1225 }
1226
1227 // Truncate at whole line boundaries to avoid UTF-8 issues
1228 msg_len = truncate_at_whole_line(log_buffer, msg_len, sizeof(log_buffer));
1229
1230 // Validate UTF-8 in formatted message
1231 validate_log_message_utf8(log_buffer, "stderr log");
1232
1233 // Write to mmap if active
1234 if (log_mmap_is_active()) {
1235 log_mmap_write(LOG_INFO, NULL, 0, NULL, "%s", log_buffer);
1236 } else {
1237 // Write to file with headers (atomic write syscall)
1238 int file_fd = atomic_load(&g_log.file);
1239 if (file_fd >= 0 && file_fd != STDERR_FILENO) {
1240 // Add header with timestamp and log level to file output
1241 char time_buf[LOG_TIMESTAMP_BUFFER_SIZE];
1242 uint64_t time_ns = time_get_realtime_ns();
1244
1245 char header_buffer[512];
1246 int header_len = format_log_header(header_buffer, sizeof(header_buffer), LOG_INFO, time_buf, "lib/log/logging.c",
1247 0, "log_plain_stderr_msg", false, time_ns);
1248
1249 if (header_len > 0) {
1250 write_to_log_file_atomic(header_buffer, header_len);
1251 }
1252 write_to_log_file_atomic(log_buffer, msg_len);
1253 if (add_newline) {
1254 write_to_log_file_atomic("\n", 1);
1255 }
1256 }
1257 }
1258
1259 // Terminal output (atomic state checks)
1260 if (!atomic_load(&g_log.terminal_output_enabled)) {
1261 return;
1262 }
1263 if (atomic_load(&g_log.terminal_locked)) {
1264 uint64_t owner = atomic_load(&g_log.terminal_owner_thread);
1265 if (owner != (uint64_t)asciichat_thread_self()) {
1266 return;
1267 }
1268 }
1269
1270 // Apply colorization for TTY output
1271 if (terminal_should_color_output(STDERR_FILENO)) {
1272 const char *colorized_msg = colorize_log_message(log_buffer);
1273 if (add_newline) {
1274 safe_fprintf(stderr, "%s\n", colorized_msg);
1275 } else {
1276 safe_fprintf(stderr, "%s", colorized_msg);
1277 }
1278 } else {
1279 if (add_newline) {
1280 safe_fprintf(stderr, "%s\n", log_buffer);
1281 } else {
1282 safe_fprintf(stderr, "%s", log_buffer);
1283 }
1284 }
1285 (void)fflush(stderr);
1286}
1287
1288void log_plain_stderr_msg(const char *fmt, ...) {
1289 if (!atomic_load(&g_log.initialized)) {
1290 return;
1291 }
1292 if (shutdown_is_requested()) {
1293 return;
1294 }
1295
1296 va_list args;
1297 va_start(args, fmt);
1298 log_plain_stderr_internal_atomic(fmt, args, true);
1299 va_end(args);
1300}
1301
1302void log_plain_stderr_nonewline_msg(const char *fmt, ...) {
1303 if (!atomic_load(&g_log.initialized)) {
1304 return;
1305 }
1306 if (shutdown_is_requested()) {
1307 return;
1308 }
1309
1310 va_list args;
1311 va_start(args, fmt);
1312 log_plain_stderr_internal_atomic(fmt, args, false);
1313 va_end(args);
1314}
1315
1316void log_file_msg(const char *fmt, ...) {
1317 if (!atomic_load(&g_log.initialized)) {
1318 return;
1319 }
1320
1321 char log_buffer[LOG_MSG_BUFFER_SIZE];
1322 va_list args;
1323 va_start(args, fmt);
1324 int msg_len = safe_vsnprintf(log_buffer, sizeof(log_buffer), fmt, args);
1325 va_end(args);
1326
1327 if (msg_len <= 0) {
1328 return;
1329 }
1330
1331 // Truncate at whole line boundaries to avoid UTF-8 issues
1332 msg_len = truncate_at_whole_line(log_buffer, msg_len, sizeof(log_buffer));
1333
1334 // Validate UTF-8 in formatted message
1335 validate_log_message_utf8(log_buffer, "file-only log");
1336
1337 // Write to mmap if active, else to file
1338 if (log_mmap_is_active()) {
1339 log_mmap_write(LOG_INFO, NULL, 0, NULL, "%s", log_buffer);
1340 } else {
1341 int file_fd = atomic_load(&g_log.file);
1342 if (file_fd >= 0 && file_fd != STDERR_FILENO) {
1343 write_to_log_file_atomic(log_buffer, msg_len);
1344 write_to_log_file_atomic("\n", 1);
1345 }
1346 }
1347}
1348
1349static const char *log_network_direction_label(remote_log_direction_t direction) {
1350 switch (direction) {
1351 case REMOTE_LOG_DIRECTION_SERVER_TO_CLIENT:
1352 return "server→client";
1353 case REMOTE_LOG_DIRECTION_CLIENT_TO_SERVER:
1354 return "client→server";
1355 default:
1356 return "network";
1357 }
1358}
1359
1360static asciichat_error_t log_network_message_internal(socket_t sockfd, const struct crypto_context_t *crypto_ctx,
1361 log_level_t level, remote_log_direction_t direction,
1362 const char *file, int line, const char *func, const char *fmt,
1363 va_list args) {
1364 if (!fmt) {
1365 return SET_ERRNO(ERROR_INVALID_PARAM, "Format string is NULL");
1366 }
1367
1368 va_list args_copy;
1369 va_copy(args_copy, args);
1370 char *formatted = format_message(fmt, args_copy);
1371 va_end(args_copy);
1372
1373 if (!formatted) {
1374 asciichat_error_t current_error = GET_ERRNO();
1375 if (current_error == ASCIICHAT_OK) {
1376 current_error = SET_ERRNO(ERROR_MEMORY, "Failed to format network log message");
1377 }
1378 return current_error;
1379 }
1380
1381 asciichat_error_t send_result = ASCIICHAT_OK;
1382 if (sockfd == INVALID_SOCKET_VALUE) {
1383 send_result = SET_ERRNO(ERROR_INVALID_PARAM, "Invalid socket descriptor");
1384 log_msg(LOG_WARN, file, line, func, "Skipping remote log message: invalid socket descriptor");
1385 } else {
1386 send_result = packet_send_remote_log(sockfd, (const crypto_context_t *)crypto_ctx, level, direction, 0, formatted);
1387 if (send_result != ASCIICHAT_OK) {
1388 log_msg(LOG_WARN, file, line, func, "Failed to send remote log message: %s", asciichat_error_string(send_result));
1389 }
1390 }
1391
1392 const char *direction_label = log_network_direction_label(direction);
1393 log_msg(level, file, line, func, "[NET %s] %s", direction_label, formatted);
1394
1395 SAFE_FREE(formatted);
1396 return send_result;
1397}
1398
1399asciichat_error_t log_network_message(socket_t sockfd, const struct crypto_context_t *crypto_ctx, log_level_t level,
1400 remote_log_direction_t direction, const char *fmt, ...) {
1401 va_list args;
1402 va_start(args, fmt);
1403 asciichat_error_t result =
1404 log_network_message_internal(sockfd, crypto_ctx, level, direction, NULL, 0, NULL, fmt, args);
1405 va_end(args);
1406 return result;
1407}
1408
1409asciichat_error_t log_net_message(socket_t sockfd, const struct crypto_context_t *crypto_ctx, log_level_t level,
1410 remote_log_direction_t direction, const char *file, int line, const char *func,
1411 const char *fmt, ...) {
1412 va_list args;
1413 va_start(args, fmt);
1414 asciichat_error_t result =
1415 log_network_message_internal(sockfd, crypto_ctx, level, direction, file, line, func, fmt, args);
1416 va_end(args);
1417 return result;
1418}
1419
1420/* ============================================================================
1421 * Color Helper Functions
1422 * ============================================================================ */
1423
1424/* Initialize terminal capabilities if not already done */
1425static void init_terminal_capabilities(void) {
1426 // Guard against recursion - this can be called indirectly via log_get_color_array()
1427 // from detect_terminal_capabilities() which uses logging
1428 if (g_terminal_caps_detecting) {
1429 return;
1430 }
1431
1432 if (!g_terminal_caps_initialized) {
1433 // Set detecting flag to prevent recursion
1434 g_terminal_caps_detecting = true;
1435
1436 // NEVER call detect_terminal_capabilities() from here - it causes infinite recursion
1437 // because detect_terminal_capabilities() uses log_debug() which calls log_get_color_array()
1438 // which calls init_terminal_capabilities() again.
1439 // Always use defaults here - log_redetect_terminal_capabilities() will do the actual detection
1440 // Use safe fallback during logging initialization to avoid recursion
1441 g_terminal_caps.color_level = TERM_COLOR_16;
1442 g_terminal_caps.capabilities = TERM_CAP_COLOR_16;
1443 g_terminal_caps.color_count = 16;
1444 g_terminal_caps.detection_reliable = false;
1445 g_terminal_caps_initialized = true;
1446
1447 // Clear detecting flag
1448 g_terminal_caps_detecting = false;
1449 }
1450}
1451
1452/* Re-detect terminal capabilities after logging is initialized */
1454 // Guard against recursion
1455 if (g_terminal_caps_detecting) {
1456 return;
1457 }
1458
1459 // Detect if not initialized, or if we're using defaults (not reliably detected)
1460 // This ensures we get proper detection after logging is ready, replacing any defaults
1461 // Once we have reliable detection, never re-detect to keep colors consistent
1462 if (!g_terminal_caps_initialized || !g_terminal_caps.detection_reliable) {
1463 g_terminal_caps_detecting = true;
1464 g_terminal_caps = detect_terminal_capabilities();
1465 g_terminal_caps_detecting = false;
1466 g_terminal_caps_initialized = true;
1467
1468 // Now log the capabilities AFTER colors are set, so this log uses the correct colors
1469 log_debug("Terminal capabilities: color_level=%d, capabilities=0x%x, utf8=%s, fps=%d", g_terminal_caps.color_level,
1470 g_terminal_caps.capabilities, g_terminal_caps.utf8_support ? "yes" : "no", g_terminal_caps.desired_fps);
1471
1472 // Now that we've detected once with reliable results, keep these colors consistent for all future logs
1473 }
1474 // Once initialized with reliable detection, never re-detect to keep colors consistent
1475}
1476
1477/* Get the appropriate color array based on terminal capabilities */
1478const char **log_get_color_array(void) {
1479 init_terminal_capabilities();
1480 /* Initialize colors if not already done */
1481 if (!g_log_colorscheme_initialized) {
1483 }
1484
1485 /* Safety check: if colors are not initialized, return NULL to prevent crashes from null pointers */
1486 if (!g_log_colorscheme_initialized) {
1487 return NULL;
1488 }
1489
1490 /* Return the compiled color scheme based on terminal capabilities
1491 * codes_16, codes_256, codes_truecolor are now proper arrays of pointers (const char *[8]),
1492 * so we can safely cast them to const char **. */
1493 if (g_terminal_caps.color_level >= TERM_COLOR_TRUECOLOR) {
1494 return (const char **)g_compiled_colors.codes_truecolor;
1495 } else if (g_terminal_caps.color_level >= TERM_COLOR_256) {
1496 return (const char **)g_compiled_colors.codes_256;
1497 } else {
1498 return (const char **)g_compiled_colors.codes_16;
1499 }
1500}
1501
1502const char *log_level_color(log_color_t color) {
1503 const char **colors = log_get_color_array();
1504 if (colors == NULL) {
1505 return ""; /* Return empty string if colors not available */
1506 }
1507 if (color >= 0 && color <= LOG_COLOR_RESET) {
1508 return colors[color];
1509 }
1510 return colors[LOG_COLOR_RESET]; /* Return reset color if invalid */
1511}
1512
1513/* ============================================================================
1514 * Color Scheme Management
1515 * ============================================================================ */
1516
1518 /* Skip color initialization during terminal detection to avoid mutex deadlock */
1519 if (g_terminal_caps_detecting) {
1520 return;
1521 }
1522
1523 /* Skip color initialization before logging is fully initialized */
1524 if (!atomic_load(&g_log.initialized)) {
1525 return;
1526 }
1527
1528 if (g_log_colorscheme_initialized) {
1529 return;
1530 }
1531
1532 /* Get active color scheme - this ensures color system is initialized */
1533 const color_scheme_t *scheme = colorscheme_get_active_scheme();
1534 if (!scheme) {
1535 /* Don't mark as initialized if we can't get a color scheme - return NULL instead */
1536 return;
1537 }
1538
1539 /* Acquire mutex for compilation (mutex is now initialized by colorscheme_init) */
1540 mutex_lock(&g_colorscheme_mutex);
1541 /* Debug: Check if g_compiled_colors is actually zero-initialized */
1542 /* Zero the structure on first use to avoid freeing garbage pointers */
1543 /* (static = {0} produces garbage in this build for unknown reasons) */
1544 static bool first_compile = true;
1545 if (first_compile) {
1546 memset(&g_compiled_colors, 0, sizeof(g_compiled_colors));
1547 first_compile = false;
1548 }
1549
1550 /* Detect terminal background */
1551 terminal_background_t background = detect_terminal_background();
1552 /* Determine color mode for compilation */
1553 terminal_color_mode_t mode;
1554 if (g_terminal_caps.color_level >= TERM_COLOR_TRUECOLOR) {
1555 mode = TERM_COLOR_TRUECOLOR;
1556 } else if (g_terminal_caps.color_level >= TERM_COLOR_256) {
1557 mode = TERM_COLOR_256;
1558 } else {
1559 mode = TERM_COLOR_16;
1560 }
1561 /* Compile the color scheme to ANSI codes */
1562 asciichat_error_t result = colorscheme_compile_scheme(scheme, mode, background, &g_compiled_colors);
1563 g_log_colorscheme_initialized = true;
1564 mutex_unlock(&g_colorscheme_mutex);
1565
1566 /* Log outside of mutex lock to avoid recursive lock deadlock */
1567 if (result != ASCIICHAT_OK) {
1568 log_debug("Failed to compile color scheme: %d", result);
1569 }
1570}
1571
1572void log_set_color_scheme(const color_scheme_t *scheme) {
1573 if (!scheme) {
1574 return;
1575 }
1576
1577 /* Mutex is managed by colors.c - just use it */
1578 mutex_lock(&g_colorscheme_mutex);
1579
1580 /* Detect terminal background */
1581 terminal_background_t background = detect_terminal_background();
1582
1583 /* Determine color mode for compilation */
1584 terminal_color_mode_t mode;
1585 if (g_terminal_caps.color_level >= TERM_COLOR_TRUECOLOR) {
1586 mode = TERM_COLOR_TRUECOLOR;
1587 } else if (g_terminal_caps.color_level >= TERM_COLOR_256) {
1588 mode = TERM_COLOR_256;
1589 } else {
1590 mode = TERM_COLOR_16;
1591 }
1592
1593 /* Compile the new color scheme */
1594 asciichat_error_t result = colorscheme_compile_scheme(scheme, mode, background, &g_compiled_colors);
1595
1596 g_log_colorscheme_initialized = true;
1597 mutex_unlock(&g_colorscheme_mutex);
1598
1599 /* Log outside of mutex lock to avoid recursive lock deadlock */
1600 if (result != ASCIICHAT_OK) {
1601 log_debug("Failed to compile color scheme: %d", result);
1602 }
1603}
1604
1605/* ============================================================================
1606 * Lock-Free MMAP Logging Integration
1607 * ============================================================================ */
1608
1609asciichat_error_t log_enable_mmap(const char *log_path) {
1610 return log_enable_mmap_sized(log_path, 0); /* Use default size */
1611}
1612
1613asciichat_error_t log_enable_mmap_sized(const char *log_path, size_t max_size) {
1614 if (!log_path) {
1615 return SET_ERRNO(ERROR_INVALID_PARAM, "log_path is required");
1616 }
1617
1618 // Initialize mmap logging - text is written directly to the mmap'd file
1619 asciichat_error_t result = log_mmap_init_simple(log_path, max_size);
1620 if (result != ASCIICHAT_OK) {
1621 return result;
1622 }
1623
1624 log_info("Lock-free mmap logging enabled: %s", log_path);
1625 return ASCIICHAT_OK;
1626}
1627
1629 if (log_mmap_is_active()) {
1631 log_info("Lock-free mmap logging disabled");
1632 }
1633}
1634
1635/* ============================================================================
1636 * Shutdown Logging Control
1637 * ============================================================================ */
1638
1640 if (g_shutdown_in_progress) {
1641 return; /* Already in shutdown phase */
1642 }
1643
1644 /* Save current terminal output state and disable console output */
1645 g_shutdown_saved_terminal_output = atomic_load(&g_log.terminal_output_enabled);
1646 atomic_store(&g_log.terminal_output_enabled, false);
1647 g_shutdown_in_progress = true;
1648}
1649
1651 if (!g_shutdown_in_progress) {
1652 return; /* Not in shutdown phase, skip terminal state restoration */
1653 }
1654
1655 /* Restore previous terminal output state */
1656 atomic_store(&g_log.terminal_output_enabled, g_shutdown_saved_terminal_output);
1657 g_shutdown_in_progress = false;
1658}
1659
1667 colorscheme_cleanup_compiled(&g_compiled_colors);
1668}
1669
1670/* ============================================================================
1671 * Log Recoloring for Plain Text Entries
1672 * ============================================================================ */
1673
1680size_t log_recolor_plain_entry(const char *plain_line, char *colored_buf, size_t buf_size) {
1681 if (!plain_line || !colored_buf || buf_size < 128) {
1682 return 0;
1683 }
1684
1685 static char work_buffer[LOG_MSG_BUFFER_SIZE + 1024];
1686
1687 // Parse format FIRST, regardless of color availability
1688 // Parse format: [TIMESTAMP] [LEVEL] [tid:THREAD_ID] FILE:LINE in FUNC(): MESSAGE
1689 const char *p = plain_line;
1690
1691 // Extract timestamp [TIMESTAMP]
1692 if (*p != '[') {
1693 return 0; // Invalid format
1694 }
1695 p++;
1696 const char *timestamp_start = p;
1697
1698 // Find the closing ] for timestamp - look for pattern that ends with proper timestamp format
1699 // Valid timestamp: HH:MM:SS.UUUUUU (time with microseconds, no date)
1700 while (*p && *p != ']') {
1701 p++;
1702 }
1703 if (*p != ']') {
1704 return 0; // Malformed - no closing bracket
1705 }
1706
1707 size_t timestamp_len = p - timestamp_start;
1708 char timestamp[64];
1709 if (timestamp_len >= sizeof(timestamp) || timestamp_len == 0) {
1710 return 0; // Invalid timestamp - empty or too long
1711 }
1712 SAFE_STRNCPY(timestamp, timestamp_start, timestamp_len);
1713 timestamp[timestamp_len] = '\0';
1714 p++; // Skip ]
1715
1716 // Skip whitespace after timestamp
1717 while (*p && *p == ' ') {
1718 p++;
1719 }
1720
1721 // Extract level [LEVEL]
1722 if (*p != '[') {
1723 return 0; // Missing opening bracket for level
1724 }
1725 p++;
1726 const char *level_start = p;
1727 while (*p && *p != ']') {
1728 p++;
1729 }
1730 if (*p != ']') {
1731 return 0; // Missing closing bracket for level
1732 }
1733 size_t level_len = p - level_start;
1734 char level_str[16];
1735 if (level_len >= sizeof(level_str) || level_len == 0) {
1736 return 0; // Level string too long or empty
1737 }
1738 /* Use strncpy directly for substring extraction (not null-terminated source) */
1739 strncpy(level_str, level_start, level_len);
1740 level_str[level_len] = '\0';
1741
1742 // Determine log level for color selection
1743 log_level_t level = LOG_INFO; // Default
1744 if (strstr(level_str, "DEV") || strstr(level_str, "DEBUG")) {
1745 level = LOG_DEBUG;
1746 } else if (strstr(level_str, "INFO")) {
1747 level = LOG_INFO;
1748 } else if (strstr(level_str, "WARN")) {
1749 level = LOG_WARN;
1750 } else if (strstr(level_str, "ERROR")) {
1751 level = LOG_ERROR;
1752 } else if (strstr(level_str, "FATAL")) {
1753 level = LOG_FATAL;
1754 }
1755
1756 p++; // Skip ]
1757
1758 // Skip whitespace after level
1759 while (*p && *p == ' ') {
1760 p++;
1761 }
1762
1763 // Extract thread ID [tid:THREAD_ID] - optional field
1764 uint64_t tid = 0;
1765 if (*p == '[' && strncmp(p, "[tid:", 5) == 0) {
1766 p += 5;
1767 char *tid_end = NULL;
1768 tid = strtoull(p, &tid_end, 10);
1769 if (!tid_end || *tid_end != ']') {
1770 // tid parsing failed, but continue anyway (might still recover)
1771 // Try to find the closing bracket
1772 while (*p && *p != ']') {
1773 p++;
1774 }
1775 if (*p == ']') {
1776 p++;
1777 }
1778 } else {
1779 p = tid_end + 1; // Skip past the ]
1780 }
1781 // Skip whitespace after tid
1782 while (*p && *p == ' ') {
1783 p++;
1784 }
1785 }
1786 // tid is optional - continue parsing if not present
1787
1788 // Extract file path (everything up to :LINE)
1789 const char *file_start = p;
1790 while (*p && *p != ':') {
1791 p++;
1792 }
1793 if (*p != ':') {
1794 return 0; // Malformed
1795 }
1796 size_t file_len = p - file_start;
1797 char file_path[256];
1798 if (file_len >= sizeof(file_path)) {
1799 return 0;
1800 }
1801 /* Use strncpy directly for substring extraction (not null-terminated source) */
1802 strncpy(file_path, file_start, file_len);
1803 file_path[file_len] = '\0';
1804 p++; // Skip :
1805
1806 // Extract line number (digits only)
1807 int line_num = 0;
1808 const char *line_start = p;
1809 while (*p && isdigit((unsigned char)*p)) {
1810 p++;
1811 }
1812 if (p == line_start) {
1813 return 0; // No line number
1814 }
1815 line_num = (int)strtol(line_start, NULL, 10);
1816
1817 // Skip whitespace and find "in" keyword (be very lenient)
1818 while (*p && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) {
1819 p++;
1820 }
1821
1822 // Try to find "in " - if not found, might still be valid, just harder to parse
1823 if (strncmp(p, "in ", 3) == 0) {
1824 p += 3; // Skip "in "
1825 } else if (*p == 'i' && *(p + 1) == 'n' && (*(p + 2) == ' ' || *(p + 2) == '\t')) {
1826 // Allow tab after "in"
1827 p += 2; // Skip "in"
1828 while (*p && (*p == ' ' || *p == '\t')) {
1829 p++;
1830 }
1831 } else {
1832 // Missing "in" keyword - this is a format error
1833 return 0;
1834 }
1835
1836 // Extract function name (everything up to "()")
1837 const char *func_start = p;
1838 while (*p && *p != '(') {
1839 p++;
1840 }
1841 if (*p != '(' || func_start == p) {
1842 return 0; // Missing function name or parentheses
1843 }
1844 size_t func_len = p - func_start;
1845
1846 // Trim trailing whitespace from function name
1847 while (func_len > 0 && (func_start[func_len - 1] == ' ' || func_start[func_len - 1] == '\t')) {
1848 func_len--;
1849 }
1850
1851 char func_name[256];
1852 if (func_len >= sizeof(func_name)) {
1853 func_len = sizeof(func_name) - 1;
1854 }
1855 if (func_len > 0) {
1856 /* Use strncpy directly for substring extraction (not null-terminated source) */
1857 strncpy(func_name, func_start, func_len);
1858 }
1859 func_name[func_len] = '\0';
1860
1861 // Skip "(" and ")" - be lenient about what's between them
1862 if (*p == '(') {
1863 p++;
1864 while (*p && *p != ')') {
1865 p++;
1866 }
1867 if (*p == ')') {
1868 p++;
1869 }
1870 }
1871
1872 // Skip whitespace and optional colon(s) and other separators
1873 while (*p && (*p == ' ' || *p == ':' || *p == '\t')) {
1874 p++;
1875 }
1876
1877 // Remaining is the message
1878 const char *message = p;
1879
1880 // Format is valid, get colors from logging system
1881 const char **colors = log_get_color_array();
1882 if (!colors) {
1883 // No colors available, return plain text
1884 static bool warned_once = false;
1885 if (!warned_once) {
1886 log_debug("WARNING: log_recolor_plain_entry() called but colors not initialized - returning plain text");
1887 warned_once = true;
1888 }
1889 size_t len = strlen(plain_line);
1890 if (len >= buf_size) {
1891 return 0;
1892 }
1893 SAFE_STRNCPY(colored_buf, plain_line, buf_size - 1);
1894 colored_buf[buf_size - 1] = '\0';
1895 return len;
1896 }
1897
1898 // Build colored output
1899 const char *level_color = colors[level];
1900 const char *reset = colors[LOG_COLOR_RESET];
1901 const char *file_color = colors[1]; // DEBUG/Cyan
1902 const char *line_color = colors[6]; // GREY (matching tid)
1903 const char *func_color = colors[0]; // DEV/Orange
1904 const char *tid_color = colors[6]; // GREY
1905
1906 int len = safe_snprintf(work_buffer, sizeof(work_buffer),
1907 "[%s%s%s] [%s%s%s] [tid:%s%llu%s] %s%s%s:%s%d%s in %s%s%s(): %s", level_color, timestamp,
1908 reset, level_color, level_str, reset, tid_color, (unsigned long long)tid, reset, file_color,
1909 file_path, reset, line_color, line_num, reset, func_color, func_name, reset, message);
1910
1911 if (len <= 0 || len >= (int)sizeof(work_buffer)) {
1912 return 0;
1913 }
1914
1915 // Colorize the message part
1916 const char *colorized_msg = colorize_log_message(message);
1917 len = safe_snprintf(work_buffer, sizeof(work_buffer),
1918 "[%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,
1919 level_color, level_str, reset, tid_color, (unsigned long long)tid, reset, file_color, file_path,
1920 reset, line_color, line_num, reset, func_color, func_name, reset, colorized_msg);
1921
1922 if (len <= 0 || len >= (int)sizeof(work_buffer) || len >= (int)buf_size) {
1923 return 0;
1924 }
1925
1926 SAFE_STRNCPY(colored_buf, work_buffer, buf_size - 1);
1927 colored_buf[buf_size - 1] = '\0';
1928 return (size_t)len;
1929}
1930
1931// Internal implementation - don't use directly, use log_console_impl macro
1932void log_console_impl(log_level_t level, const char *file, int line, const char *func, const char *message) {
1933 if (!message) {
1934 return;
1935 }
1936
1937 int fd = terminal_choose_log_fd(level);
1938 if (fd < 0) {
1939 return;
1940 }
1941
1942 // Check if JSON output is enabled (json_file >= 0 means enabled)
1943 int json_fd = atomic_load(&g_log.json_file);
1944 bool use_json = (json_fd >= 0);
1945
1946 if (use_json) {
1947 // Use async-safe JSON formatter (safe for signal handlers)
1948 extern void log_json_async_safe(int fd, log_level_t level, const char *file, int line, const char *func,
1949 const char *message);
1950 log_json_async_safe(fd, level, file, line, func, message);
1951 } else {
1952 // Text output to console using platform_write_all to handle partial writes
1953 size_t msg_len = strlen(message);
1954 platform_write_all(fd, (const uint8_t *)message, msg_len);
1955 if (msg_len == 0 || message[msg_len - 1] != '\n') {
1956 platform_write_all(fd, (const uint8_t *)"\n", 1);
1957 }
1958 }
1959}
1960
1970void *log_get_template(void) {
1971 return (void *)g_log.format;
1972}
size_t platform_write_all(int fd, const void *buf, size_t count)
Write all data to file descriptor with automatic retry on transient errors.
Definition abstraction.c:39
char * ansi_strip_escapes(const char *input, size_t input_len)
Definition ansi.c:13
const char * colorize_log_message(const char *message)
Colorize a log message for terminal output.
Definition colorize.c:513
const color_scheme_t * colorscheme_get_active_scheme(void)
terminal_background_t detect_terminal_background(void)
asciichat_error_t colorscheme_compile_scheme(const color_scheme_t *scheme, terminal_color_mode_t mode, terminal_background_t background, compiled_color_scheme_t *compiled)
void colorscheme_cleanup_compiled(compiled_color_scheme_t *compiled)
mutex_t g_colorscheme_mutex
Definition colorscheme.c:38
ASCIICHAT_API bool g_color_flag_value
Definition common.c:50
ASCIICHAT_API bool g_color_flag_passed
Definition common.c:49
bool shutdown_is_requested(void)
Definition common.c:65
void platform_log_hook(log_level_t level, const char *message)
Definition console.c:40
void grep_destroy(void)
Definition grep.c:1240
bool grep_should_output(const char *log_line, size_t *match_start, size_t *match_len)
Definition grep.c:648
bool enabled
Is filtering active?
Definition grep.c:78
int buffer_size
Size of circular buffer.
Definition grep.c:84
const char * grep_highlight_colored(const char *colored_text, const char *plain_text, size_t match_start, size_t match_len)
Definition grep.c:806
int socket_t
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)
Definition json.c:93
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 json.c:251
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)
Definition log/format.c:455
void log_template_free(log_template_t *format)
Definition log/format.c:293
log_template_t * log_template_parse(const char *format_str, bool console_only)
Definition log/format.c:289
void log_truncate_if_large(void)
void log_plain_stderr_nonewline_msg(const char *fmt,...)
void log_plain_msg(const char *fmt,...)
bool log_get_terminal_output(void)
asciichat_error_t log_enable_mmap(const char *log_path)
void log_init_colors(void)
void log_msg(log_level_t level, const char *file, int line, const char *func, const char *fmt,...)
void log_shutdown_end(void)
void log_destroy(void)
void log_set_force_stderr(bool enabled)
log_level_t log_get_level(void)
const char ** log_get_color_array(void)
const char * log_level_color(log_color_t color)
void log_set_flush_delay(unsigned int delay_ms)
char * format_message(const char *format, va_list args)
void log_cleanup_colors(void)
Clean up compiled color scheme.
void log_shutdown_begin(void)
void log_disable_file_output(void)
bool log_get_force_stderr(void)
void log_set_level(log_level_t level)
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.
void log_init(const char *filename, log_level_t level, bool force_stderr, bool use_mmap)
asciichat_error_t log_net_message(socket_t sockfd, const struct crypto_context_t *crypto_ctx, log_level_t level, remote_log_direction_t direction, const char *file, int line, const char *func, const char *fmt,...)
bool log_lock_terminal(void)
asciichat_error_t log_network_message(socket_t sockfd, const struct crypto_context_t *crypto_ctx, log_level_t level, remote_log_direction_t direction, const char *fmt,...)
asciichat_error_t log_set_format(const char *format_str, bool console_only)
size_t get_current_time_formatted(char *time_buf)
asciichat_error_t log_enable_mmap_sized(const char *log_path, size_t max_size)
void log_set_json_output(int fd)
void log_file_msg(const char *fmt,...)
void log_disable_mmap(void)
void log_set_terminal_output(bool enabled)
void * log_get_template(void)
Get the current log format template.
void log_set_color_scheme(const color_scheme_t *scheme)
#define LOGGING_INTERNAL_ERROR(error, message,...)
const char * get_level_string_padded(log_level_t level)
Get padded level string for consistent alignment.
void log_plain_stderr_msg(const char *fmt,...)
void log_redetect_terminal_capabilities(void)
void log_unlock_terminal(bool previous_state)
void log_console_impl(log_level_t level, const char *file, int line, const char *func, const char *message)
__attribute__((weak))
Definition log/logging.c:39
void log_terminal_msg(log_level_t level, const char *file, int line, const char *func, const char *fmt,...)
void server_status_log_append(const char *message)
Definition log.c:44
char file_path[PLATFORM_MAX_PATH_LENGTH]
Definition mmap.c:39
void log_mmap_rotate(void)
Definition mmap.c:423
bool log_mmap_is_active(void)
Definition mmap.c:390
void log_mmap_write(int level, const char *file, int line, const char *func, const char *fmt,...)
Definition mmap.c:312
bool log_mmap_get_usage(size_t *used, size_t *capacity)
Definition mmap.c:409
void log_mmap_destroy(void)
Definition mmap.c:260
bool initialized
Definition mmap.c:38
asciichat_error_t log_mmap_init_simple(const char *log_path, size_t max_size)
Definition mmap.c:252
action_args_t args
asciichat_error_t packet_send_remote_log(socket_t sockfd, const crypto_context_t *crypto_ctx, log_level_t level, remote_log_direction_t direction, uint16_t flags, const char *message)
Definition packet.c:893
int terminal_choose_log_fd(log_level_t level)
bool terminal_should_color_output(int fd)
Determine if color output should be used.
int platform_strcasecmp(const char *s1, const char *s2)
terminal_capabilities_t detect_terminal_capabilities(void)
const options_t * options_get(void)
Definition rcu.c:347
int safe_fprintf(FILE *stream, const char *format,...)
Safe formatted output to file stream.
Definition system.c:480
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456
int safe_vsnprintf(char *buffer, size_t buffer_size, const char *format, va_list ap)
Safe formatted string printing with va_list.
Definition system.c:507
#define PLATFORM_MAX_PATH_LENGTH
Definition system.c:64
int mutex_init(mutex_t *mutex)
Definition threading.c:16
asciichat_thread_t asciichat_thread_self(void)
Definition threading.c:54
uint64_t asciichat_thread_current_id(void)
Definition threading.c:84
int mutex_destroy(mutex_t *mutex)
Definition threading.c:21
bool utf8_is_valid(const char *str)
Definition utf8.c:158
uint64_t time_get_realtime_ns(void)
Definition util/time.c:59
asciichat_error_t platform_localtime(const time_t *timer, struct tm *result)
Definition util.c:48
int platform_close(int fd)
int platform_open(const char *pathname, int flags,...)
ssize_t platform_read(int fd, void *buf, size_t count)
ssize_t platform_write(int fd, const void *buf, size_t count)