You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
portainer/app/docker/helpers/logHelper/formatLogs.ts

157 lines
4.2 KiB

import tokenize from '@nxmix/tokenize-ansi';
import { FontWeight } from 'xterm';
import {
colors,
BACKGROUND_COLORS_BY_ANSI,
FOREGROUND_COLORS_BY_ANSI,
RGBColor,
} from './colors';
import { formatJSONLine } from './formatJSONLogs';
import { formatZerologLogs, ZerologRegex } from './formatZerologLogs';
import { Token, Span, TIMESTAMP_LENGTH, FormattedLine } 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: FormattedLine[] = [];
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 });
}
return formattedLogs;
}
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 '';
}