4#include <ascii-chat/common.h>
5#include <ascii-chat/log/logging.h>
6#include <ascii-chat/platform/util.h>
7#include <ascii-chat/tooling/panic/instrument_log.h>
9#include <ascii-chat/uthash/uthash.h>
10#include <ascii-chat/util/parsing.h>
23#include <ascii-chat/platform/windows/getopt.h>
83static const char *macro_flag_label(uint32_t flag) {
85 case ASCII_INSTR_SOURCE_PRINT_MACRO_EXPANSION:
87 case ASCII_INSTR_SOURCE_PRINT_MACRO_INVOCATION:
89 case ASCII_INSTR_SOURCE_PRINT_MACRO_NONE:
112 uint64_t *new_values = SAFE_REALLOC(list->
values, new_capacity *
sizeof(uint64_t), uint64_t *);
113 if (new_values == NULL) {
116 list->
values = new_values;
125 if (list == NULL || list->
count == 0) {
129 for (
size_t i = 0; i < list->
count; ++i) {
130 if (list->
values[i] == value) {
137static const char *resolve_default_log_dir(
void) {
138 const char *dir = SAFE_GETENV(
"ASCII_INSTR_SOURCE_PRINT_OUTPUT_DIR");
139 if (dir != NULL && dir[0] !=
'\0') {
142 dir = SAFE_GETENV(
"TMPDIR");
143 if (dir != NULL && dir[0] !=
'\0') {
146 dir = SAFE_GETENV(
"TEMP");
147 if (dir != NULL && dir[0] !=
'\0') {
150 dir = SAFE_GETENV(
"TMP");
151 if (dir != NULL && dir[0] !=
'\0') {
163 if (record == NULL) {
168 SAFE_FREE(record->
file);
172 memset(record, 0,
sizeof(*record));
178 HASH_ITER(hh, *entries, current, tmp) {
179 HASH_DEL(*entries, current);
180 free_record(¤t->
record);
185static char *duplicate_segment(
const char *begin,
size_t length) {
186 char *copy = SAFE_MALLOC(length + 1,
char *);
190 memcpy(copy, begin, length);
195static bool extract_token(
const char *line,
const char *key,
char **out_value) {
196 const char *position = strstr(line, key);
197 if (position == NULL) {
200 position += strlen(key);
201 const char *end = strchr(position,
' ');
203 end = line + strlen(line);
205 size_t length = (size_t)(end - position);
206 *out_value = duplicate_segment(position, length);
207 return *out_value != NULL;
210static bool extract_snippet(
const char *line,
char **out_value) {
211 const char *position = strstr(line,
"snippet=");
212 if (position == NULL) {
215 position += strlen(
"snippet=");
216 size_t length = strlen(position);
217 while (length > 0 && (position[length - 1] ==
'\n' || position[length - 1] ==
'\r')) {
220 *out_value = duplicate_segment(position, length);
221 return *out_value != NULL;
224static bool extract_uint64(
const char *line,
const char *key, uint64_t *out_value) {
225 const char *position = strstr(line, key);
226 if (position == NULL) {
229 position += strlen(key);
230 unsigned long long value = 0;
231 asciichat_error_t result =
parse_ulonglong(position, &value, 0, ULLONG_MAX);
232 if (result != ASCIICHAT_OK) {
235 *out_value = (uint64_t)value;
239static bool extract_uint32(
const char *line,
const char *key, uint32_t *out_value) {
240 uint64_t tmp_value = 0;
241 if (!extract_uint64(line, key, &tmp_value)) {
244 if (tmp_value > UINT32_MAX) {
247 *out_value = (uint32_t)tmp_value;
251static bool parse_log_line(
const char *line,
log_record_t *record) {
252 if (!extract_uint64(line,
"pid=", &record->
pid)) {
255 if (!extract_uint64(line,
"tid=", &record->
tid)) {
258 if (!extract_uint64(line,
"seq=", &record->
seq)) {
261 if (!extract_token(line,
"ts=", &record->
timestamp)) {
264 if (!extract_token(line,
"elapsed=", &record->
elapsed)) {
267 if (!extract_token(line,
"file=", &record->
file)) {
270 if (!extract_uint32(line,
"line=", &record->
line)) {
273 if (!extract_token(line,
"func=", &record->
function)) {
276 if (!extract_uint32(line,
"macro=", &record->
macro_flag)) {
279 if (!extract_snippet(line, &record->
snippet)) {
283 size_t len = strlen(line);
284 record->
raw_line = duplicate_segment(line, len);
301 if (!thread_filter_list_contains(&config->
threads, record->
tid)) {
310 HASH_FIND(hh, *entries, &record->
tid,
sizeof(uint64_t), entry);
315 HASH_ADD(hh, *entries,
thread_id,
sizeof(uint64_t), entry);
320 free_record(&entry->
record);
327static int compare_entries(
const void *lhs,
const void *rhs) {
330 if ((*a)->thread_id < (*b)->thread_id) {
333 if ((*a)->thread_id > (*b)->thread_id) {
340 size_t count = HASH_COUNT(*entries);
342 printf(
"No instrumentation records matched the given filters.\n");
350 HASH_ITER(hh, *entries, entry, tmp) {
351 sorted[index++] = entry;
354 qsort(sorted, count,
sizeof(*sorted), compare_entries);
356 printf(
"Latest instrumentation record per thread (%zu thread%s)\n", count, count == 1 ?
"" :
"s");
357 printf(
"======================================================================\n");
358 for (
size_t i = 0; i < count; ++i) {
365 printf(
"tid=%" PRIu64
" seq=%" PRIu64
" pid=%" PRIu64
"\n", record->
tid, record->
seq, record->
pid);
366 printf(
" timestamp : %s\n", record->
timestamp);
367 printf(
" elapsed : %s\n", record->
elapsed);
368 printf(
" location : %s:%u\n", record->
file != NULL ? record->
file :
"<unknown>", record->line);
369 printf(
" function : %s\n", record->
function != NULL ? record->
function :
"<unknown>");
371 printf(
" snippet : %s\n", record->
snippet != NULL ? record->
snippet :
"<missing>");
372 printf(
"----------------------------------------------------------------------\n");
378static void usage(FILE *stream,
const char *program) {
380 "Usage: %s [options]\n"
381 " --log-file <path> Single log file to analyze (e.g., trace.log)\n"
382 " --log-dir <path> Directory containing ascii-instr-*.log files (default: resolve from environment)\n"
383 " --thread <id> Limit to specific thread ID (repeatable)\n"
384 " --include <substr> Include records whose file path contains substring\n"
385 " --exclude <substr> Exclude records whose file path contains substring\n"
386 " --raw Emit raw log lines instead of formatted summary\n"
387 " --help Show this help and exit\n",
393static const char *prescan_log_file(
int argc,
char **argv) {
394 for (
int i = 1; i < argc; i++) {
395 if (strcmp(argv[i],
"--log-file") == 0 && i + 1 < argc) {
399 if (strncmp(argv[i],
"--log-file=", 11) == 0) {
407static bool is_simple_log_file_invocation(
int argc,
char **argv) {
409 if (argc == 3 && strcmp(argv[1],
"--log-file") == 0) {
412 if (argc == 2 && strncmp(argv[1],
"--log-file=", 11) == 0) {
418static bool parse_arguments(
int argc,
char **argv,
report_config_t *config) {
420 const char *log_file_arg = prescan_log_file(argc, argv);
421 if (log_file_arg != NULL) {
427 if (config->
log_file != NULL && is_simple_log_file_invocation(argc, argv)) {
431 static const struct option kLongOptions[] = {
432 {
"log-file", required_argument, NULL,
'f'}, {
"log-dir", required_argument, NULL,
'd'},
433 {
"thread", required_argument, NULL,
't'}, {
"include", required_argument, NULL,
'i'},
434 {
"exclude", required_argument, NULL,
'x'}, {
"raw", no_argument, NULL,
'r'},
435 {
"help", no_argument, NULL,
'h'}, {0, 0, 0, 0},
439 while ((option = getopt_long(argc, argv,
"", kLongOptions, NULL)) != -1) {
448 unsigned long long tid_value = 0;
449 asciichat_error_t result =
parse_ulonglong(optarg, &tid_value, 0, ULLONG_MAX);
450 if (result != ASCIICHAT_OK) {
451 log_error(
"Invalid thread id: %s", optarg);
454 if (!thread_filter_list_append(&config->
threads, (uint64_t)tid_value)) {
455 log_error(
"Failed to record thread filter");
470 usage(stdout, argv[0]);
473 usage(stderr, argv[0]);
479 log_error(
"Unexpected positional argument: %s", argv[optind]);
480 usage(stderr, argv[0]);
486 log_error(
"Cannot specify both --log-file and --log-dir");
492 config->
log_dir = resolve_default_log_dir();
500 if (handle == NULL) {
501 log_warn(
"Cannot open log file '%s': %s", path, SAFE_STRERROR(errno));
506 while (fgets(buffer,
sizeof(buffer), handle) != NULL) {
508 if (!parse_log_line(buffer, &record)) {
509 free_record(&record);
512 if (!record_matches_filters(config, &record)) {
513 free_record(&record);
516 update_entry(entries, &record);
524static bool build_windows_glob_pattern(
char *buffer,
size_t capacity,
const char *directory,
const char *suffix) {
525 if (buffer == NULL || directory == NULL || suffix == NULL) {
530 for (
const char *cursor = directory; *cursor !=
'\0'; ++cursor) {
531 char ch = *cursor ==
'/' ?
'\\' : *cursor;
532 if (length + 1 >= capacity) {
535 buffer[length++] = ch;
540 if (buffer[length - 1] !=
'\\') {
541 if (length + 1 >= capacity) {
544 buffer[length++] =
'\\';
546 const size_t suffix_length = strlen(suffix);
547 if (length + suffix_length >= capacity) {
550 memcpy(buffer + length, suffix, suffix_length + 1);
558 return process_file(config, config->
log_file, entries);
562 char pattern[MAX_PATH];
563 if (!build_windows_glob_pattern(pattern,
sizeof(pattern), config->
log_dir,
"ascii-instr-*.log")) {
564 log_error(
"Instrumentation log directory path is too long: %s", config->
log_dir);
568 struct _finddata_t data = {0};
569 intptr_t handle = _findfirst(pattern, &data);
571 log_error(
"Unable to open instrumentation log directory '%s': %s", config->
log_dir, SAFE_STRERROR(errno));
577 if ((data.attrib & _A_SUBDIR) != 0) {
580 size_t name_len = strlen(data.name);
581 if (name_len < 4 || strcmp(data.name + name_len - 4,
".log") != 0) {
585 char path_buffer[MAX_PATH];
586 int written =
safe_snprintf(path_buffer,
sizeof(path_buffer),
"%s/%s", config->
log_dir, data.name);
587 if (written < 0 || written >= (
int)
sizeof(path_buffer)) {
588 log_warn(
"Skipping path that exceeds buffer: %s/%s", config->
log_dir, data.name);
591 if (!process_file(config, path_buffer, entries)) {
594 }
while (_findnext(handle, &data) == 0);
599 DIR *directory = opendir(config->
log_dir);
600 if (directory == NULL) {
601 log_error(
"Unable to open instrumentation log directory '%s': %s", config->
log_dir, SAFE_STRERROR(errno));
605 struct dirent *entry = NULL;
606 while ((entry = readdir(directory)) != NULL) {
607 if (entry->d_name[0] ==
'.') {
610 if (strncmp(entry->d_name,
"ascii-instr-", strlen(
"ascii-instr-")) != 0) {
613 size_t name_length = strlen(entry->d_name);
614 if (name_length < 5 || strcmp(entry->d_name + name_length - 4,
".log") != 0) {
618 char path_buffer[PATH_MAX];
619 int written =
safe_snprintf(path_buffer,
sizeof(path_buffer),
"%s/%s", config->
log_dir, entry->d_name);
620 if (written < 0 || written >= (
int)
sizeof(path_buffer)) {
621 log_warn(
"Skipping path that exceeds buffer: %s/%s", config->
log_dir, entry->d_name);
625 process_file(config, path_buffer, entries);
633int main(
int argc,
char **argv) {
637 .include_filter = NULL,
638 .exclude_filter = NULL,
639 .threads = {.values = NULL, .count = 0, .capacity = 0},
640 .emit_raw_line =
false,
643 log_init(NULL, LOG_INFO,
false,
false);
645 int exit_code = EXIT_SUCCESS;
646 if (!parse_arguments(argc, argv, &config)) {
647 exit_code = ERROR_USAGE;
652 if (!collect_entries(&config, &entries)) {
653 exit_code = ERROR_GENERAL;
654 destroy_entries(&entries);
658 print_summary(&config, &entries);
659 destroy_entries(&entries);
662 thread_filter_list_destroy(&config.
threads);
void log_init(const char *filename, log_level_t level, bool force_stderr, bool use_mmap)
void usage(FILE *desc, asciichat_mode_t mode)
asciichat_error_t parse_ulonglong(const char *str, unsigned long long *out_value, unsigned long long min_value, unsigned long long max_value)
struct report_config report_config_t
Configuration for panic report generation.
struct thread_entry thread_entry_t
Hash table entry for thread-local last log record.
struct thread_filter_list thread_filter_list_t
Dynamic array of thread IDs for filtering instrumentation logs.
int main(int argc, char **argv)
struct log_record log_record_t
Parsed instrumentation log record.
Parsed instrumentation log record.
char * function
Function name.
char * file
Source file path.
char * snippet
Code snippet.
uint32_t macro_flag
Macro expansion flag.
uint32_t line
Source line number.
char * elapsed
Elapsed time since start.
char * raw_line
Original raw log line.
char * timestamp
Timestamp string.
uint64_t seq
Sequence number.
Configuration for panic report generation.
const char * exclude_filter
File path substring filter for exclusion.
const char * log_file
Single log file path (alternative to log_dir)
bool emit_raw_line
Whether to emit raw log lines in output.
thread_filter_list_t threads
Thread IDs to include in report.
const char * log_dir
Directory containing instrumentation log files.
const char * include_filter
File path substring filter for inclusion.
Hash table entry for thread-local last log record.
uint64_t thread_id
Thread ID (hash key)
log_record_t record
Last log record from this thread.
UT_hash_handle hh
uthash handle
Dynamic array of thread IDs for filtering instrumentation logs.
uint64_t * values
Array of thread IDs.
size_t count
Number of thread IDs.
size_t capacity
Allocated capacity.
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
FILE * platform_fopen(const char *filename, const char *mode)