ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
instrument_log.c
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2// Debug instrumentation logging runtime for ascii-chat line tracing
3
4#include <ascii-chat/tooling/panic/instrument_log.h>
5#include <ascii-chat/common.h>
6#include <ascii-chat/platform/util.h>
7#include <ascii-chat/platform/mutex.h>
8#include <ascii-chat/platform/system.h>
9#include <ascii-chat/platform/thread.h>
10#include <ascii-chat/util/path.h>
11#include <ascii-chat/util/time.h>
12
13#include <ctype.h>
14#include <errno.h>
15#include <fcntl.h>
16#include <limits.h>
17#include <stdarg.h>
18#include <stdbool.h>
19#include <stdio.h>
20#include <stdlib.h>
21#include <string.h>
22#include <sys/stat.h>
23#include <sys/types.h>
24#include <time.h>
25
26#ifdef _WIN32
27#include <io.h>
28#else
29#include <unistd.h>
30#endif
31
32#if !defined(_WIN32)
33#define ASCII_INSTR_SOURCE_PRINT_HAVE_REGEX 1
34#include <regex.h>
35#else
36#define ASCII_INSTR_SOURCE_PRINT_HAVE_REGEX 0
37// Windows doesn't have mode_t
38typedef int mode_t;
39#endif
40
41#ifdef _WIN32
42#include <direct.h>
43#ifndef mkdir
44#define mkdir(path, mode) _mkdir(path)
45#endif
46// Windows uses _write instead of write
47#define posix_write _write
48#ifndef STDERR_FILENO
49#define STDERR_FILENO 2
50#endif
51#else
52#define posix_write write
53#endif
54
55#ifndef ASCII_INSTR_SOURCE_PRINT_DEFAULT_BASENAME
56#define ASCII_INSTR_SOURCE_PRINT_DEFAULT_BASENAME "ascii-instr"
57#endif
58
59#ifndef ASCII_INSTR_SOURCE_PRINT_MAX_LINE
60#define ASCII_INSTR_SOURCE_PRINT_MAX_LINE 4096
61#endif
62
63#ifndef ASCII_INSTR_SOURCE_PRINT_MAX_SNIPPET
64#define ASCII_INSTR_SOURCE_PRINT_MAX_SNIPPET 2048
65#endif
66
73
82
91
126
127static tls_key_t g_runtime_key;
128static mutex_t g_runtime_mutex;
129static bool g_runtime_initialized = false;
130static bool g_runtime_mutex_initialized = false;
131static char g_output_dir[PATH_MAX];
132static bool g_output_dir_set = false;
133static bool g_disable_write = false;
134static uint64_t g_start_ns = 0;
135static bool g_ticks_initialized = false;
136static bool g_coverage_enabled = false;
137static bool g_echo_to_stderr = false;
138static bool g_echo_to_stderr_initialized = false;
139
140static void asciichat_instr_runtime_init_once(void);
141static void asciichat_instr_runtime_tls_destructor(void *ptr);
142static int asciichat_instr_open_log_file(asciichat_instr_runtime_t *runtime);
143static void asciichat_instr_runtime_configure(asciichat_instr_runtime_t *runtime);
144static bool asciichat_instr_should_log(const asciichat_instr_runtime_t *runtime, const char *file_path,
145 uint32_t line_number, const char *function_name);
146static int asciichat_instr_write_full(int fd, const char *buffer, size_t len);
147static bool asciichat_instr_env_is_enabled(const char *value);
148static bool asciichat_instr_parse_positive_uint32(const char *value, uint32_t *out_value);
149#if ASCII_INSTR_SOURCE_PRINT_HAVE_REGEX
150static bool asciichat_instr_compile_regex(regex_t *regex, const char *pattern);
151#endif
152static void asciichat_instr_only_list_destroy(asciichat_instr_only_list_t *list);
153static bool asciichat_instr_only_list_append(asciichat_instr_only_list_t *list, asciichat_instr_selector_type_t type,
154 const char *module, const char *pattern);
155static void asciichat_instr_trim(char *value);
156static bool asciichat_instr_parse_only_filters(asciichat_instr_runtime_t *runtime, const char *value);
157static bool asciichat_instr_only_selectors_match(const asciichat_instr_runtime_t *runtime, const char *file_path,
158 const char *function_name);
159static bool asciichat_instr_match_glob(const char *pattern, const char *value);
160static const char *asciichat_instr_basename(const char *path);
161static bool asciichat_instr_path_contains_module(const char *file_path, const char *module_name);
162
163static _Thread_local bool g_logging_reentry_guard = false;
164static bool g_instrumentation_enabled = false;
165static bool g_instrumentation_enabled_checked = false;
166
168 if (g_disable_write) {
169 return NULL;
170 }
171
172 // Initialize runtime once using mutex-protected initialization
173 if (!g_runtime_initialized) {
174 // Initialize mutex if needed
175 if (!g_runtime_mutex_initialized) {
176 mutex_init(&g_runtime_mutex);
177 g_runtime_mutex_initialized = true;
178 }
179
180 mutex_lock(&g_runtime_mutex);
181 if (!g_runtime_initialized) {
182 asciichat_instr_runtime_init_once();
183 }
184 mutex_unlock(&g_runtime_mutex);
185 }
186
187 asciichat_instr_runtime_t *runtime = ascii_tls_get(g_runtime_key);
188 if (runtime != NULL) {
189 return runtime;
190 }
191
192 runtime = SAFE_CALLOC(1, sizeof(*runtime), asciichat_instr_runtime_t *);
193 if (runtime == NULL) {
194 return NULL;
195 }
196
197 runtime->pid = platform_get_pid();
199 runtime->sequence = 0;
200 runtime->call_counter = 0;
201 runtime->fd = -1;
202 runtime->filter_include = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_INCLUDE");
203 runtime->filter_exclude = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_EXCLUDE");
204 runtime->filter_thread = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_THREAD");
205 runtime->filter_function_include = NULL;
206 runtime->filter_function_exclude = NULL;
207 runtime->filters_enabled = false;
208 runtime->rate = 1;
209 runtime->rate_enabled = false;
210
211 asciichat_instr_runtime_configure(runtime);
212
213 if (ascii_tls_set(g_runtime_key, runtime) != 0) {
214 SAFE_FREE(runtime);
215 return NULL;
216 }
217
218 return runtime;
219}
220
222 if (runtime == NULL) {
223 return;
224 }
225
226 if (runtime->fd >= 0) {
227 platform_close(runtime->fd);
228 runtime->fd = -1;
229 }
230
231#if ASCII_INSTR_SOURCE_PRINT_HAVE_REGEX
232 if (runtime->include_regex_valid) {
233 regfree(&runtime->include_regex);
234 runtime->include_regex_valid = false;
235 }
236 if (runtime->exclude_regex_valid) {
237 regfree(&runtime->exclude_regex);
238 runtime->exclude_regex_valid = false;
239 }
240 if (runtime->function_include_regex_valid) {
241 regfree(&runtime->function_include_regex);
242 runtime->function_include_regex_valid = false;
243 }
244 if (runtime->function_exclude_regex_valid) {
245 regfree(&runtime->function_exclude_regex);
246 runtime->function_exclude_regex_valid = false;
247 }
248#endif
249
250 asciichat_instr_only_list_destroy(&runtime->only_selectors);
251 SAFE_FREE(runtime);
252}
253
255 if (g_runtime_mutex_initialized) {
256 mutex_lock(&g_runtime_mutex);
257 }
258
259 if (g_runtime_initialized) {
260 g_disable_write = true;
261 ascii_tls_key_delete(g_runtime_key);
262 g_runtime_initialized = false;
263 g_ticks_initialized = false;
264 g_start_ns = 0;
265 g_coverage_enabled = false;
266 g_output_dir_set = false;
267 g_output_dir[0] = '\0';
268 g_instrumentation_enabled_checked = false;
269 g_instrumentation_enabled = false;
270 g_echo_to_stderr_initialized = false;
271 g_echo_to_stderr = false;
272 }
273
274 // Reset g_disable_write so instrumentation can be re-enabled in subsequent tests
275 g_disable_write = false;
276
277 if (g_runtime_mutex_initialized) {
278 mutex_unlock(&g_runtime_mutex);
279 mutex_destroy(&g_runtime_mutex);
280 g_runtime_mutex_initialized = false;
281 }
282}
283
284void asciichat_instr_log_line(const char *file_path, uint32_t line_number, const char *function_name,
285 const char *snippet, uint8_t is_macro_expansion) {
286 // Instrumentation is enabled by default when the binary is built with Source Print.
287 // Set ASCII_INSTR_SOURCE_PRINT_ENABLE=0 to disable tracing at runtime.
288 if (!g_instrumentation_enabled_checked) {
289 const char *enable_env = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_ENABLE");
290 // Default to enabled (true) - only disable if explicitly set to "0", "false", "off", or "no"
291 if (enable_env != NULL && enable_env[0] != '\0') {
292 g_instrumentation_enabled = asciichat_instr_env_is_enabled(enable_env);
293 } else {
294 g_instrumentation_enabled = true; // Default to enabled when instrumented
295 }
296 g_instrumentation_enabled_checked = true;
297 }
298
299 if (!g_instrumentation_enabled) {
300 return;
301 }
302
303 if (g_disable_write) {
304 return;
305 }
306
307 if (g_logging_reentry_guard) {
308 return;
309 }
310
311 g_logging_reentry_guard = true;
312
314 if (runtime == NULL) {
315 goto cleanup;
316 }
317
318 if (!asciichat_instr_should_log(runtime, file_path, line_number, function_name)) {
319 goto cleanup;
320 }
321
322 runtime->call_counter++;
323 if (runtime->rate_enabled) {
324 const uint64_t counter = runtime->call_counter;
325 if (((counter - 1U) % runtime->rate) != 0U) {
326 goto cleanup;
327 }
328 }
329
330 if (runtime->fd < 0) {
331 if (asciichat_instr_open_log_file(runtime) != 0) {
332 runtime->stderr_fallback = true;
333 }
334 }
335
336 const int fd = runtime->stderr_fallback ? STDERR_FILENO : runtime->fd;
338 size_t pos = 0;
339
340 uint64_t realtime_ns = time_get_realtime_ns();
341 time_t sec = (time_t)(realtime_ns / NS_PER_SEC_INT);
342 long nsec = (long)(realtime_ns % NS_PER_SEC_INT);
343 struct tm tm_now;
344 memset(&tm_now, 0, sizeof(tm_now));
345 if (platform_gtime(&sec, &tm_now) != ASCIICHAT_OK) {
346 memset(&tm_now, 0, sizeof(tm_now));
347 }
348
349 char timestamp[32];
350 size_t ts_len = strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%S", &tm_now);
351 if (ts_len == 0) {
352 SAFE_STRNCPY(timestamp, "1970-01-01T00:00:00", sizeof(timestamp));
353 ts_len = strlen(timestamp);
354 }
355
356 char elapsed_buf[32];
357 elapsed_buf[0] = '\0';
358 if (g_ticks_initialized) {
359 uint64_t now_ns = time_get_ns();
360 uint64_t elapsed_ns = time_elapsed_ns(g_start_ns, now_ns);
361 if (format_duration_ns((double)elapsed_ns, elapsed_buf, sizeof(elapsed_buf)) < 0) {
362 elapsed_buf[0] = '\0';
363 }
364 }
365
366 runtime->sequence++;
367
368 const char *elapsed_field = (elapsed_buf[0] != '\0') ? elapsed_buf : "-";
369
370 const char *safe_file_path = (file_path != NULL) ? file_path : "<unknown>";
371 pos += safe_snprintf(buffer + pos, sizeof(buffer) - pos,
372 "pid=%d tid=%llu seq=%llu ts=%.*s.%09ldZ elapsed=%s file=%s line=%u func=%s macro=%u snippet=",
373 runtime->pid, (unsigned long long)runtime->thread_id, (unsigned long long)runtime->sequence,
374 (int)ts_len, timestamp, nsec, elapsed_field, safe_file_path, line_number,
375 function_name ? function_name : "<unknown>", (unsigned)is_macro_expansion);
376
377 if (snippet != NULL) {
378 size_t snippet_len = strnlen(snippet, ASCII_INSTR_SOURCE_PRINT_MAX_SNIPPET);
379 for (size_t i = 0; i < snippet_len && pos < sizeof(buffer) - 2; ++i) {
380 const char ch = snippet[i];
381 switch (ch) {
382 case '\n':
383 buffer[pos++] = '\\';
384 buffer[pos++] = 'n';
385 break;
386 case '\r':
387 buffer[pos++] = '\\';
388 buffer[pos++] = 'r';
389 break;
390 case '\t':
391 buffer[pos++] = '\\';
392 buffer[pos++] = 't';
393 break;
394 default:
395 buffer[pos++] = ch;
396 break;
397 }
398 }
399 }
400
401 if (pos >= sizeof(buffer) - 1) {
402 pos = sizeof(buffer) - 2;
403 }
404
405 buffer[pos++] = '\n';
406 buffer[pos] = '\0';
407
408 asciichat_instr_write_full(fd, buffer, pos);
409
410 // Also echo to stderr if requested via environment variable
411 if (!g_echo_to_stderr_initialized) {
412 const char *echo_env = SAFE_GETENV("ASCII_CHAT_DEBUG_SELF_SOURCE_CODE_LOG_STDERR");
413 g_echo_to_stderr = asciichat_instr_env_is_enabled(echo_env);
414 g_echo_to_stderr_initialized = true;
415 }
416
417 if (g_echo_to_stderr && !runtime->stderr_fallback) {
418 // Write to stderr in addition to the log file
419 // Suppress Windows deprecation warning for POSIX write function
420#ifdef _WIN32
421#pragma clang diagnostic push
422#pragma clang diagnostic ignored "-Wdeprecated-declarations"
423#endif
424 (void)posix_write(STDERR_FILENO, buffer, pos);
425#ifdef _WIN32
426#pragma clang diagnostic pop
427#endif
428 }
429
430cleanup:
431 g_logging_reentry_guard = false;
432}
433
435 if (g_disable_write) {
436 return false;
437 }
438
439 // Initialize runtime once using mutex-protected initialization
440 if (!g_runtime_initialized) {
441 if (!g_runtime_mutex_initialized) {
442 mutex_init(&g_runtime_mutex);
443 g_runtime_mutex_initialized = true;
444 }
445
446 mutex_lock(&g_runtime_mutex);
447 if (!g_runtime_initialized) {
448 asciichat_instr_runtime_init_once();
449 }
450 mutex_unlock(&g_runtime_mutex);
451 }
452
453 return g_coverage_enabled;
454}
455
456void asciichat_instr_log_pc(uintptr_t program_counter) {
458 return;
459 }
460
461 char snippet[64];
462 safe_snprintf(snippet, sizeof(snippet), "pc=0x%zx", (size_t)program_counter);
463 asciichat_instr_log_line("__coverage__", 0, "<coverage>", snippet, ASCII_INSTR_SOURCE_PRINT_MACRO_NONE);
464}
465
466static void asciichat_instr_runtime_init_once(void) {
467 // NOTE: This function is always called with g_runtime_mutex held by the caller
468 // so we don't need to lock/unlock here
469 if (!g_runtime_initialized) {
470 (void)ascii_tls_key_create(&g_runtime_key, asciichat_instr_runtime_tls_destructor);
471 const char *output_dir_env = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_OUTPUT_DIR");
472 if (output_dir_env != NULL && output_dir_env[0] != '\0') {
473 char *normalized_output_dir = NULL;
474 asciichat_error_t validation_result =
475 path_validate_user_path(output_dir_env, PATH_ROLE_LOG_FILE, &normalized_output_dir);
476 if (validation_result == ASCIICHAT_OK && normalized_output_dir != NULL) {
477 SAFE_STRNCPY(g_output_dir, normalized_output_dir, sizeof(g_output_dir));
478 g_output_dir[sizeof(g_output_dir) - 1] = '\0';
479 g_output_dir_set = true;
480 } else {
481 log_warn("Ignoring invalid ASCII_INSTR_SOURCE_PRINT_OUTPUT_DIR path: %s", output_dir_env);
482 }
483 SAFE_FREE(normalized_output_dir);
484 }
485 const char *coverage_env = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_ENABLE_COVERAGE");
486 g_coverage_enabled = asciichat_instr_env_is_enabled(coverage_env);
487 g_start_ns = time_get_ns();
488 g_ticks_initialized = true;
489 g_runtime_initialized = true;
490 }
491}
492
493static void asciichat_instr_runtime_tls_destructor(void *ptr) {
495}
496
497static bool asciichat_instr_build_log_path(asciichat_instr_runtime_t *runtime) {
498 // Check for custom log file path first
499 const char *custom_log_file = SAFE_GETENV("ASCII_CHAT_DEBUG_SELF_SOURCE_CODE_LOG_FILE");
500 bool is_custom_path = (custom_log_file != NULL && custom_log_file[0] != '\0');
501
502 if (is_custom_path) {
503 const char *debug_env = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_ECHO_STDERR");
504 if (debug_env && debug_env[0] == '1') {
505 fprintf(stderr, "ASCII_INSTR: Using custom log path: %s\n", custom_log_file);
506 }
507
508 // For instrumentation log files, bypass strict path validation since this is a debug feature
509 // Just normalize the path to an absolute path without security checks
510 char *expanded = expand_path(custom_log_file);
511 if (!expanded) {
512 log_warn("Failed to expand ASCII_CHAT_DEBUG_SELF_SOURCE_CODE_LOG_FILE path: %s", custom_log_file);
513 return false;
514 }
515
516 // Convert to absolute path if relative
517 char absolute_buf[PATH_MAX];
518 if (!path_is_absolute(expanded)) {
519 char cwd_buf[PATH_MAX];
520 if (!platform_get_cwd(cwd_buf, sizeof(cwd_buf))) {
521 SAFE_FREE(expanded);
522 return false;
523 }
524 // Check if expanded already starts with a separator to avoid double separators
525 if (strlen(expanded) > 0 && expanded[0] == PATH_DELIM) {
526 safe_snprintf(absolute_buf, sizeof(absolute_buf), "%s%s", cwd_buf, expanded);
527 } else {
528 safe_snprintf(absolute_buf, sizeof(absolute_buf), "%s%c%s", cwd_buf, PATH_DELIM, expanded);
529 }
530 SAFE_FREE(expanded);
531 expanded = platform_strdup(absolute_buf);
532 if (!expanded) {
533 return false;
534 }
535 }
536
537 SAFE_STRNCPY(runtime->log_path, expanded, sizeof(runtime->log_path));
538 runtime->log_path[sizeof(runtime->log_path) - 1] = '\0';
539 SAFE_FREE(expanded);
540
541 if (debug_env && debug_env[0] == '1') {
542 fprintf(stderr, "ASCII_INSTR: Resolved custom log path: %s\n", runtime->log_path);
543 }
544
545 // Don't check if file exists - allow appending to existing file
546 // Path already normalized, skip validation below
547 } else {
548 // Determine output directory
549 char output_dir_buf[PATH_MAX];
550 if (g_output_dir_set) {
551 // Use the output directory from ASCII_INSTR_SOURCE_PRINT_OUTPUT_DIR
552 SAFE_STRNCPY(output_dir_buf, g_output_dir, sizeof(output_dir_buf));
553 } else if (!platform_get_cwd(output_dir_buf, sizeof(output_dir_buf))) {
554 // Fallback to temp directory if cwd fails
555 const char *fallback = SAFE_GETENV("TMPDIR");
556 if (fallback == NULL) {
557 fallback = SAFE_GETENV("TEMP");
558 }
559 if (fallback == NULL) {
560 fallback = SAFE_GETENV("TMP");
561 }
562 if (fallback == NULL) {
563 fallback = "/tmp";
564 }
565 SAFE_STRNCPY(output_dir_buf, fallback, sizeof(output_dir_buf));
566 }
567
568 // Build log file name
569 if (g_output_dir_set) {
570 // When output directory is explicitly set, use unique naming with pid and tid
571 if (safe_snprintf(runtime->log_path, sizeof(runtime->log_path), "%s%c%s-%d-%llu.log", output_dir_buf, PATH_DELIM,
573 (unsigned long long)runtime->thread_id) >= (int)sizeof(runtime->log_path)) {
574 return false;
575 }
576 } else {
577 // Use simple "trace.log" name in current directory
578 if (safe_snprintf(runtime->log_path, sizeof(runtime->log_path), "%s%ctrace.log", output_dir_buf, PATH_DELIM) >=
579 (int)sizeof(runtime->log_path)) {
580 return false;
581 }
582 }
583 }
584
585 // Only validate auto-generated paths (custom paths already validated above)
586 if (!is_custom_path) {
587 char *validated_log_path = NULL;
588 asciichat_error_t validate_result =
589 path_validate_user_path(runtime->log_path, PATH_ROLE_LOG_FILE, &validated_log_path);
590 if (validate_result != ASCIICHAT_OK || validated_log_path == NULL) {
591 SAFE_FREE(validated_log_path);
592 log_warn("Failed to validate instrumentation log path: %s", runtime->log_path);
593 return false;
594 }
595 SAFE_STRNCPY(runtime->log_path, validated_log_path, sizeof(runtime->log_path));
596 runtime->log_path[sizeof(runtime->log_path) - 1] = '\0';
597 SAFE_FREE(validated_log_path);
598 }
599
600 // Find last path separator
601 const char *last_sep = strrchr(runtime->log_path, PATH_DELIM);
602 if (last_sep != NULL && last_sep != runtime->log_path) {
603 const size_t dir_path_len = (size_t)(last_sep - runtime->log_path);
604 char dir_path[PATH_MAX];
605 memcpy(dir_path, runtime->log_path, dir_path_len);
606 dir_path[dir_path_len] = '\0';
607 if (mkdir(dir_path, DIR_PERM_PRIVATE) != 0) {
608 if (errno != EEXIST) {
609 return false;
610 }
611 }
612 }
613
614 return true;
615}
616
617static int asciichat_instr_open_log_file(asciichat_instr_runtime_t *runtime) {
618 if (!asciichat_instr_build_log_path(runtime)) {
619 const char *debug_env = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_ECHO_STDERR");
620 if (debug_env && debug_env[0] == '1') {
621 fprintf(stderr, "ASCII_INSTR: Failed to build log path\n");
622 }
623 return -1;
624 }
625
626 // Check if this is a custom log file (env var was set)
627 const char *custom_log_file = SAFE_GETENV("ASCII_CHAT_DEBUG_SELF_SOURCE_CODE_LOG_FILE");
628 const bool is_custom_file = (custom_log_file != NULL && custom_log_file[0] != '\0');
629
630 // For custom files, allow appending to existing file (no O_EXCL)
631 // For auto-generated files, use O_EXCL to avoid conflicts
632 int flags;
633 if (is_custom_file) {
634 flags = PLATFORM_O_WRONLY | PLATFORM_O_CREAT | PLATFORM_O_APPEND | PLATFORM_O_BINARY;
635 } else {
636 flags = PLATFORM_O_WRONLY | PLATFORM_O_CREAT | PLATFORM_O_EXCL | PLATFORM_O_APPEND | PLATFORM_O_BINARY;
637 }
638
639 const char *debug_env = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_ECHO_STDERR");
640 if (debug_env && debug_env[0] == '1') {
641 fprintf(stderr, "ASCII_INSTR: Opening log file: %s (custom=%d)\n", runtime->log_path, is_custom_file);
642 }
643
644 const mode_t mode = S_IRUSR | S_IWUSR;
645 int fd = platform_open(runtime->log_path, flags, mode);
646 if (fd < 0) {
647 if (debug_env && debug_env[0] == '1') {
648 fprintf(stderr, "ASCII_INSTR: Failed to open log file: %s (errno=%d)\n", runtime->log_path, errno);
649 }
650 return -1;
651 }
652
653 if (debug_env && debug_env[0] == '1') {
654 fprintf(stderr, "ASCII_INSTR: Successfully opened log file: %s (fd=%d)\n", runtime->log_path, fd);
655 }
656
657 runtime->fd = fd;
658 return 0;
659}
660
661static void asciichat_instr_runtime_configure(asciichat_instr_runtime_t *runtime) {
662 runtime->filter_function_include = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_FUNCTION_INCLUDE");
663 runtime->filter_function_exclude = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_FUNCTION_EXCLUDE");
664
665 const char *only_env = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_ONLY");
666 asciichat_instr_parse_only_filters(runtime, only_env);
667
668#if ASCII_INSTR_SOURCE_PRINT_HAVE_REGEX
669 const char *include_regex = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_INCLUDE_REGEX");
670 if (include_regex != NULL && include_regex[0] != '\0') {
671 runtime->include_regex_valid = asciichat_instr_compile_regex(&runtime->include_regex, include_regex);
672 }
673
674 const char *exclude_regex = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_EXCLUDE_REGEX");
675 if (exclude_regex != NULL && exclude_regex[0] != '\0') {
676 runtime->exclude_regex_valid = asciichat_instr_compile_regex(&runtime->exclude_regex, exclude_regex);
677 }
678
679 const char *function_include_regex = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_FUNCTION_INCLUDE_REGEX");
680 if (function_include_regex != NULL && function_include_regex[0] != '\0') {
682 asciichat_instr_compile_regex(&runtime->function_include_regex, function_include_regex);
683 }
684
685 const char *function_exclude_regex = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_FUNCTION_EXCLUDE_REGEX");
686 if (function_exclude_regex != NULL && function_exclude_regex[0] != '\0') {
688 asciichat_instr_compile_regex(&runtime->function_exclude_regex, function_exclude_regex);
689 }
690#endif
691
692 const char *rate_env = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_RATE");
693 uint32_t rate_value = 0;
694 if (asciichat_instr_parse_positive_uint32(rate_env, &rate_value) && rate_value > 1U) {
695 runtime->rate = rate_value;
696 runtime->rate_enabled = true;
697 }
698
699 runtime->filters_enabled = (runtime->filter_include != NULL) || (runtime->filter_exclude != NULL) ||
700 (runtime->filter_thread != NULL) || (runtime->filter_function_include != NULL) ||
701 (runtime->filter_function_exclude != NULL)
703 || runtime->include_regex_valid || runtime->exclude_regex_valid ||
705#endif
706 || runtime->only_selectors.count > 0;
707}
708
709static bool asciichat_instr_env_is_enabled(const char *value) {
710 if (value == NULL) {
711 return false;
712 }
713
714 while (*value != '\0' && isspace((unsigned char)*value) != 0) {
715 value++;
716 }
717
718 if (*value == '\0') {
719 return false;
720 }
721
722 if (strcmp(value, STR_ZERO) == 0) {
723 return false;
724 }
725
726 char lowered[8];
727 size_t len = 0;
728 while (value[len] != '\0' && len < sizeof(lowered) - 1) {
729 lowered[len] = (char)tolower((unsigned char)value[len]);
730 len++;
731 }
732 lowered[len] = '\0';
733
734 return !(strcmp(lowered, STR_FALSE) == 0 || strcmp(lowered, STR_OFF) == 0 || strcmp(lowered, STR_NO) == 0);
735}
736
737static bool asciichat_instr_parse_positive_uint32(const char *value, uint32_t *out_value) {
738 if (value == NULL || out_value == NULL) {
739 return false;
740 }
741
742 while (*value != '\0' && isspace((unsigned char)*value) != 0) {
743 value++;
744 }
745
746 if (*value == '\0') {
747 return false;
748 }
749
750 errno = 0;
751 char *endptr = NULL;
752 unsigned long long parsed = strtoull(value, &endptr, 10);
753 if (errno != 0 || endptr == value || parsed == 0ULL || parsed > UINT32_MAX) {
754 return false;
755 }
756
757 *out_value = (uint32_t)parsed;
758 return true;
759}
760
761#if ASCII_INSTR_SOURCE_PRINT_HAVE_REGEX
762static bool asciichat_instr_compile_regex(regex_t *regex, const char *pattern) {
763 if (regex == NULL || pattern == NULL) {
764 return false;
765 }
766 int ret = regcomp(regex, pattern, REG_EXTENDED | REG_NOSUB);
767 return ret == 0;
768}
769#endif
770
771static void asciichat_instr_only_list_destroy(asciichat_instr_only_list_t *list) {
772 if (list == NULL) {
773 return;
774 }
775 if (list->items != NULL) {
776 for (size_t i = 0; i < list->count; ++i) {
777 asciichat_instr_only_selector_t *selector = &list->items[i];
778 SAFE_FREE(selector->pattern);
779 SAFE_FREE(selector->module);
780 }
781 SAFE_FREE(list->items);
782 }
783 list->count = 0;
784 list->capacity = 0;
785}
786
787static bool asciichat_instr_only_list_append(asciichat_instr_only_list_t *list, asciichat_instr_selector_type_t type,
788 const char *module, const char *pattern) {
789 if (list == NULL) {
790 return false;
791 }
792
795 if (pattern == NULL || pattern[0] == '\0') {
796 return false;
797 }
798 }
799
801 if (module == NULL || module[0] == '\0') {
802 return false;
803 }
804 }
805
806 if (list->count == list->capacity) {
807 size_t new_capacity = (list->capacity == 0) ? 4U : list->capacity * 2U;
809 SAFE_REALLOC(list->items, new_capacity * sizeof(*new_items), asciichat_instr_only_selector_t *);
810 if (new_items == NULL) {
811 return false; // SAFE_REALLOC already called FATAL, but satisfy analyzer
812 }
813 list->items = new_items;
814 list->capacity = new_capacity;
815 }
816
817 asciichat_instr_only_selector_t *selector = &list->items[list->count];
818 memset(selector, 0, sizeof(*selector));
819 selector->type = type;
820
821 if (module != NULL && module[0] != '\0') {
822 SAFE_STRDUP(selector->module, module);
823 }
824 if (pattern != NULL && pattern[0] != '\0') {
825 SAFE_STRDUP(selector->pattern, pattern);
826 }
827
828 list->count++;
829 return true;
830}
831
832static void asciichat_instr_trim(char *value) {
833 if (value == NULL) {
834 return;
835 }
836
837 char *start = value;
838 while (*start != '\0' && isspace((unsigned char)*start) != 0) {
839 start++;
840 }
841
842 char *end = start + strlen(start);
843 while (end > start && isspace((unsigned char)end[-1]) != 0) {
844 end--;
845 }
846
847 const size_t length = (size_t)(end - start);
848 if (start != value && length > 0U) {
849 memmove(value, start, length);
850 }
851 value[length] = '\0';
852}
853
854static bool asciichat_instr_parse_only_filters(asciichat_instr_runtime_t *runtime, const char *value) {
855 if (runtime == NULL) {
856 return false;
857 }
858
859 asciichat_instr_only_list_destroy(&runtime->only_selectors);
860
861 if (value == NULL || value[0] == '\0') {
862 return true;
863 }
864
865 char *mutable_value = NULL;
866 SAFE_STRDUP(mutable_value, value);
867 if (mutable_value == NULL) {
868 return false;
869 }
870
871 char *cursor = mutable_value;
872 while (cursor != NULL && *cursor != '\0') {
873 char *token_start = cursor;
874 while (*cursor != '\0' && *cursor != ',') {
875 cursor++;
876 }
877 if (*cursor == ',') {
878 *cursor = '\0';
879 cursor++;
880 } else {
881 cursor = NULL;
882 }
883
884 asciichat_instr_trim(token_start);
885 if (*token_start == '\0') {
886 continue;
887 }
888
889 char *equal_sign = strchr(token_start, '=');
890 if (equal_sign != NULL) {
891 *equal_sign = '\0';
892 char *kind = token_start;
893 char *spec = equal_sign + 1;
894 asciichat_instr_trim(kind);
895 asciichat_instr_trim(spec);
896 if (*spec == '\0') {
897 continue;
898 }
899
900 if (strcmp(kind, "func") == 0 || strcmp(kind, "function") == 0) {
901 (void)asciichat_instr_only_list_append(&runtime->only_selectors,
903 } else if (strcmp(kind, "module") == 0) {
904 char *module_value = spec;
905 char *module_pattern = strchr(module_value, ':');
906 if (module_pattern != NULL) {
907 *module_pattern = '\0';
908 module_pattern++;
909 asciichat_instr_trim(module_pattern);
910 }
911 asciichat_instr_trim(module_value);
912 if (*module_value == '\0') {
913 continue;
914 }
915 const char *pattern_part = (module_pattern != NULL && *module_pattern != '\0') ? module_pattern : NULL;
916 (void)asciichat_instr_only_list_append(&runtime->only_selectors, ASCII_INSTR_SOURCE_PRINT_SELECTOR_MODULE,
917 module_value, pattern_part);
918 } else {
919 // "file" kind or unknown kinds default to FILE_GLOB
920 (void)asciichat_instr_only_list_append(&runtime->only_selectors, ASCII_INSTR_SOURCE_PRINT_SELECTOR_FILE_GLOB,
921 NULL, spec);
922 }
923 continue;
924 }
925
926 char *colon = strchr(token_start, ':');
927 if (colon != NULL) {
928 *colon = '\0';
929 char *module_name = token_start;
930 char *pattern_part = colon + 1;
931 asciichat_instr_trim(module_name);
932 asciichat_instr_trim(pattern_part);
933 if (*module_name == '\0') {
934 continue;
935 }
936 const char *pattern_spec = (*pattern_part != '\0') ? pattern_part : NULL;
937 (void)asciichat_instr_only_list_append(&runtime->only_selectors, ASCII_INSTR_SOURCE_PRINT_SELECTOR_MODULE,
938 module_name, pattern_spec);
939 continue;
940 }
941
942 (void)asciichat_instr_only_list_append(&runtime->only_selectors, ASCII_INSTR_SOURCE_PRINT_SELECTOR_FILE_SUBSTRING,
943 NULL, token_start);
944 }
945
946 SAFE_FREE(mutable_value);
947 return true;
948}
949
950static bool asciichat_instr_match_glob(const char *pattern, const char *value) {
951 if (pattern == NULL || value == NULL) {
952 return false;
953 }
954
955 const char *p = pattern;
956 const char *v = value;
957 const char *star = NULL;
958 const char *match = NULL;
959
960 while (*v != '\0') {
961 if (*p == '*') {
962 star = p++;
963 match = v;
964 } else if (*p == '?' || *p == *v) {
965 p++;
966 v++;
967 } else if (star != NULL) {
968 p = star + 1;
969 match++;
970 v = match;
971 } else {
972 return false;
973 }
974 }
975
976 while (*p == '*') {
977 p++;
978 }
979
980 return *p == '\0';
981}
982
983static const char *asciichat_instr_basename(const char *path) {
984 if (path == NULL) {
985 return NULL;
986 }
987
988 // Find last path separator
989 const char *last_sep = strrchr(path, PATH_DELIM);
990 if (last_sep != NULL && last_sep[1] != '\0') {
991 return last_sep + 1;
992 }
993 return path;
994}
995
996static bool asciichat_instr_path_contains_module(const char *file_path, const char *module_name) {
997 if (file_path == NULL || module_name == NULL || module_name[0] == '\0') {
998 return false;
999 }
1000
1001 const size_t module_len = strlen(module_name);
1002 const char *cursor = file_path;
1003 while ((cursor = strstr(cursor, module_name)) != NULL) {
1004 bool left_ok = (cursor == file_path);
1005 if (!left_ok) {
1006 const char prev = cursor[-1];
1007 left_ok = (prev == PATH_DELIM);
1008 }
1009
1010 const char tail = cursor[module_len];
1011 bool right_ok = (tail == '\0' || tail == PATH_DELIM);
1012
1013 if (left_ok && right_ok) {
1014 return true;
1015 }
1016
1017 cursor = cursor + 1;
1018 }
1019
1020 return false;
1021}
1022
1023static bool asciichat_instr_only_selectors_match(const asciichat_instr_runtime_t *runtime, const char *file_path,
1024 const char *function_name) {
1025 const asciichat_instr_only_list_t *list = &runtime->only_selectors;
1026 if (list->count == 0) {
1027 return true;
1028 }
1029
1030 const char *base_name = asciichat_instr_basename(file_path);
1031 for (size_t i = 0; i < list->count; ++i) {
1032 const asciichat_instr_only_selector_t *selector = &list->items[i];
1033 switch (selector->type) {
1035 if (file_path != NULL && selector->pattern != NULL && strstr(file_path, selector->pattern) != NULL) {
1036 return true;
1037 }
1038 break;
1040 if (file_path != NULL && selector->pattern != NULL && asciichat_instr_match_glob(selector->pattern, file_path)) {
1041 return true;
1042 }
1043 break;
1045 if (function_name != NULL && selector->pattern != NULL &&
1046 asciichat_instr_match_glob(selector->pattern, function_name)) {
1047 return true;
1048 }
1049 break;
1051 if (file_path != NULL && selector->module != NULL &&
1052 asciichat_instr_path_contains_module(file_path, selector->module)) {
1053 if (selector->pattern == NULL ||
1054 (base_name != NULL && asciichat_instr_match_glob(selector->pattern, base_name))) {
1055 return true;
1056 }
1057 }
1058 break;
1059 default:
1060 break;
1061 }
1062 }
1063
1064 return false;
1065}
1066
1067static bool asciichat_instr_should_log(const asciichat_instr_runtime_t *runtime, const char *file_path,
1068 uint32_t line_number, const char *function_name) {
1069 if (!runtime->filters_enabled) {
1070 return true;
1071 }
1072
1073 if (runtime->filter_thread != NULL) {
1074 char tid_buf[32];
1075 safe_snprintf(tid_buf, sizeof(tid_buf), "%llu", (unsigned long long)runtime->thread_id);
1076 if (strstr(runtime->filter_thread, tid_buf) == NULL) {
1077 return false;
1078 }
1079 }
1080
1081 if (runtime->filter_include != NULL) {
1082 if (file_path == NULL || strstr(file_path, runtime->filter_include) == NULL) {
1083 return false;
1084 }
1085 }
1086
1087 if (runtime->filter_exclude != NULL && file_path != NULL) {
1088 if (strstr(file_path, runtime->filter_exclude) != NULL) {
1089 return false;
1090 }
1091 }
1092
1093#if ASCII_INSTR_SOURCE_PRINT_HAVE_REGEX
1094 if (runtime->include_regex_valid) {
1095 if (file_path == NULL || regexec(&runtime->include_regex, file_path, 0, NULL, 0) != 0) {
1096 return false;
1097 }
1098 }
1099
1100 if (runtime->exclude_regex_valid && file_path != NULL) {
1101 if (regexec(&runtime->exclude_regex, file_path, 0, NULL, 0) == 0) {
1102 return false;
1103 }
1104 }
1105#endif
1106
1107 if (runtime->filter_function_include != NULL) {
1108 if (function_name == NULL || strstr(function_name, runtime->filter_function_include) == NULL) {
1109 return false;
1110 }
1111 }
1112
1113 if (runtime->filter_function_exclude != NULL && function_name != NULL) {
1114 if (strstr(function_name, runtime->filter_function_exclude) != NULL) {
1115 return false;
1116 }
1117 }
1118
1119#if ASCII_INSTR_SOURCE_PRINT_HAVE_REGEX
1120 if (runtime->function_include_regex_valid) {
1121 if (function_name == NULL || regexec(&runtime->function_include_regex, function_name, 0, NULL, 0) != 0) {
1122 return false;
1123 }
1124 }
1125
1126 if (runtime->function_exclude_regex_valid && function_name != NULL) {
1127 if (regexec(&runtime->function_exclude_regex, function_name, 0, NULL, 0) == 0) {
1128 return false;
1129 }
1130 }
1131#endif
1132
1133 if (runtime->only_selectors.count > 0) {
1134 if (!asciichat_instr_only_selectors_match(runtime, file_path, function_name)) {
1135 return false;
1136 }
1137 }
1138
1139 (void)line_number;
1140 return true;
1141}
1142
1143static int asciichat_instr_write_full(int fd, const char *buffer, size_t len) {
1144 size_t total = 0;
1145 while (total < len) {
1146 ssize_t written = platform_write(fd, buffer + total, len - total);
1147 if (written < 0) {
1148 if (errno == EINTR) {
1149 continue;
1150 }
1151 return -1;
1152 }
1153 total += (size_t)written;
1154 }
1155 return 0;
1156}
void asciichat_instr_runtime_global_destroy(void)
bool asciichat_instr_coverage_enabled(void)
asciichat_instr_selector_type
@ ASCII_INSTR_SOURCE_PRINT_SELECTOR_FUNCTION_GLOB
@ ASCII_INSTR_SOURCE_PRINT_SELECTOR_FILE_GLOB
@ ASCII_INSTR_SOURCE_PRINT_SELECTOR_FILE_SUBSTRING
@ ASCII_INSTR_SOURCE_PRINT_SELECTOR_MODULE
#define posix_write
void asciichat_instr_runtime_destroy(asciichat_instr_runtime_t *runtime)
struct asciichat_instr_runtime asciichat_instr_runtime_t
Per-thread instrumentation runtime state.
void asciichat_instr_log_line(const char *file_path, uint32_t line_number, const char *function_name, const char *snippet, uint8_t is_macro_expansion)
#define ASCII_INSTR_SOURCE_PRINT_MAX_SNIPPET
struct asciichat_instr_only_selector asciichat_instr_only_selector_t
Selector for filtering instrumentation output by file, function, or module.
void asciichat_instr_log_pc(uintptr_t program_counter)
#define ASCII_INSTR_SOURCE_PRINT_MAX_LINE
#define ASCII_INSTR_SOURCE_PRINT_DEFAULT_BASENAME
enum asciichat_instr_selector_type asciichat_instr_selector_type_t
struct asciichat_instr_only_list asciichat_instr_only_list_t
Dynamic array of instrumentation selectors for filtering output.
#define ASCII_INSTR_SOURCE_PRINT_HAVE_REGEX
asciichat_instr_runtime_t * asciichat_instr_runtime_get(void)
char file_path[PLATFORM_MAX_PATH_LENGTH]
Definition mmap.c:39
bool path_is_absolute(const char *path)
Definition path.c:696
asciichat_error_t path_validate_user_path(const char *input, path_role_t role, char **normalized_out)
Definition path.c:974
char * expand_path(const char *path)
Definition path.c:471
char * platform_strdup(const char *s)
Dynamic array of instrumentation selectors for filtering output.
asciichat_instr_only_selector_t * items
Array of selectors.
size_t capacity
Allocated capacity.
size_t count
Number of active selectors.
Selector for filtering instrumentation output by file, function, or module.
asciichat_instr_selector_type_t type
Type of selector (file substring, glob, function glob, or module)
char * pattern
Pattern to match (file/function glob pattern)
Per-thread instrumentation runtime state.
const char * filter_exclude
File path substring to exclude (from env var)
bool function_exclude_regex_valid
Whether function_exclude_regex was successfully compiled.
asciichat_instr_only_list_t only_selectors
List of "only" selectors for filtering.
const char * filter_include
File path substring to include (from env var)
uint64_t call_counter
Total number of instrumentation calls.
char log_path[PATH_MAX]
Path to log file.
bool rate_enabled
Whether rate limiting is enabled.
regex_t function_exclude_regex
Compiled regex for function name exclusion.
uint64_t sequence
Sequence number for log entries.
regex_t exclude_regex
Compiled regex for file path exclusion.
bool function_include_regex_valid
Whether function_include_regex was successfully compiled.
uint32_t rate
Rate limiting: log every Nth call.
const char * filter_function_exclude
Function name substring to exclude (from env var)
const char * filter_function_include
Function name substring to include (from env var)
regex_t include_regex
Compiled regex for file path inclusion.
int fd
Log file descriptor (-1 if not open)
uint64_t thread_id
Thread ID.
regex_t function_include_regex
Compiled regex for function name inclusion.
const char * filter_thread
Thread ID filter (from env var)
bool stderr_fallback
Use stderr if log file can't be opened.
bool exclude_regex_valid
Whether exclude_regex was successfully compiled.
bool filters_enabled
Whether any filters are active.
bool include_regex_valid
Whether include_regex was successfully compiled.
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456
int ascii_tls_key_create(tls_key_t *key, void(*destructor)(void *))
Definition threading.c:89
int ascii_tls_set(tls_key_t key, void *value)
Definition threading.c:101
int mutex_init(mutex_t *mutex)
Definition threading.c:16
void * ascii_tls_get(tls_key_t key)
Definition threading.c:97
int ascii_tls_key_delete(tls_key_t key)
Definition threading.c:93
uint64_t asciichat_thread_current_id(void)
Definition threading.c:84
int mutex_destroy(mutex_t *mutex)
Definition threading.c:21
uint64_t time_get_ns(void)
Definition util/time.c:48
uint64_t time_get_realtime_ns(void)
Definition util/time.c:59
int format_duration_ns(double nanoseconds, char *buffer, size_t buffer_size)
Definition util/time.c:275
uint64_t time_elapsed_ns(uint64_t start_ns, uint64_t end_ns)
Definition util/time.c:90
bool platform_get_cwd(char *cwd, size_t path_size)
Definition util.c:108
int platform_close(int fd)
int platform_open(const char *pathname, int flags,...)
ssize_t platform_write(int fd, const void *buf, size_t count)
pid_t platform_get_pid(void)
Definition wasm/system.c:35