Files
claude code agent da6933c218 refactor(logs): collapse no-op ternary, drop speculative export, fix stale comment (F8,F9,F10)
F8: formatJSONLogs plain-text fallback — both arms of `withTimestamps ? rawText
    : text` yield rawText (text === rawText when !withTimestamps), so use rawText.
F9: controllerLogsController comment referenced the old 'Live logs' label removed
    by F7 — update it to 'Auto-refresh logs'.
F10: stripHeadersFunc has no external importers — drop the speculative export.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 15:37:37 +03:00

175 lines
5.0 KiB
TypeScript

import tokenize from '@nxmix/tokenize-ansi';
import { FontWeight } from '@xterm/xterm';
import {
colors,
BACKGROUND_COLORS_BY_ANSI,
FOREGROUND_COLORS_BY_ANSI,
RGBColor,
} from './colors';
import { formatJSONLine } from './formatJSONLogs';
import { formatZerologLogs, ZerologRegex } from './formatZerologLogs';
import { nextLineId } from './lineId';
import {
Token,
Span,
TIMESTAMP_LENGTH,
FormattedLine,
FormattedLineContent,
} from './types';
type FormatOptions = {
stripHeaders?: boolean;
withTimestamps?: boolean;
splitter?: string;
};
const defaultOptions: FormatOptions = {
splitter: '\n',
};
export function formatLogs(
rawLogs: string,
{
stripHeaders,
withTimestamps,
splitter = '\n',
}: FormatOptions = defaultOptions
) {
let logs = rawLogs;
if (stripHeaders) {
logs = stripHeadersFunc(logs);
}
// if JSON logs come serialized 2 times, parse them once to unwrap them
if (logs.startsWith('"')) {
try {
logs = JSON.parse(logs);
} catch (error) {
// noop, throw error away if logs cannot be parsed
}
}
const tokens: Token[][] = tokenize(logs);
const formattedLogs: FormattedLineContent[] = [];
let fgColor: string | undefined;
let bgColor: string | undefined;
let fontWeight: FontWeight | undefined;
let line = '';
let spans: Span[] = [];
tokens.forEach((token) => {
const [type] = token;
const fgAnsi = FOREGROUND_COLORS_BY_ANSI[type];
const bgAnsi = BACKGROUND_COLORS_BY_ANSI[type];
if (fgAnsi) {
fgColor = cssColorFromRgb(fgAnsi);
} else if (type === 'moreColor') {
fgColor = extendedColorForToken(token);
} else if (type === 'fgDefault') {
fgColor = undefined;
} else if (bgAnsi) {
bgColor = cssColorFromRgb(bgAnsi);
} else if (type === 'bgMoreColor') {
bgColor = extendedColorForToken(token);
} else if (type === 'bgDefault') {
bgColor = undefined;
} else if (type === 'reset') {
fgColor = undefined;
bgColor = undefined;
fontWeight = undefined;
} else if (type === 'bold') {
fontWeight = 'bold';
} else if (type === 'normal') {
fontWeight = 'normal';
} else if (type === 'text') {
const tokenLines = (token[1] as string).split(splitter);
tokenLines.forEach((tokenLine, idx) => {
if (idx && line) {
formattedLogs.push({ line, spans });
line = '';
spans = [];
}
const text = stripEscapeCodes(tokenLine);
try {
if (
(!withTimestamps && text.startsWith('{')) ||
(withTimestamps && text.substring(TIMESTAMP_LENGTH).startsWith('{'))
) {
const lines = formatJSONLine(text, withTimestamps);
formattedLogs.push(...lines);
} else if (
(!withTimestamps && ZerologRegex.test(text)) ||
(withTimestamps &&
ZerologRegex.test(text.substring(TIMESTAMP_LENGTH)))
) {
const lines = formatZerologLogs(text, withTimestamps);
formattedLogs.push(...lines);
} else {
spans.push({ fgColor, bgColor, text, fontWeight });
line += text;
}
} catch (error) {
// in case parsing fails for whatever reason, push the raw logs and continue
spans.push({ fgColor, bgColor, text, fontWeight });
line += text;
}
});
}
});
if (line) {
formattedLogs.push({ line, spans });
}
// Assign the stable id centrally, once, here — so every line (plain, JSON,
// zerolog, stack-trace) gets a monotonic id regardless of which formatter
// produced it. Enables `track by log.id` in the viewer template.
return formattedLogs.map(
(content): FormattedLine => ({ id: nextLineId(), ...content })
);
}
// Strips Docker's 8-byte multiplexed-stream headers from a non-TTY log buffer:
// drops the leading header, then every header that follows a newline. This
// text-level strip is only safe on a fully-buffered body (the AngularJS polling
// services for container/service/task logs) where every line starts on a frame
// boundary. The live-stream path (logStream.ts) demuxes frames at the byte
// level instead and calls formatLogs WITHOUT `stripHeaders`.
function stripHeadersFunc(logs: string) {
return logs.substring(8).replace(/\r?\n(.{8})/g, '\n');
}
function stripEscapeCodes(logs: string) {
return logs.replace(
// eslint-disable-next-line no-control-regex
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
''
);
}
function cssColorFromRgb(rgb: RGBColor) {
const [r, g, b] = rgb;
return `rgb(${r}, ${g}, ${b})`;
}
// assuming types based on original JS implementation
// there is not much type definitions for the tokenize library
function extendedColorForToken(token: Token[]) {
const [, colorMode, colorRef] = token as [undefined, number, number];
if (colorMode === 2) {
return cssColorFromRgb(token.slice(2) as RGBColor);
}
if (colorMode === 5 && colors[colorRef]) {
return cssColorFromRgb(colors[colorRef]);
}
return '';
}