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/json.c
Go to the documentation of this file.
1
6
7/* Storage capacities in elements; text buffers include the terminating NUL. */
8#define LOG_JSON_JSON_BUFFER_SIZE 2048
9#define LOG_JSON_ESCAPED_MESSAGE_BUFFER_SIZE 512
10#define LOG_JSON_ESCAPED_FILE_BUFFER_SIZE 256
11#define LOG_JSON_ESCAPED_FUNC_BUFFER_SIZE 128
12
13#include <ascii-chat/common.h>
14#include <ascii-chat/log/json.h>
15#include <ascii-chat/log/log.h>
16#include <libplatform/compiler.h>
17#include <libplatform/system.h>
18#include <libplatform/terminal.h>
21#include <ascii-chat/source_root.h>
23#include <yyjson.h>
24#include <stdint.h>
25#include <string.h>
26#include <time.h>
27
28/* ============================================================================
29 * Note: log_set_json_output() is implemented in logging.c where the g_log
30 * struct is defined.
31 * ============================================================================ */
32
33#include <unistd.h>
34#include <stdarg.h>
35
44static size_t format_timestamp_microseconds(uint64_t time_nanoseconds, char *buf, size_t buf_size) {
45 if (buf_size < 32) {
46 return 0;
47 }
48
49 /* Extract seconds and nanoseconds */
50 time_t seconds = (time_t)(time_nanoseconds / NS_PER_SEC_INT);
51 long nanoseconds = (long)(time_nanoseconds % NS_PER_SEC_INT);
52
53 /* Convert seconds to struct tm */
54 struct tm tm_info;
55 platform_localtime(&seconds, &tm_info);
56
57 /* Format the time part (HH:MM:SS) */
58 size_t len = strftime(buf, buf_size, "%H:%M:%S", &tm_info);
59 if (len == 0 || len >= buf_size) {
60 return 0;
61 }
62
63 /* Convert nanoseconds to microseconds for display */
64 long microseconds = nanoseconds / 1000;
65 if (microseconds < 0) {
66 microseconds = 0;
67 }
68 if (microseconds > 999999) {
69 microseconds = 999999;
70 }
71
72 /* Append microseconds */
73 int result = safe_snprintf(buf + len, buf_size - len, ".%06ld", microseconds);
74 if (result < 0 || result >= (int)(buf_size - len)) {
75 return 0;
76 }
77
78 return len + (size_t)result;
79}
80
87static const char *log_level_to_string(log_level_t level) {
88 switch (level) {
89 case LOG_DEBUG:
90 return "DEBUG";
91 case LOG_INFO:
92 return "INFO";
93 case LOG_WARN:
94 return "WARN";
95 case LOG_ERROR:
96 return "ERROR";
97 case LOG_FATAL:
98 return "FATAL";
99 default:
100 return "UNKNOWN";
101 }
102}
103
104void log_json_write(int fd, log_level_t level, uint64_t time_nanoseconds, const char *file, int line, const char *func,
105 const char *message) {
106 if (fd < 0 || !message) {
107 return;
108 }
109
110 /* Create root object */
111 yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
112 if (!doc) {
113 return;
114 }
115
116 yyjson_mut_val *root = yyjson_mut_obj(doc);
117 if (!root) {
118 yyjson_mut_doc_free(doc);
119 return;
120 }
121 yyjson_mut_doc_set_root(doc, root);
122
123 /* Create "header" object */
124 yyjson_mut_val *header = yyjson_mut_obj(doc);
125 if (header) {
126 yyjson_mut_obj_add_val(doc, root, "header", header);
127
128 /* Add timestamp (HH:MM:SS.microseconds) */
129 char timestamp_buf[LOG_TIMESTAMP_BUFFER_SIZE];
130 size_t ts_len = format_timestamp_microseconds(time_nanoseconds, timestamp_buf, sizeof(timestamp_buf));
131 if (ts_len > 0) {
132 yyjson_mut_obj_add_strncpy(doc, header, "timestamp", timestamp_buf, ts_len);
133 }
134
135 /* Add level */
136 yyjson_mut_obj_add_str(doc, header, "level", log_level_to_string(level));
137
138 /* Add thread ID */
139 uint64_t tid = (uint64_t)thread_current_id();
140 yyjson_mut_obj_add_uint(doc, header, "tid", tid);
141
142 /* Add file (if not NULL) - normalize to project-relative path */
143 if (file) {
144 const char *rel_file = extract_project_relative_path(file, ASCIICHAT_OWNED_SOURCE_DIRS);
145 yyjson_mut_obj_add_str(doc, header, "file", rel_file);
146 }
147
148 /* Add line (if > 0) */
149 if (line > 0) {
150 yyjson_mut_obj_add_int(doc, header, "line", line);
151 }
152
153 /* Add func (if not NULL) */
154 if (func) {
155 yyjson_mut_obj_add_str(doc, header, "func", func);
156 }
157 }
158
159 /* Create "body" object with message */
160 yyjson_mut_val *body = yyjson_mut_obj(doc);
161 if (body) {
162 yyjson_mut_obj_add_val(doc, root, "body", body);
163 yyjson_mut_obj_add_str(doc, body, "message", message);
164 }
165
166 /* Serialize to compact JSON (single line, no pretty-printing) */
167 size_t json_len = 0;
168 char *json_str = yyjson_mut_write(doc, 0, &json_len);
169
170 if (json_str && json_len > 0) {
171 // Include the delimiter in the same write so concurrent records stay on separate lines.
172 json_str[json_len] = '\n';
173 platform_write_all(fd, (const uint8_t *)json_str, json_len + 1);
174
175 /* Free serialized JSON string */
176 free(json_str);
177 }
178
179 /* Free document */
180 yyjson_mut_doc_free(doc);
181}
182
194static size_t json_escape_async_safe(const char *src, char *dest, size_t dest_size) {
195 if (!src || !dest || dest_size < 2) {
196 return 0;
197 }
198
199 size_t pos = 0;
200 const char *p = src;
201 while (*p && pos < dest_size - 1) {
202 char c = *p++;
203 if (c == '"' || c == '\\') {
204 if (pos + 1 < dest_size - 1) {
205 dest[pos++] = '\\';
206 dest[pos++] = c;
207 } else {
208 break;
209 }
210 } else if (c == '\n') {
211 if (pos + 1 < dest_size - 1) {
212 dest[pos++] = '\\';
213 dest[pos++] = 'n';
214 } else {
215 break;
216 }
217 } else if (c == '\r') {
218 if (pos + 1 < dest_size - 1) {
219 dest[pos++] = '\\';
220 dest[pos++] = 'r';
221 } else {
222 break;
223 }
224 } else if (c == '\t') {
225 if (pos + 1 < dest_size - 1) {
226 dest[pos++] = '\\';
227 dest[pos++] = 't';
228 } else {
229 break;
230 }
231 } else if ((unsigned char)c < 32) {
232 /* Skip other control characters */
233 continue;
234 } else if ((unsigned char)c >= 0x80) {
235 // Preserve complete UTF-8 characters when the fixed output buffer fills.
236 const char *character = p - 1;
237 size_t available = 0;
238 while (available < 4 && character[available])
239 available++;
240 int bytes = utf8_next_char_bytes(character, available);
241 if (bytes < 0)
242 continue;
243 if (pos + (size_t)bytes >= dest_size)
244 break;
245 for (int i = 0; i < bytes; ++i)
246 dest[pos++] = character[i];
247 p += bytes - 1;
248 } else {
249 dest[pos++] = c;
250 }
251 }
252 dest[pos] = '\0';
253 return pos;
254}
255
273void log_json_async_safe(int fd, log_level_t level, const char *file, int line, const char *func, const char *message) {
274 if (fd < 0 || !message) {
275 return;
276 }
277
278 /* Format JSON manually on stack using only async-safe operations */
279 char json_buffer[LOG_JSON_JSON_BUFFER_SIZE];
280 char escaped_message[LOG_JSON_ESCAPED_MESSAGE_BUFFER_SIZE];
281 char escaped_file[LOG_JSON_ESCAPED_FILE_BUFFER_SIZE];
282 char escaped_func[LOG_JSON_ESCAPED_FUNC_BUFFER_SIZE];
283 char timestamp_buf[LOG_TIMESTAMP_BUFFER_SIZE];
284
285 /* Escape the strings (normalize file path to project-relative) */
286 json_escape_async_safe(message, escaped_message, sizeof(escaped_message));
287 if (file) {
288 const char *rel_file = extract_project_relative_path(file, ASCIICHAT_OWNED_SOURCE_DIRS);
289 json_escape_async_safe(rel_file, escaped_file, sizeof(escaped_file));
290 } else {
291 escaped_file[0] = '\0';
292 }
293 json_escape_async_safe(func ? func : "", escaped_func, sizeof(escaped_func));
294
295 /* Format timestamp using only async-safe operations */
296 uint64_t time_ns = time_get_realtime_ns();
297 format_timestamp_microseconds(time_ns, timestamp_buf, sizeof(timestamp_buf));
298
299 /* Get thread ID (note: thread_current_id is assumed to be async-safe) */
300 uint64_t tid = (uint64_t)thread_current_id();
301
302 /* Format complete JSON object with timestamp */
303 int written = snprintf(json_buffer, sizeof(json_buffer),
304 "{\"header\":{\"timestamp\":\"%s\",\"level\":\"%s\",\"tid\":%lu,"
305 "\"file\":\"%s\",\"line\":%d,\"func\":\"%s\"},"
306 "\"body\":{\"message\":\"%s\"}}\n",
307 timestamp_buf, log_level_to_string(level), (unsigned long)tid, file ? escaped_file : "", line,
308 func ? escaped_func : "", escaped_message);
309
310 /* Write to fd if successful */
311 if (written > 0 && written < (int)sizeof(json_buffer)) {
312 platform_write_all(fd, (const uint8_t *)json_buffer, (size_t)written);
313 }
314}
⚙️ Common definitions, error codes, macros, and types shared throughout the application
unsigned long long uint64_t
Definition common.h:63
unsigned char uint8_t
Definition common.h:60
log_level_t
Logging levels enumeration.
Definition types.h:29
#define LOG_TIMESTAMP_BUFFER_SIZE
Maximum size of a timestamp string.
Definition log/log.h:101
#define NS_PER_SEC_INT
Definition time.h:169
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Platform-safe snprintf wrapper.
size_t platform_write_all(int fd, const void *buf, size_t count)
Write all bytes to a file descriptor, handling partial writes.
asciichat_error_t platform_localtime(const time_t *timer, struct tm *result)
Platform-safe localtime wrapper.
const char * extract_project_relative_path(const char *file, const char *const *owned_dirs)
Extract a path relative to the source root embedded by CMake.
int utf8_next_char_bytes(const char *str, size_t max_bytes)
Get byte length of next UTF-8 character.
Definition utf8.c:177
📝 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
#define LOG_JSON_JSON_BUFFER_SIZE
Definition log/json.c:8
#define LOG_JSON_ESCAPED_MESSAGE_BUFFER_SIZE
Definition log/json.c:9
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
#define LOG_JSON_ESCAPED_FILE_BUFFER_SIZE
Definition log/json.c:10
#define LOG_JSON_ESCAPED_FUNC_BUFFER_SIZE
Definition log/json.c:11
📝 Logging API with multiple log levels and terminal output control
📂 Path Manipulation Utilities
🔤 String Manipulation and Shell Escaping Utilities
⏱️ High-precision timing utilities using sokol_time.h and uthash
#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