7#include <ascii-chat/log/format.h>
8#include <ascii-chat/log/colorize.h>
9#include <ascii-chat/util/string.h>
10#include <ascii-chat/util/utf8.h>
11#include <ascii-chat/util/time.h>
12#include <ascii-chat/util/path.h>
13#include <ascii-chat/common.h>
14#include <ascii-chat/log/logging.h>
42static log_template_t *parse_format_string(
const char *format_str,
bool console_only) {
44 SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid format string: %s", format_str);
49 SET_ERRNO(ERROR_INVALID_STATE,
"Invalid UTF-8 in log format string");
53 log_template_t *result = SAFE_CALLOC(1,
sizeof(log_template_t), log_template_t *);
54 result->original = SAFE_MALLOC(strlen(format_str) + 1,
char *);
55 strcpy(result->original, format_str);
56 result->console_only = console_only;
59 result->specs = SAFE_CALLOC(strlen(format_str) + 1,
sizeof(log_format_spec_t), log_format_spec_t *);
61 SAFE_FREE(result->original);
66 const char *p = format_str;
71 if (*p ==
'\\' && *(p + 1)) {
76 result->specs[spec_idx].type = LOG_FORMAT_NEWLINE;
79 }
else if (next ==
'\\') {
81 result->specs[spec_idx].type = LOG_FORMAT_LITERAL;
82 result->specs[spec_idx].literal = SAFE_MALLOC(2,
char *);
83 if (!result->specs[spec_idx].literal) {
86 result->specs[spec_idx].literal[0] =
'\\';
87 result->specs[spec_idx].literal[1] =
'\0';
88 result->specs[spec_idx].literal_len = 1;
93 result->specs[spec_idx].type = LOG_FORMAT_LITERAL;
94 result->specs[spec_idx].literal = SAFE_MALLOC(2,
char *);
95 if (!result->specs[spec_idx].literal) {
98 result->specs[spec_idx].literal[0] =
'\\';
99 result->specs[spec_idx].literal[1] =
'\0';
100 result->specs[spec_idx].literal_len = 1;
104 }
else if (*p ==
'%' && *(p + 1)) {
105 if (*(p + 1) ==
'%') {
107 result->specs[spec_idx].type = LOG_FORMAT_LITERAL;
108 result->specs[spec_idx].literal = SAFE_MALLOC(2,
char *);
109 if (!result->specs[spec_idx].literal) {
112 result->specs[spec_idx].literal[0] =
'%';
113 result->specs[spec_idx].literal[1] =
'\0';
114 result->specs[spec_idx].literal_len = 1;
121 if (strncmp(p,
"time(", 5) == 0) {
124 const char *fmt_start = p;
125 const char *fmt_end = strchr(p,
')');
128 log_error(
"Invalid %%time format: missing closing )");
132 result->specs[spec_idx].type = LOG_FORMAT_TIME;
133 size_t fmt_len = fmt_end - fmt_start;
134 result->specs[spec_idx].literal = SAFE_MALLOC(fmt_len + 1,
char *);
135 if (!result->specs[spec_idx].literal) {
138 memcpy(result->specs[spec_idx].literal, fmt_start, fmt_len);
139 result->specs[spec_idx].literal[fmt_len] =
'\0';
140 result->specs[spec_idx].literal_len = fmt_len;
144 }
else if (strncmp(p,
"level_aligned", 13) == 0) {
145 result->specs[spec_idx].type = LOG_FORMAT_LEVEL_ALIGNED;
148 }
else if (strncmp(p,
"level", 5) == 0) {
149 result->specs[spec_idx].type = LOG_FORMAT_LEVEL;
152 }
else if (strncmp(p,
"file_relative", 13) == 0) {
153 result->specs[spec_idx].type = LOG_FORMAT_FILE_RELATIVE;
156 }
else if (strncmp(p,
"file", 4) == 0) {
157 result->specs[spec_idx].type = LOG_FORMAT_FILE;
160 }
else if (strncmp(p,
"line", 4) == 0) {
161 result->specs[spec_idx].type = LOG_FORMAT_LINE;
164 }
else if (strncmp(p,
"func", 4) == 0) {
165 result->specs[spec_idx].type = LOG_FORMAT_FUNC;
168 }
else if (strncmp(p,
"tid", 3) == 0) {
169 result->specs[spec_idx].type = LOG_FORMAT_TID;
172 }
else if (strncmp(p,
"colored_message", 15) == 0) {
173 result->specs[spec_idx].type = LOG_FORMAT_COLORED_MESSAGE;
176 }
else if (strncmp(p,
"ms", 2) == 0) {
177 result->specs[spec_idx].type = LOG_FORMAT_MICROSECONDS;
180 }
else if (strncmp(p,
"ns", 2) == 0) {
181 result->specs[spec_idx].type = LOG_FORMAT_NANOSECONDS;
184 }
else if (strncmp(p,
"message", 7) == 0) {
185 result->specs[spec_idx].type = LOG_FORMAT_MESSAGE;
188 }
else if (strncmp(p,
"colorlog_level_string_to_color", 30) == 0) {
189 result->specs[spec_idx].type = LOG_FORMAT_COLORLOG_LEVEL;
192 }
else if (strncmp(p,
"color(", 6) == 0) {
198 const char *color_start = p;
199 while (*p && paren_depth > 0) {
208 if (!*p || paren_depth != 0) {
210 log_error(
"Invalid %%color format: missing closing )");
214 size_t color_arg_len = p - color_start;
217 result->specs[spec_idx].type = LOG_FORMAT_COLOR;
218 result->specs[spec_idx].literal = SAFE_MALLOC(color_arg_len + 1,
char *);
219 if (!result->specs[spec_idx].literal) {
222 memcpy(result->specs[spec_idx].literal, color_start, color_arg_len);
223 result->specs[spec_idx].literal[color_arg_len] =
'\0';
224 result->specs[spec_idx].literal_len = color_arg_len;
232 const char *fmt_start = p;
236 while (*p && *p !=
'%' && *p !=
'\\' && fmt_len < 8) {
242 log_error(
"Empty format specifier: %%");
246 result->specs[spec_idx].type = LOG_FORMAT_STRFTIME_CODE;
247 result->specs[spec_idx].literal = SAFE_MALLOC(fmt_len + 1,
char *);
248 if (!result->specs[spec_idx].literal) {
251 memcpy(result->specs[spec_idx].literal, fmt_start, fmt_len);
252 result->specs[spec_idx].literal[fmt_len] =
'\0';
253 result->specs[spec_idx].literal_len = fmt_len;
261 const char *text_start = p;
262 while (*p && *p !=
'\\' && *p !=
'%') {
266 result->specs[spec_idx].type = LOG_FORMAT_LITERAL;
267 size_t text_len = p - text_start;
268 result->specs[spec_idx].literal = SAFE_MALLOC(text_len + 1,
char *);
269 if (!result->specs[spec_idx].literal) {
274 memcpy(result->specs[spec_idx].literal, text_start, text_len);
275 result->specs[spec_idx].literal[text_len] =
'\0';
276 result->specs[spec_idx].literal_len = text_len;
281 result->spec_count = spec_idx;
290 return parse_format_string(format_str, console_only);
295 SET_ERRNO(ERROR_INVALID_PARAM,
"null format");
300 for (
size_t i = 0; i < format->spec_count; i++) {
301 if (format->specs[i].literal) {
302 SAFE_FREE(format->specs[i].literal);
305 SAFE_FREE(format->specs);
308 if (format->original) {
309 SAFE_FREE(format->original);
335static const char *get_level_string(log_level_t level) {
366static log_color_t parse_color_level(
const char *level_name, log_level_t current_level) {
368 return LOG_COLOR_RESET;
372 if (strcmp(level_name,
"*") == 0) {
373 switch (current_level) {
375 return LOG_COLOR_DEV;
377 return LOG_COLOR_DEBUG;
379 return LOG_COLOR_INFO;
381 return LOG_COLOR_WARN;
383 return LOG_COLOR_ERROR;
385 return LOG_COLOR_FATAL;
387 return LOG_COLOR_RESET;
392 if (strcmp(level_name,
"DEV") == 0) {
393 return LOG_COLOR_DEV;
394 }
else if (strcmp(level_name,
"DEBUG") == 0) {
395 return LOG_COLOR_DEBUG;
396 }
else if (strcmp(level_name,
"INFO") == 0) {
397 return LOG_COLOR_INFO;
398 }
else if (strcmp(level_name,
"WARN") == 0) {
399 return LOG_COLOR_WARN;
400 }
else if (strcmp(level_name,
"ERROR") == 0) {
401 return LOG_COLOR_ERROR;
402 }
else if (strcmp(level_name,
"FATAL") == 0) {
403 return LOG_COLOR_FATAL;
407 if (strcmp(level_name,
"GREY") == 0 || strcmp(level_name,
"GRAY") == 0) {
408 return LOG_COLOR_GREY;
411 return LOG_COLOR_RESET;
434static int render_format_content(
const char *content,
char *buf,
size_t buf_size, log_level_t level,
435 const char *timestamp,
const char *file,
int line,
const char *func, uint64_t tid,
436 const char *message,
bool use_colors, uint64_t time_nanoseconds) {
437 if (!content || !buf || buf_size == 0) {
438 SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid arguments: content=%p, buf=%p, buf_size=%zu", content, buf, buf_size);
444 if (!content_format) {
448 int written =
log_template_apply(content_format, buf, buf_size, level, timestamp, file, line, func, tid, message,
449 use_colors, time_nanoseconds);
456 const char *timestamp,
const char *file,
int line,
const char *func, uint64_t tid,
457 const char *message,
bool use_colors, uint64_t time_nanoseconds) {
458 if (!format || !buf || buf_size == 0) {
462 int total_written = 0;
464 size_t remaining = buf_size - 1;
468 for (
size_t i = 0; i < format->spec_count; i++) {
469 const log_format_spec_t *spec = &format->specs[i];
472 switch (spec->type) {
473 case LOG_FORMAT_LITERAL:
474 written =
safe_snprintf(p, remaining + 1,
"%s", spec->literal ? spec->literal :
"");
477 case LOG_FORMAT_TIME:
482 log_debug(
"time_format_now failed for format: %s", spec->literal);
488 case LOG_FORMAT_LEVEL:
490 written =
safe_snprintf(p, remaining + 1,
"%s", get_level_string(level));
493 case LOG_FORMAT_LEVEL_ALIGNED:
498 case LOG_FORMAT_FILE:
504 case LOG_FORMAT_FILE_RELATIVE:
511 case LOG_FORMAT_LINE:
517 case LOG_FORMAT_FUNC:
524 written =
safe_snprintf(p, remaining + 1,
"%llu", (
unsigned long long)tid);
527 case LOG_FORMAT_MICROSECONDS: {
529 long nanoseconds = (long)(time_nanoseconds % NS_PER_SEC_INT);
530 long microseconds = nanoseconds / 1000;
531 if (microseconds < 0)
533 if (microseconds > 999999)
534 microseconds = 999999;
535 written =
safe_snprintf(p, remaining + 1,
"%06ld", microseconds);
539 case LOG_FORMAT_NANOSECONDS: {
541 long nanoseconds = (long)(time_nanoseconds % NS_PER_SEC_INT);
544 if (nanoseconds > 999999999)
545 nanoseconds = 999999999;
546 written =
safe_snprintf(p, remaining + 1,
"%09ld", nanoseconds);
550 case LOG_FORMAT_STRFTIME_CODE: {
555 size_t fmt_len = spec->literal_len + 1;
556 char *format_str = SAFE_MALLOC(fmt_len + 1,
char *);
559 memcpy(format_str + 1, spec->literal, spec->literal_len);
560 format_str[fmt_len] =
'\0';
563 log_debug(
"time_format_now failed for format code: %s", format_str);
566 SAFE_FREE(format_str);
572 case LOG_FORMAT_MESSAGE:
578 case LOG_FORMAT_COLORED_MESSAGE: {
582 written =
safe_snprintf(p, remaining + 1,
"%s", colorized_msg);
587 case LOG_FORMAT_COLORLOG_LEVEL:
593 case LOG_FORMAT_COLOR: {
597 if (!spec->literal || spec->literal_len == 0) {
603 const char *comma_pos = strchr(spec->literal,
',');
606 log_debug(
"log_template_apply: %%color format missing comma in: %s", spec->literal);
612 size_t level_len = comma_pos - spec->literal;
614 if (level_len >=
sizeof(level_name)) {
619 memcpy(level_name, spec->literal, level_len);
620 level_name[level_len] =
'\0';
623 log_color_t color = parse_color_level(level_name, level);
626 const char *content_start = comma_pos + 1;
627 while (*content_start ==
' ' || *content_start ==
'\t') {
632 char content_buf[512];
633 int content_len = render_format_content(content_start, content_buf,
sizeof(content_buf), level, timestamp, file,
634 line, func, tid, message, use_colors, time_nanoseconds);
636 if (content_len < 0 || content_len >= (
int)
sizeof(content_buf)) {
645 written =
safe_snprintf(p, remaining + 1,
"%s", colored_content);
649 case LOG_FORMAT_NEWLINE:
667 if ((
size_t)written > remaining) {
669 log_debug(
"log_template_apply: buffer overflow prevented");
674 remaining -= written;
675 total_written += written;
679 return total_written;
const char * colorize_log_message(const char *message)
Colorize a log message for terminal output.
const char * get_level_string_padded(log_level_t level)
Get padded level string for consistent alignment.
const char * extract_project_relative_path(const char *file)
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
bool utf8_is_valid(const char *str)
const char * colored_string(log_color_t color, const char *text)
int time_format_now(const char *format_str, char *buf, size_t buf_size)