8ca0608b21
Foundation for append-only log rendering and HTTP log streaming. - FormattedLine gains a stable, monotonically increasing `id` (new lineId.ts sequence), assigned centrally in formatLogs. Internal formatters now return id-less FormattedLineContent. This lets the viewer use `track by log.id` so already-rendered rows are never re-bound (fixes the text-selection collapse). - formatJSONLine: runtime guard so a bare JSON string/array log line falls back to plain text instead of rendering Object.keys as `0=h 1=e ...`. - createLogStreamProcessor: stateful demuxer that buffers streamed text, emits only complete lines (carrying the partial remainder), and reuses formatLogs/stripHeadersFunc for Docker 8-byte frame demux. - Unit tests for the demuxer, stable-id assignment and the JSON guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
70 lines
1.9 KiB
TypeScript
70 lines
1.9 KiB
TypeScript
import { without } from 'lodash';
|
|
|
|
import {
|
|
FormattedLineContent,
|
|
Span,
|
|
JSONLogs,
|
|
TIMESTAMP_LENGTH,
|
|
} from './types';
|
|
import {
|
|
formatCaller,
|
|
formatKeyValuePair,
|
|
formatLevel,
|
|
formatMessage,
|
|
formatStackTrace,
|
|
formatTime,
|
|
} from './formatters';
|
|
|
|
function removeKnownKeys(keys: string[]) {
|
|
return without(keys, 'time', 'level', 'caller', 'message', 'stack_trace');
|
|
}
|
|
|
|
export function formatJSONLine(
|
|
rawText: string,
|
|
withTimestamps?: boolean
|
|
): FormattedLineContent[] {
|
|
const spans: Span[] = [];
|
|
const lines: FormattedLineContent[] = [];
|
|
let line = '';
|
|
|
|
const text = withTimestamps ? rawText.substring(TIMESTAMP_LENGTH) : rawText;
|
|
|
|
const parsed: unknown = JSON.parse(text);
|
|
// Only treat the line as a structured JSON log when it actually parses to a
|
|
// plain object. A line serialized as a bare JSON string (e.g. `"hello"`)
|
|
// parses to a string, whose `Object.keys` is `['0','1',...]` — which used to
|
|
// render as `0=h 1=e ...`. Fall back to the plain-text path in that case.
|
|
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
const plain = withTimestamps ? rawText : text;
|
|
return [{ line: plain, spans: [{ text: plain }] }];
|
|
}
|
|
const json = parsed as JSONLogs;
|
|
const { time, level, caller, message, stack_trace: stackTrace } = json;
|
|
const keys = removeKnownKeys(Object.keys(json));
|
|
|
|
if (withTimestamps) {
|
|
const timestamp = rawText.substring(0, TIMESTAMP_LENGTH);
|
|
spans.push({ text: timestamp });
|
|
line += `${timestamp} `;
|
|
}
|
|
line += formatTime(time, spans, line);
|
|
line += formatLevel(level, spans, line);
|
|
line += formatCaller(caller, spans, line);
|
|
line += formatMessage(message, spans, line, !!keys.length);
|
|
|
|
keys.forEach((key, idx) => {
|
|
line += formatKeyValuePair(
|
|
key,
|
|
json[key],
|
|
spans,
|
|
line,
|
|
idx === keys.length - 1
|
|
);
|
|
});
|
|
|
|
lines.push({ line, spans });
|
|
formatStackTrace(stackTrace, lines);
|
|
|
|
return lines;
|
|
}
|