Files
hiperiso/logging/hiperiso-log/report.c
T
2026-06-30 14:30:52 +03:00

637 lines
22 KiB
C

/*
* report.c - Generate analysis reports from boot logs.
*
* Reads serial.log, trace.bin, qemu.cmdline, and env.txt from a log
* directory, then writes report.json and report.txt into the same directory.
*
* Also derives a structured "analysis" (boot result, failure domain, graphics
* hints) from the serial console and writes a flat analysis.meta sidecar
* (KEY=VALUE) so the initramfs can fold derived fields into the canonical
* session manifest without a JSON parser.
*/
#include "report.h"
#include "trace_parser.h"
#include "serial_parser.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#define MAX_PATH 4096
#define MAX_CMDLINE 4096
#define MAX_ENV_ENTRIES 64
#define MAX_ENV_KEY 128
#define MAX_ENV_VALUE 512
#define MAX_LINEBUF (MAX_ENV_KEY + MAX_ENV_VALUE + 64)
typedef struct {
char key[MAX_ENV_KEY];
char value[MAX_ENV_VALUE];
} env_entry_t;
/* ── Derived analysis (Redbear telemetry) ───────────────────── */
typedef struct {
const char *boot_result; /* "success" | "failure" | "incomplete" | "no_serial" */
const char *failure_domain; /* "none" | "firmware" | "bootloader" | "kernel" |
* "hardware" | "userspace" | "unknown" */
int reached_login; /* saw login/complete stage */
int kernel_panic_count; /* error lines containing "panic" */
int graphics_drm_seen; /* any DRM/KMS line observed */
char graphics_drivers[256]; /* comma-separated unique driver names found */
char graphics_resolution[64]; /* first WxH token found, or "" */
} analysis_t;
static void trim_trailing_nl(char *s)
{
size_t len = strlen(s);
while (len > 0 && (s[len - 1] == '\n' || s[len - 1] == '\r'))
s[--len] = '\0';
}
static int read_env_file(const char *path, env_entry_t *entries,
int max_entries, int *count)
{
FILE *f = fopen(path, "r");
if (!f)
return -1;
*count = 0;
char buf[MAX_LINEBUF];
while (*count < max_entries && fgets(buf, sizeof(buf), f)) {
char *colon = strchr(buf, ':');
if (!colon)
continue;
*colon = '\0';
char *value = colon + 1;
while (*value == ' ' || *value == '\t')
value++;
trim_trailing_nl(value);
if (strlen(buf) == 0)
continue;
strncpy(entries[*count].key, buf, MAX_ENV_KEY - 1);
entries[*count].key[MAX_ENV_KEY - 1] = '\0';
strncpy(entries[*count].value, value, MAX_ENV_VALUE - 1);
entries[*count].value[MAX_ENV_VALUE - 1] = '\0';
(*count)++;
}
fclose(f);
return 0;
}
static int read_cmdline(const char *path, char *cmdline, size_t maxlen)
{
cmdline[0] = '\0';
FILE *f = fopen(path, "r");
if (!f)
return -1;
if (!fgets(cmdline, (int)maxlen, f)) {
fclose(f);
return -1;
}
fclose(f);
trim_trailing_nl(cmdline);
return 0;
}
static void json_escape(FILE *out, const char *str)
{
fputc('"', out);
while (*str) {
unsigned char ch = (unsigned char)*str;
switch (ch) {
case '"': fputs("\\\"", out); break;
case '\\': fputs("\\\\", out); break;
case '\n': fputs("\\n", out); break;
case '\r': fputs("\\r", out); break;
case '\t': fputs("\\t", out); break;
default:
if (ch < 0x20)
fprintf(out, "\\u%04x", ch);
else
fputc((int)ch, out);
}
str++;
}
fputc('"', out);
}
static void build_path(char *dst, size_t dstsz,
const char *dir, const char *file)
{
snprintf(dst, dstsz, "%s/%s", dir, file);
}
/* ── Analysis derivation ────────────────────────────────────── */
static const char *stage_to_domain(boot_stage_t s)
{
switch (s) {
case STAGE_FIRMWARE: return "firmware";
case STAGE_BOOTLOADER: return "bootloader";
case STAGE_KERNEL_INIT: return "kernel";
case STAGE_HARDWARE_INIT: return "hardware";
case STAGE_USERSPACE: return "userspace";
default: return "unknown";
}
}
/* Scan a single text line for a WxH resolution token (e.g. "1024x768").
* Returns 1 and fills out[] on success, 0 if none found. */
static int extract_resolution(const char *s, char *out, size_t outsz)
{
for (const char *p = s; *p; p++) {
if (*p < '0' || *p > '9')
continue;
const char *wstart = p;
while (*p >= '0' && *p <= '9')
p++;
int wlen = (int)(p - wstart);
if (wlen < 3 || wlen > 4)
continue;
if (*p != 'x' && *p != 'X')
continue;
p++;
const char *hstart = p;
while (*p >= '0' && *p <= '9')
p++;
int hlen = (int)(p - hstart);
if (hlen < 3 || hlen > 4)
continue;
snprintf(out, outsz, "%.*sx%.*s", wlen, wstart, hlen, hstart);
return 1;
}
return 0;
}
/* GPU driver names we can detect from serial console text. These are
* matched as substrings; only names literally present are reported. */
static const char *known_gpu_drivers[] = {
"i915", "amdgpu", "radeon", "nouveau", "nvidia",
"virtio_gpu", "virtio-gpu", "cirrus", "vmwgfx",
"mgag200", "simpledrm", "ast", "panfrost", NULL
};
static void scan_graphics(const serial_summary_t *s, analysis_t *a)
{
a->graphics_drivers[0] = '\0';
a->graphics_resolution[0] = '\0';
a->graphics_drm_seen = 0;
int first = 1;
for (serial_line_t *sl = s->lines; sl; sl = sl->next) {
const char *t = sl->text;
if (!t || !*t)
continue;
if (strstr(t, "[drm]") || strstr(t, "[DRM]") ||
strstr(t, "drm_kms_helper") || strstr(t, "DRM:") ||
strstr(t, "fb:"))
a->graphics_drm_seen = 1;
for (int i = 0; known_gpu_drivers[i]; i++) {
if (strstr(t, known_gpu_drivers[i]) &&
!strstr(a->graphics_drivers, known_gpu_drivers[i])) {
if (!first)
strncat(a->graphics_drivers, ",",
sizeof(a->graphics_drivers) -
strlen(a->graphics_drivers) - 1);
strncat(a->graphics_drivers, known_gpu_drivers[i],
sizeof(a->graphics_drivers) -
strlen(a->graphics_drivers) - 1);
first = 0;
}
}
if (a->graphics_resolution[0] == '\0')
extract_resolution(t, a->graphics_resolution,
sizeof(a->graphics_resolution));
}
}
static void derive_analysis(const serial_summary_t *s, int has_serial,
analysis_t *a)
{
memset(a, 0, sizeof(*a));
a->boot_result = "no_serial";
a->failure_domain = "unknown";
a->kernel_panic_count = 0;
if (!has_serial || !s)
return;
for (int i = 0; i < s->error_count; i++) {
if (strstr(s->error_messages[i], "panic") ||
strstr(s->error_messages[i], "Panic"))
a->kernel_panic_count++;
}
a->reached_login = (s->lines_per_stage[STAGE_LOGIN] > 0 ||
s->lines_per_stage[STAGE_COMPLETE] > 0);
/* Highest stage with observed lines (excluding error/unknown). */
boot_stage_t progressed = STAGE_UNKNOWN;
for (int i = STAGE_FIRMWARE; i < STAGE_ERROR; i++) {
if (s->lines_per_stage[i] > 0)
progressed = (boot_stage_t)i;
}
if (a->kernel_panic_count > 0) {
a->boot_result = "failure";
a->failure_domain = (progressed != STAGE_UNKNOWN)
? stage_to_domain(progressed) : "unknown";
} else if (a->reached_login) {
a->boot_result = (s->error_count > 0) ? "success_with_warnings" : "success";
a->failure_domain = "none";
} else if (s->error_count > 0) {
a->boot_result = "failure";
a->failure_domain = (progressed != STAGE_UNKNOWN)
? stage_to_domain(progressed) : "unknown";
} else {
a->boot_result = "incomplete";
a->failure_domain = (progressed != STAGE_UNKNOWN)
? stage_to_domain(progressed) : "unknown";
}
scan_graphics(s, a);
}
/* ── analysis.meta flat sidecar (consumed by initramfs init) ── */
static int write_analysis_meta(const char *path, const analysis_t *a,
const serial_summary_t *serial, int has_serial,
const trace_summary_t *trace, int has_trace)
{
FILE *out = fopen(path, "w");
if (!out)
return -1;
const char *stage = "unknown";
if (has_serial && serial->final_stage >= 0 &&
serial->final_stage < STAGE_COUNT)
stage = boot_stage_names[serial->final_stage];
/* stages_reached: comma-separated list of stages that had activity */
char stages_reached[384] = "";
if (has_serial) {
int first = 1;
for (int i = STAGE_FIRMWARE; i <= STAGE_COMPLETE; i++) {
if (serial->lines_per_stage[i] > 0) {
if (!first) {
strncat(stages_reached, ",",
sizeof(stages_reached) - strlen(stages_reached) - 1);
}
strncat(stages_reached, boot_stage_names[i],
sizeof(stages_reached) - strlen(stages_reached) - 1);
first = 0;
}
}
}
/* trace_duration_ms: from trace timestamps, already parsed */
uint64_t trace_duration_ms = 0;
if (has_trace && trace->last_timestamp > trace->first_timestamp)
trace_duration_ms = (trace->last_timestamp - trace->first_timestamp) / 1000000ULL;
fprintf(out, "BOOT_RESULT=%s\n", a->boot_result);
fprintf(out, "FAILURE_DOMAIN=%s\n", a->failure_domain);
fprintf(out, "BOOT_STAGE_FINAL=%s\n", stage);
fprintf(out, "STAGES_REACHED=%s\n", stages_reached);
fprintf(out, "REACHED_LOGIN=%d\n", a->reached_login ? 1 : 0);
fprintf(out, "KERNEL_PANIC_COUNT=%d\n", a->kernel_panic_count);
fprintf(out, "ERROR_COUNT=%d\n", has_serial ? serial->error_count : 0);
fprintf(out, "HAS_SERIAL=%d\n", has_serial ? 1 : 0);
fprintf(out, "HAS_TRACE=%d\n", has_trace ? 1 : 0);
fprintf(out, "BOOT_DURATION_MS=%" PRIu64 "\n",
has_serial ? serial->boot_duration_ms : (uint64_t)0);
fprintf(out, "TRACE_DURATION_MS=%" PRIu64 "\n", trace_duration_ms);
fprintf(out, "GRAPHICS_DRIVERS=%s\n", a->graphics_drivers);
fprintf(out, "GRAPHICS_RESOLUTION=%s\n", a->graphics_resolution);
fprintf(out, "GRAPHICS_DRM_SEEN=%d\n", a->graphics_drm_seen ? 1 : 0);
fclose(out);
return 0;
}
/* ── report.json ────────────────────────────────────────────── */
static int write_report_json(const char *path,
const env_entry_t *env, int env_count,
const char *cmdline,
const serial_summary_t *serial, int has_serial,
const trace_summary_t *trace, int has_trace,
const analysis_t *analysis)
{
FILE *out = fopen(path, "w");
if (!out)
return -1;
fprintf(out, "{\n");
fprintf(out, " \"environment\": {");
for (int i = 0; i < env_count; i++) {
if (i > 0)
fprintf(out, ",");
fprintf(out, "\n ");
json_escape(out, env[i].key);
fprintf(out, ": ");
json_escape(out, env[i].value);
}
fprintf(out, env_count > 0 ? "\n },\n" : "},\n");
fprintf(out, " \"qemu_cmdline\": ");
json_escape(out, cmdline);
fprintf(out, ",\n");
const char *stage = "unknown";
if (has_serial) {
if (serial->final_stage >= 0 && serial->final_stage < STAGE_COUNT)
stage = boot_stage_names[serial->final_stage];
}
fprintf(out, " \"boot_stage\": ");
json_escape(out, stage);
fprintf(out, ",\n");
fprintf(out, " \"boot_duration_ms\": %" PRIu64 ",\n",
has_serial ? serial->boot_duration_ms : (uint64_t)0);
fprintf(out, " \"errors\": [");
for (int i = 0; has_serial && i < serial->error_count; i++) {
if (i > 0)
fprintf(out, ", ");
json_escape(out, serial->error_messages[i]);
}
fprintf(out, "],\n");
if (has_serial) {
fprintf(out, " \"serial_summary\": {\n");
fprintf(out, " \"total_lines\": %zu,\n", serial->total_lines);
for (int i = 0; i < STAGE_COUNT; i++) {
fprintf(out, " \"lines_%s\": %zu%s\n",
boot_stage_names[i],
serial->lines_per_stage[i],
i < STAGE_COUNT - 1 ? "," : "");
}
fprintf(out, " },\n");
} else {
fprintf(out, " \"serial_summary\": null,\n");
}
if (has_trace) {
uint64_t trace_dur_ms = 0;
if (trace->last_timestamp > trace->first_timestamp)
trace_dur_ms = (trace->last_timestamp - trace->first_timestamp) / 1000000ULL;
fprintf(out, " \"trace_summary\": {\n");
fprintf(out, " \"total_events\": %" PRIu64 ",\n", trace->total_events);
fprintf(out, " \"disk_reads\": %" PRIu64 ",\n", trace->total_disk_reads);
fprintf(out, " \"disk_writes\": %" PRIu64 ",\n", trace->total_disk_writes);
fprintf(out, " \"serial_writes\": %" PRIu64 ",\n", trace->total_serial_writes);
fprintf(out, " \"first_timestamp_ns\": %" PRIu64 ",\n", trace->first_timestamp);
fprintf(out, " \"last_timestamp_ns\": %" PRIu64 ",\n", trace->last_timestamp);
fprintf(out, " \"duration_ms\": %" PRIu64 "\n", trace_dur_ms);
fprintf(out, " },\n");
} else {
fprintf(out, " \"trace_summary\": null,\n");
}
/* Derived analysis block (additive, always present). */
fprintf(out, " \"analysis\": {\n");
fprintf(out, " \"boot_result\": ");
json_escape(out, analysis->boot_result);
fprintf(out, ",\n");
fprintf(out, " \"failure_domain\": ");
json_escape(out, analysis->failure_domain);
fprintf(out, ",\n");
fprintf(out, " \"boot_stage_final\": ");
json_escape(out, stage);
fprintf(out, ",\n");
fprintf(out, " \"stages_reached\": [");
if (has_serial) {
int first_stg = 1;
for (int i = STAGE_FIRMWARE; i <= STAGE_COMPLETE; i++) {
if (serial->lines_per_stage[i] > 0) {
if (!first_stg)
fprintf(out, ", ");
json_escape(out, boot_stage_names[i]);
first_stg = 0;
}
}
}
fprintf(out, "],\n");
fprintf(out, " \"reached_login\": %s,\n",
analysis->reached_login ? "true" : "false");
fprintf(out, " \"kernel_panic_count\": %d,\n",
analysis->kernel_panic_count);
fprintf(out, " \"error_count\": %d,\n",
has_serial ? serial->error_count : 0);
fprintf(out, " \"has_serial\": %s,\n", has_serial ? "true" : "false");
fprintf(out, " \"has_trace\": %s,\n", has_trace ? "true" : "false");
fprintf(out, " \"boot_duration_ms\": %" PRIu64 ",\n",
has_serial ? serial->boot_duration_ms : (uint64_t)0);
{
uint64_t tdm = 0;
if (has_trace && trace->last_timestamp > trace->first_timestamp)
tdm = (trace->last_timestamp - trace->first_timestamp) / 1000000ULL;
fprintf(out, " \"trace_duration_ms\": %" PRIu64 ",\n", tdm);
}
fprintf(out, " \"graphics\": {\n");
fprintf(out, " \"drm_seen\": %s,\n",
analysis->graphics_drm_seen ? "true" : "false");
fprintf(out, " \"drivers\": [");
{
const char *p = analysis->graphics_drivers;
int first_drv = 1;
while (*p) {
const char *comma = strchr(p, ',');
size_t len = comma ? (size_t)(comma - p) : strlen(p);
if (len > 0) {
if (!first_drv)
fprintf(out, ", ");
fprintf(out, "\"%.*s\"", (int)len, p);
first_drv = 0;
}
if (!comma)
break;
p = comma + 1;
}
}
fprintf(out, "],\n");
fprintf(out, " \"resolution\": ");
json_escape(out, analysis->graphics_resolution);
fprintf(out, "\n");
fprintf(out, " }\n");
fprintf(out, " }\n");
fprintf(out, "}\n");
fclose(out);
return 0;
}
/* ── report.txt ─────────────────────────────────────────────── */
static int write_report_txt(const char *path,
const env_entry_t *env, int env_count,
const char *cmdline,
const serial_summary_t *serial, int has_serial,
const trace_summary_t *trace, int has_trace,
const analysis_t *analysis)
{
FILE *out = fopen(path, "w");
if (!out)
return -1;
fprintf(out, "========================================\n");
fprintf(out, " hiperiso Boot Analysis Report\n");
fprintf(out, "========================================\n\n");
fprintf(out, "--- Environment ---\n");
for (int i = 0; i < env_count; i++)
fprintf(out, " %-20s %s\n", env[i].key, env[i].value);
if (env_count == 0)
fprintf(out, " (not available)\n");
fprintf(out, "\n");
fprintf(out, "--- QEMU Command Line ---\n");
if (cmdline[0])
fprintf(out, " %s\n", cmdline);
else
fprintf(out, " (not available)\n");
fprintf(out, "\n");
if (has_serial) {
const char *stage = (serial->final_stage >= 0 &&
serial->final_stage < STAGE_COUNT)
? boot_stage_names[serial->final_stage]
: "unknown";
fprintf(out, "--- Serial Console Summary ---\n");
fprintf(out, " Total lines: %zu\n", serial->total_lines);
fprintf(out, " Final boot stage: %s\n", stage);
fprintf(out, " Boot duration: %" PRIu64 " ms\n",
serial->boot_duration_ms);
fprintf(out, "\n Lines per stage:\n");
for (int i = 0; i < STAGE_COUNT; i++)
fprintf(out, " %-20s %zu\n",
boot_stage_names[i], serial->lines_per_stage[i]);
if (serial->error_count > 0) {
fprintf(out, "\n Errors (%d):\n", serial->error_count);
for (int i = 0; i < serial->error_count; i++)
fprintf(out, " %s\n", serial->error_messages[i]);
}
fprintf(out, "\n");
} else {
fprintf(out, "--- Serial Console: NOT AVAILABLE ---\n\n");
}
if (has_trace) {
fprintf(out, "--- Trace Summary ---\n");
trace_print_text(trace, out);
} else {
fprintf(out, "--- Trace: NOT AVAILABLE ---\n");
}
fprintf(out, "\n--- Derived Analysis ---\n");
fprintf(out, " Boot result: %s\n", analysis->boot_result);
fprintf(out, " Failure domain: %s\n", analysis->failure_domain);
if (has_serial) {
fprintf(out, " Stage progression: ");
int first_stg = 1;
for (int i = STAGE_FIRMWARE; i <= STAGE_COMPLETE; i++) {
if (serial->lines_per_stage[i] > 0) {
fprintf(out, "%s%s",
first_stg ? "" : " \xe2\x86\x92 ",
boot_stage_names[i]);
first_stg = 0;
}
}
fprintf(out, "\n");
}
fprintf(out, " Reached login: %s\n",
analysis->reached_login ? "yes" : "no");
fprintf(out, " Kernel panics: %d\n", analysis->kernel_panic_count);
fprintf(out, " Error lines: %d\n",
has_serial ? serial->error_count : 0);
fprintf(out, " Graphics DRM seen: %s\n",
analysis->graphics_drm_seen ? "yes" : "no");
if (analysis->graphics_drivers[0])
fprintf(out, " Graphics drivers: %s\n", analysis->graphics_drivers);
if (analysis->graphics_resolution[0])
fprintf(out, " Graphics res: %s\n", analysis->graphics_resolution);
fprintf(out, "\n========================================\n");
fprintf(out, " End of Report\n");
fprintf(out, "========================================\n");
fclose(out);
return 0;
}
int report_generate(const char *log_dir)
{
if (!log_dir)
return -1;
char path[MAX_PATH];
env_entry_t env[MAX_ENV_ENTRIES];
int env_count = 0;
char cmdline[MAX_CMDLINE];
cmdline[0] = '\0';
build_path(path, sizeof(path), log_dir, "env.txt");
read_env_file(path, env, MAX_ENV_ENTRIES, &env_count);
build_path(path, sizeof(path), log_dir, "qemu.cmdline");
read_cmdline(path, cmdline, sizeof(cmdline));
serial_summary_t serial;
memset(&serial, 0, sizeof(serial));
build_path(path, sizeof(path), log_dir, "serial.log");
int has_serial = (serial_parse_file(path, &serial) == 0);
trace_summary_t trace;
memset(&trace, 0, sizeof(trace));
build_path(path, sizeof(path), log_dir, "trace.bin");
int has_trace = (trace_parse_file(path, &trace) == 0);
analysis_t analysis;
derive_analysis(has_serial ? &serial : NULL, has_serial, &analysis);
int rc = 0;
build_path(path, sizeof(path), log_dir, "report.json");
if (write_report_json(path, env, env_count, cmdline,
&serial, has_serial, &trace, has_trace,
&analysis) != 0)
rc = -1;
build_path(path, sizeof(path), log_dir, "report.txt");
if (write_report_txt(path, env, env_count, cmdline,
&serial, has_serial, &trace, has_trace,
&analysis) != 0)
rc = -1;
/* Flat sidecar for the initramfs to fold into the session manifest. */
build_path(path, sizeof(path), log_dir, "analysis.meta");
if (write_analysis_meta(path, &analysis, &serial, has_serial,
&trace, has_trace) != 0)
rc = -1;
if (has_serial)
serial_free_summary(&serial);
if (has_trace)
trace_free_summary(&trace);
return rc;
}