ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
report.c
Go to the documentation of this file.
1
11// SPDX-License-Identifier: MIT
12// Summarizer for ascii-chat instrumentation runtime logs
13
14#include <ascii-chat/common.h>
15#include <ascii-chat/log/log.h>
17
18#include <ascii-chat/uthash.h>
20
21#include <errno.h>
22#include <inttypes.h>
23#include <limits.h>
24#include <stdbool.h>
25#include <stdio.h>
26#include <stdlib.h>
27#include <string.h>
28
29#include <unistd.h>
30#ifdef _WIN32
32#else
33#include <getopt.h>
34#endif
35
44
56
76
88
89static const char *macro_flag_label(uint32_t flag) {
90 switch (flag) {
92 return "expansion";
94 return "invocation";
96 default:
97 return "none";
98 }
99}
100
101static void thread_filter_list_destroy(thread_filter_list_t *list) {
102 if (list == NULL) {
103 return;
104 }
105 SAFE_FREE(list->values);
106 list->values = NULL;
107 list->count = 0;
108 list->capacity = 0;
109}
110
111static bool thread_filter_list_append(thread_filter_list_t *list, uint64_t value) {
112 if (list == NULL) {
113 return false;
114 }
115
116 if (list->count == list->capacity) {
117 size_t new_capacity = list->capacity == 0 ? 4 : list->capacity * 2;
118 uint64_t *new_values = SAFE_REALLOC(list->values, new_capacity * sizeof(uint64_t), uint64_t *);
119 if (new_values == NULL) {
120 return false;
121 }
122 list->values = new_values;
123 list->capacity = new_capacity;
124 }
125
126 list->values[list->count++] = value;
127 return true;
128}
129
130static bool thread_filter_list_contains(const thread_filter_list_t *list, uint64_t value) {
131 if (list == NULL || list->count == 0) {
132 return true;
133 }
134
135 for (size_t i = 0; i < list->count; ++i) {
136 if (list->values[i] == value) {
137 return true;
138 }
139 }
140 return false;
141}
142
143static const char *resolve_default_log_dir(void) {
144 const char *dir = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_OUTPUT_DIR");
145 if (dir != NULL && dir[0] != '\0') {
146 return dir;
147 }
148 dir = SAFE_GETENV("TMPDIR");
149 if (dir != NULL && dir[0] != '\0') {
150 return dir;
151 }
152 dir = SAFE_GETENV("TEMP");
153 if (dir != NULL && dir[0] != '\0') {
154 return dir;
155 }
156 dir = SAFE_GETENV("TMP");
157 if (dir != NULL && dir[0] != '\0') {
158 return dir;
159 }
160 // Default to system temp directory
161 const char *tmp = SAFE_GETENV("TMPDIR");
162 if (!tmp)
163 tmp = SAFE_GETENV("TMP");
164 if (!tmp)
165 tmp = SAFE_GETENV("TEMP");
166#ifdef _WIN32
167 if (!tmp)
168 tmp = ".";
169#else
170 if (!tmp)
171 tmp = "/tmp";
172#endif
173 return tmp;
174}
175
176static void free_record(log_record_t *record) {
177 if (record == NULL) {
178 return;
179 }
180 SAFE_FREE(record->timestamp);
181 SAFE_FREE(record->elapsed);
182 SAFE_FREE(record->file);
183 SAFE_FREE(record->function);
184 SAFE_FREE(record->snippet);
185 SAFE_FREE(record->raw_line);
186 memset(record, 0, sizeof(*record));
187}
188
189static void destroy_entries(thread_entry_t **entries) {
190 thread_entry_t *current;
191 thread_entry_t *tmp;
192 HASH_ITER(hh, *entries, current, tmp) {
193 HASH_DEL(*entries, current);
194 free_record(&current->record);
195 SAFE_FREE(current);
196 }
197}
198
199static char *duplicate_segment(const char *begin, size_t length) {
200 char *copy = SAFE_MALLOC(length + 1, char *);
201 if (copy == NULL) {
202 return NULL;
203 }
204 memcpy(copy, begin, length);
205 copy[length] = '\0';
206 return copy;
207}
208
209static bool extract_token(const char *line, const char *key, char **out_value) {
210 const char *position = strstr(line, key);
211 if (position == NULL) {
212 return false;
213 }
214 position += strlen(key);
215 const char *end = strchr(position, ' ');
216 if (end == NULL) {
217 end = line + strlen(line);
218 }
219 size_t length = (size_t)(end - position);
220 *out_value = duplicate_segment(position, length);
221 return *out_value != NULL;
222}
223
224static bool extract_snippet(const char *line, char **out_value) {
225 const char *position = strstr(line, "snippet=");
226 if (position == NULL) {
227 return false;
228 }
229 position += strlen("snippet=");
230 size_t length = strlen(position);
231 while (length > 0 && (position[length - 1] == '\n' || position[length - 1] == '\r')) {
232 length--;
233 }
234 *out_value = duplicate_segment(position, length);
235 return *out_value != NULL;
236}
237
238static bool extract_uint64(const char *line, const char *key, uint64_t *out_value) {
239 const char *position = strstr(line, key);
240 if (position == NULL) {
241 return false;
242 }
243 position += strlen(key);
244 unsigned long long value = 0;
245 asciichat_error_t result = parse_ulonglong(position, &value, 0, ULLONG_MAX);
246 if (result != ASCIICHAT_OK) {
247 return false;
248 }
249 *out_value = (uint64_t)value;
250 return true;
251}
252
253static bool extract_uint32(const char *line, const char *key, uint32_t *out_value) {
254 uint64_t tmp_value = 0;
255 if (!extract_uint64(line, key, &tmp_value)) {
256 return false;
257 }
258 if (tmp_value > UINT32_MAX) {
259 return false;
260 }
261 *out_value = (uint32_t)tmp_value;
262 return true;
263}
264
265static bool parse_log_line(const char *line, log_record_t *record) {
266 if (!extract_uint64(line, "pid=", &record->pid)) {
267 return false;
268 }
269 if (!extract_uint64(line, "tid=", &record->tid)) {
270 return false;
271 }
272 if (!extract_uint64(line, "seq=", &record->seq)) {
273 return false;
274 }
275 if (!extract_token(line, "ts=", &record->timestamp)) {
276 return false;
277 }
278 if (!extract_token(line, "elapsed=", &record->elapsed)) {
279 return false;
280 }
281 if (!extract_token(line, "file=", &record->file)) {
282 return false;
283 }
284 if (!extract_uint32(line, "line=", &record->line)) {
285 return false;
286 }
287 if (!extract_token(line, "func=", &record->function)) {
288 return false;
289 }
290 if (!extract_uint32(line, "macro=", &record->macro_flag)) {
291 return false;
292 }
293 if (!extract_snippet(line, &record->snippet)) {
294 return false;
295 }
296
297 size_t len = strlen(line);
298 record->raw_line = duplicate_segment(line, len);
299 return record->raw_line != NULL;
300}
301
302static bool record_matches_filters(const report_config_t *config, const log_record_t *record) {
303 if (config->include_filter != NULL && config->include_filter[0] != '\0') {
304 if (record->file == NULL || strstr(record->file, config->include_filter) == NULL) {
305 return false;
306 }
307 }
308
309 if (config->exclude_filter != NULL && config->exclude_filter[0] != '\0') {
310 if (record->file != NULL && strstr(record->file, config->exclude_filter) != NULL) {
311 return false;
312 }
313 }
314
315 if (!thread_filter_list_contains(&config->threads, record->tid)) {
316 return false;
317 }
318
319 return true;
320}
321
322static void update_entry(thread_entry_t **entries, const log_record_t *record) {
323 thread_entry_t *entry = NULL;
324 HASH_FIND(hh, *entries, &record->tid, sizeof(uint64_t), entry);
325 if (entry == NULL) {
326 entry = SAFE_CALLOC(1, sizeof(*entry), thread_entry_t *);
327 entry->thread_id = record->tid;
328 entry->record = *record;
329 HASH_ADD(hh, *entries, thread_id, sizeof(uint64_t), entry);
330 return;
331 }
332
333 if (record->seq >= entry->record.seq) {
334 free_record(&entry->record);
335 entry->record = *record;
336 } else {
337 free_record((log_record_t *)record);
338 }
339}
340
341static int compare_entries(const void *lhs, const void *rhs) {
342 const thread_entry_t *const *a = lhs;
343 const thread_entry_t *const *b = rhs;
344 if ((*a)->thread_id < (*b)->thread_id) {
345 return -1;
346 }
347 if ((*a)->thread_id > (*b)->thread_id) {
348 return 1;
349 }
350 return 0;
351}
352
353static void print_summary(const report_config_t *config, thread_entry_t **entries) {
354 size_t count = HASH_COUNT(*entries);
355 if (count == 0) {
356 printf("No instrumentation records matched the given filters.\n");
357 return;
358 }
359
360 thread_entry_t **sorted = SAFE_CALLOC(count, sizeof(*sorted), thread_entry_t **);
361 size_t index = 0;
362 thread_entry_t *entry = NULL;
363 thread_entry_t *tmp = NULL;
364 HASH_ITER(hh, *entries, entry, tmp) {
365 sorted[index++] = entry;
366 }
367
368 qsort(sorted, count, sizeof(*sorted), compare_entries);
369
370 printf("Latest instrumentation record per thread (%zu thread%s)\n", count, count == 1 ? "" : "s");
371 printf("======================================================================\n");
372 for (size_t i = 0; i < count; ++i) {
373 const log_record_t *record = &sorted[i]->record;
374 if (config->emit_raw_line) {
375 printf("%s\n", record->raw_line);
376 continue;
377 }
378
379 printf("tid=%" PRIu64 " seq=%" PRIu64 " pid=%" PRIu64 "\n", record->tid, record->seq, record->pid);
380 printf(" timestamp : %s\n", record->timestamp);
381 printf(" elapsed : %s\n", record->elapsed);
382 printf(" location : %s:%u\n", record->file != NULL ? record->file : "<unknown>", record->line);
383 printf(" function : %s\n", record->function != NULL ? record->function : "<unknown>");
384 printf(" macro : %s (%u)\n", macro_flag_label(record->macro_flag), record->macro_flag);
385 printf(" snippet : %s\n", record->snippet != NULL ? record->snippet : "<missing>");
386 printf("----------------------------------------------------------------------\n");
387 }
388
389 SAFE_FREE(sorted);
390}
391
392static void usage(FILE *stream, const char *program) {
393 fprintf(stream,
394 "Usage: %s [options]\n"
395 " --log-file <path> Single log file to analyze (e.g., trace.log)\n"
396 " --log-dir <path> Directory containing ascii-instr-*.log files (default: resolve from environment)\n"
397 " --thread <id> Limit to specific thread ID (repeatable)\n"
398 " --include <substr> Include records whose file path contains substring\n"
399 " --exclude <substr> Exclude records whose file path contains substring\n"
400 " --raw Emit raw log lines instead of formatted summary\n"
401 " --help Show this help and exit\n",
402 program);
403}
404
405// Simple pre-scan for --log-file to avoid corrupted optarg from instrumented getopt
406// This is a workaround for stack corruption caused by panic instrumentation
407static const char *prescan_log_file(int argc, char **argv) {
408 for (int i = 1; i < argc; i++) {
409 if (strcmp(argv[i], "--log-file") == 0 && i + 1 < argc) {
410 return argv[i + 1];
411 }
412 // Handle --log-file=path format
413 if (strncmp(argv[i], "--log-file=", 11) == 0) {
414 return argv[i] + 11;
415 }
416 }
417 return NULL;
418}
419
420// Check if only --log-file is specified (to skip getopt entirely)
421static bool is_simple_log_file_invocation(int argc, char **argv) {
422 // Expected patterns: program --log-file path (argc=3) or program --log-file=path (argc=2)
423 if (argc == 3 && strcmp(argv[1], "--log-file") == 0) {
424 return true;
425 }
426 if (argc == 2 && strncmp(argv[1], "--log-file=", 11) == 0) {
427 return true;
428 }
429 return false;
430}
431
432static bool parse_arguments(int argc, char **argv, report_config_t *config) {
433 // Pre-scan for --log-file before getopt (workaround for instrumentation bug)
434 const char *log_file_arg = prescan_log_file(argc, argv);
435 if (log_file_arg != NULL) {
436 config->log_file = log_file_arg;
437 }
438
439 // If just --log-file with no other args, skip getopt entirely
440 // (instrumented getopt corrupts memory)
441 if (config->log_file != NULL && is_simple_log_file_invocation(argc, argv)) {
442 return true;
443 }
444
445 static const struct option kLongOptions[] = {
446 {"log-file", required_argument, NULL, 'f'}, {"log-dir", required_argument, NULL, 'd'},
447 {"thread", required_argument, NULL, 't'}, {"include", required_argument, NULL, 'i'},
448 {"exclude", required_argument, NULL, 'x'}, {"raw", no_argument, NULL, 'r'},
449 {"help", no_argument, NULL, 'h'}, {0, 0, 0, 0},
450 };
451
452 int option = 0;
453 while ((option = getopt_long(argc, argv, "", kLongOptions, NULL)) != -1) {
454 switch (option) {
455 case 'f':
456 // Already handled by prescan - skip (optarg may be corrupted)
457 break;
458 case 'd':
459 config->log_dir = optarg;
460 break;
461 case 't': {
462 unsigned long long tid_value = 0;
463 asciichat_error_t result = parse_ulonglong(optarg, &tid_value, 0, ULLONG_MAX);
464 if (result != ASCIICHAT_OK) {
465 log_error("Invalid thread id: %s", optarg);
466 return false;
467 }
468 if (!thread_filter_list_append(&config->threads, (uint64_t)tid_value)) {
469 log_error("Failed to record thread filter");
470 return false;
471 }
472 break;
473 }
474 case 'i':
475 config->include_filter = optarg;
476 break;
477 case 'x':
478 config->exclude_filter = optarg;
479 break;
480 case 'r':
481 config->emit_raw_line = true;
482 break;
483 case 'h':
484 usage(stdout, argv[0]);
485 return false;
486 default:
487 usage(stderr, argv[0]);
488 return false;
489 }
490 }
491
492 if (optind < argc) {
493 log_error("Unexpected positional argument: %s", argv[optind]);
494 usage(stderr, argv[0]);
495 return false;
496 }
497
498 // Validate --log-file and --log-dir are mutually exclusive
499 if (config->log_file != NULL && config->log_dir != NULL) {
500 log_error("Cannot specify both --log-file and --log-dir");
501 return false;
502 }
503
504 // Fall back to default log directory only if no log file specified
505 if (config->log_file == NULL && config->log_dir == NULL) {
506 config->log_dir = resolve_default_log_dir();
507 }
508
509 return true;
510}
511
512static bool process_file(const report_config_t *config, const char *path, thread_entry_t **entries) {
513 FILE *handle = platform_fopen("file_stream", path, "r");
514 if (handle == NULL) {
515 log_warn("Cannot open log file '%s': %s", path, SAFE_STRERROR(errno));
516 return false;
517 }
518
519 char buffer[8192];
520 while (fgets(buffer, sizeof(buffer), handle) != NULL) {
521 log_record_t record = {0};
522 if (!parse_log_line(buffer, &record)) {
523 free_record(&record);
524 continue;
525 }
526 if (!record_matches_filters(config, &record)) {
527 free_record(&record);
528 continue;
529 }
530 update_entry(entries, &record);
531 }
532
533 fclose(handle);
534 return true;
535}
536
542
543static bool collect_entry_cb(const platform_dir_entry_t *entry, void *user_data) {
544 collect_ctx_t *ctx = (collect_ctx_t *)user_data;
545 if (entry->is_dir)
546 return true;
547
548 // Filter: must start with "ascii-instr-" and end with ".log"
549 if (strncmp(entry->name, "ascii-instr-", strlen("ascii-instr-")) != 0)
550 return true;
551 size_t name_len = strlen(entry->name);
552 if (name_len < 5 || strcmp(entry->name + name_len - 4, ".log") != 0)
553 return true;
554
555 char path_buffer[PLATFORM_MAX_PATH_LENGTH];
556 int written = safe_snprintf(path_buffer, sizeof(path_buffer), "%s/%s", ctx->config->log_dir, entry->name);
557 if (written < 0 || written >= (int)sizeof(path_buffer)) {
558 log_warn("Skipping path that exceeds buffer: %s/%s", ctx->config->log_dir, entry->name);
559 return true;
560 }
561
562 if (!process_file(ctx->config, path_buffer, ctx->entries)) {
563 ctx->success = false;
564 }
565 return true;
566}
567
568static bool collect_entries(const report_config_t *config, thread_entry_t **entries) {
569 // If a single log file is specified, just process that file
570 if (config->log_file != NULL) {
571 return process_file(config, config->log_file, entries);
572 }
573
574 collect_ctx_t ctx = {.config = config, .entries = entries, .success = true};
575 asciichat_error_t err = platform_dir_foreach(config->log_dir, collect_entry_cb, &ctx);
576 if (err != ASCIICHAT_OK) {
577 log_error("Unable to open instrumentation log directory '%s'", config->log_dir);
578 return false;
579 }
580 return ctx.success;
581}
582
583int main(int argc, char **argv) {
584 report_config_t config = {
585 .log_dir = NULL,
586 .log_file = NULL,
587 .include_filter = NULL,
588 .exclude_filter = NULL,
589 .threads = {.values = NULL, .count = 0, .capacity = 0},
590 .emit_raw_line = false,
591 };
592
593 log_init(NULL, LOG_INFO, false, false);
594
595 int exit_code = EXIT_SUCCESS;
596 if (!parse_arguments(argc, argv, &config)) {
597 exit_code = ERROR_USAGE;
598 goto cleanup;
599 }
600
601 thread_entry_t *entries = NULL;
602 if (!collect_entries(&config, &entries)) {
603 exit_code = ERROR_GENERAL;
604 destroy_entries(&entries);
605 goto cleanup;
606 }
607
608 print_summary(&config, &entries);
609 destroy_entries(&entries);
610
611cleanup:
612 thread_filter_list_destroy(&config.threads);
613 log_destroy();
614 return exit_code;
615}
int thread_id
⚙️ Common definitions, error codes, macros, and types shared throughout the application
Cross-platform error handling utilities.
#define SAFE_REALLOC(ptr, size, cast)
Definition common.h:284
unsigned int uint32_t
Definition common.h:58
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
#define SAFE_GETENV(name)
Definition common.h:434
#define SAFE_STRERROR(errnum)
Definition common.h:465
#define SAFE_CALLOC(count, size, cast)
Definition common.h:274
unsigned long long uint64_t
Definition common.h:59
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_GENERAL
Definition error_codes.h:52
@ ERROR_USAGE
Definition error_codes.h:53
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
void log_destroy(void)
Destroy the logging system and close log file.
Definition log/log.c:624
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
void log_init(const char *filename, log_level_t level, bool force_stderr, bool use_mmap)
Initialize the logging system.
Definition log/log.c:533
void usage(FILE *desc, asciichat_mode_t mode)
Print usage information for a specific mode.
@ ASCII_INSTR_SOURCE_PRINT_MACRO_INVOCATION
@ ASCII_INSTR_SOURCE_PRINT_MACRO_EXPANSION
@ ASCII_INSTR_SOURCE_PRINT_MACRO_NONE
asciichat_error_t platform_dir_foreach(const char *path, platform_dir_foreach_cb callback, void *user_data)
Iterate over entries in a directory.
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:148
FILE * platform_fopen(const char *name, const char *filename, const char *mode)
Safe file open stream (fopen replacement)
int errno
🔍 Debug instrumentation logging runtime for ascii-chat line tracing
Application limits and constraints.
📝 Logging API with multiple log levels and terminal output control
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)
Definition report.c:583
struct log_record log_record_t
Parsed instrumentation log record.
ClangTool/LibTooling compatibility shim for stdbool.h.
const report_config_t * config
Definition report.c:538
thread_entry_t ** entries
Definition report.c:539
bool success
Definition report.c:540
Parsed instrumentation log record.
Definition report.c:63
uint64_t pid
Process ID.
Definition report.c:64
char * function
Function name.
Definition report.c:71
char * file
Source file path.
Definition report.c:69
char * snippet
Code snippet.
Definition report.c:73
uint32_t macro_flag
Macro expansion flag.
Definition report.c:72
uint32_t line
Source line number.
Definition report.c:70
char * elapsed
Elapsed time since start.
Definition report.c:68
char * raw_line
Original raw log line.
Definition report.c:74
uint64_t tid
Thread ID.
Definition report.c:65
char * timestamp
Timestamp string.
Definition report.c:67
uint64_t seq
Sequence number.
Definition report.c:66
Directory entry information for platform_dir_foreach()
Definition filesystem.h:409
const char * name
Definition filesystem.h:410
bool is_dir
Definition filesystem.h:411
Configuration for panic report generation.
Definition report.c:48
const char * exclude_filter
File path substring filter for exclusion.
Definition report.c:52
const char * log_file
Single log file path (alternative to log_dir)
Definition report.c:50
bool emit_raw_line
Whether to emit raw log lines in output.
Definition report.c:54
thread_filter_list_t threads
Thread IDs to include in report.
Definition report.c:53
const char * log_dir
Directory containing instrumentation log files.
Definition report.c:49
const char * include_filter
File path substring filter for inclusion.
Definition report.c:51
Hash table entry for thread-local last log record.
Definition report.c:83
uint64_t thread_id
Thread ID (hash key)
Definition report.c:84
log_record_t record
Last log record from this thread.
Definition report.c:85
UT_hash_handle hh
uthash handle
Definition report.c:86
Dynamic array of thread IDs for filtering instrumentation logs.
Definition report.c:39
uint64_t * values
Array of thread IDs.
Definition report.c:40
size_t count
Number of thread IDs.
Definition report.c:41
size_t capacity
Allocated capacity.
Definition report.c:42
#define PLATFORM_MAX_PATH_LENGTH
Definition system.c:69
#define LOG_INFO
Definition types.h:40
asciichat_error_t parse_ulonglong(const char *str, unsigned long long *out_value, unsigned long long min_value, unsigned long long max_value)
Parse unsigned long long integer with range validation.
🔍 Safe Parsing Utilities