feat(web): colorize run logs by level and target

Parse each tracing line in the run logs panel and tint the timestamp,
level, target, and message separately. Errors and warnings now stand
out at a glance (coral/amber) while debug/trace and the surrounding
chrome recede. Original whitespace is preserved so the formatter's
column alignment is intact.
This commit is contained in:
Bryan Helmkamp 2026-04-27 16:06:02 -07:00
parent 94447f9da2
commit 4beee7358b
No known key found for this signature in database

View file

@ -57,6 +57,7 @@ function renderBody(logsQuery: ReturnType<typeof useRunLogs>) {
function LogPanel({ text }: { text: string }) {
const byteCount = new Blob([text]).size;
const lines = useMemo(() => text.split("\n"), [text]);
return (
<div className="rounded-md border border-line bg-panel-alt">
<div className="flex items-center justify-between gap-3 border-b border-line px-3 py-2">
@ -67,12 +68,60 @@ function LogPanel({ text }: { text: string }) {
</div>
</div>
<pre className="max-h-[70vh] overflow-auto whitespace-pre p-4 font-mono text-xs leading-5 text-fg-2">
{text}
{lines.map((line, i) => (
<LogLine key={i} line={line} trailingNewline={i < lines.length - 1} />
))}
</pre>
</div>
);
}
const LOG_LINE_RE =
/^(\S+)(\s+)(TRACE|DEBUG|INFO|WARN|ERROR)(\s+)(.*)$/;
const LEVEL_COLOR: Record<string, string> = {
ERROR: "text-coral",
WARN: "text-amber",
INFO: "text-teal-500",
DEBUG: "text-fg-3",
TRACE: "text-fg-muted",
};
function LogLine({ line, trailingNewline }: { line: string; trailingNewline: boolean }) {
const newline = trailingNewline ? "\n" : "";
const match = LOG_LINE_RE.exec(line);
if (!match) {
return <span>{line}{newline}</span>;
}
const [, timestamp, gap1, level, gap2, rest] = match;
return (
<span>
<span className="text-fg-muted">{timestamp}</span>
{gap1}
<span className={`font-semibold ${LEVEL_COLOR[level] ?? "text-fg-2"}`}>{level}</span>
{gap2}
<LogRest text={rest} />
{newline}
</span>
);
}
const LOG_REST_RE = /^([a-zA-Z_][\w:]*):(\s+)(.*)$/;
function LogRest({ text }: { text: string }) {
const match = LOG_REST_RE.exec(text);
if (!match) return <>{text}</>;
const [, target, gap, message] = match;
return (
<>
<span className="text-fg-3">{target}</span>
<span className="text-fg-muted">:</span>
{gap}
<span>{message}</span>
</>
);
}
function errorMessage(error: unknown): string | undefined {
return error instanceof Error ? error.message : undefined;
}