mirror of
https://github.com/portainer/portainer.git
synced 2025-08-02 04:15:28 +02:00
fix(logging): default to pretty logging [EE-4371] (#7847)
* fix(logging): default to pretty logging EE-4371 * feat(app/logs): prettify stack traces in JSON logs * feat(nomad/logs): prettify JSON logs in log viewer * feat(kubernetes/logs): prettigy JSON logs in log viewers * feat(app/logs): format and color zerolog prettified logs * fix(app/logs): pre-parse logs when they are double serialized Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com> Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>
This commit is contained in:
parent
ee5600b6af
commit
535a26412f
27 changed files with 935 additions and 279 deletions
63
app/docker/helpers/logHelper/colors/colors.ts
Normal file
63
app/docker/helpers/logHelper/colors/colors.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
// original code comes from https://www.npmjs.com/package/x256
|
||||
// only picking the used parts as there is no type definition
|
||||
// package is unmaintained and repository doesn't exist anymore
|
||||
|
||||
// colors scraped from
|
||||
// http://www.calmar.ws/vim/256-xterm-24bit-rgb-color-chart.html
|
||||
// %s/ *\d\+ \+#\([^ ]\+\)/\1\r/g
|
||||
|
||||
import rawColors from './rawColors.json';
|
||||
|
||||
export type RGBColor = [number, number, number];
|
||||
export type TextColor = string | undefined;
|
||||
|
||||
function hexToRGB(hex: string): RGBColor {
|
||||
const r = parseInt(hex.slice(0, 2), 16);
|
||||
const g = parseInt(hex.slice(2, 4), 16);
|
||||
const b = parseInt(hex.slice(4, 6), 16);
|
||||
return [r, g, b];
|
||||
}
|
||||
|
||||
export const colors = rawColors.map(hexToRGB);
|
||||
|
||||
export const FOREGROUND_COLORS_BY_ANSI: {
|
||||
[k: string]: RGBColor;
|
||||
} = {
|
||||
black: colors[0],
|
||||
red: colors[1],
|
||||
green: colors[2],
|
||||
yellow: colors[3],
|
||||
blue: colors[4],
|
||||
magenta: colors[5],
|
||||
cyan: colors[6],
|
||||
white: colors[7],
|
||||
brightBlack: colors[8],
|
||||
brightRed: colors[9],
|
||||
brightGreen: colors[10],
|
||||
brightYellow: colors[11],
|
||||
brightBlue: colors[12],
|
||||
brightMagenta: colors[13],
|
||||
brightCyan: colors[14],
|
||||
brightWhite: colors[15],
|
||||
};
|
||||
|
||||
export const BACKGROUND_COLORS_BY_ANSI: {
|
||||
[k: string]: RGBColor;
|
||||
} = {
|
||||
bgBlack: colors[0],
|
||||
bgRed: colors[1],
|
||||
bgGreen: colors[2],
|
||||
bgYellow: colors[3],
|
||||
bgBlue: colors[4],
|
||||
bgMagenta: colors[5],
|
||||
bgCyan: colors[6],
|
||||
bgWhite: colors[7],
|
||||
bgBrightBlack: colors[8],
|
||||
bgBrightRed: colors[9],
|
||||
bgBrightGreen: colors[10],
|
||||
bgBrightYellow: colors[11],
|
||||
bgBrightBlue: colors[12],
|
||||
bgBrightMagenta: colors[13],
|
||||
bgBrightCyan: colors[14],
|
||||
bgBrightWhite: colors[15],
|
||||
};
|
7
app/docker/helpers/logHelper/colors/index.ts
Normal file
7
app/docker/helpers/logHelper/colors/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export {
|
||||
type RGBColor,
|
||||
type TextColor,
|
||||
colors,
|
||||
FOREGROUND_COLORS_BY_ANSI,
|
||||
BACKGROUND_COLORS_BY_ANSI,
|
||||
} from './colors';
|
258
app/docker/helpers/logHelper/colors/rawColors.json
Normal file
258
app/docker/helpers/logHelper/colors/rawColors.json
Normal file
|
@ -0,0 +1,258 @@
|
|||
[
|
||||
"000000",
|
||||
"800000",
|
||||
"008000",
|
||||
"808000",
|
||||
"000080",
|
||||
"800080",
|
||||
"008080",
|
||||
"c0c0c0",
|
||||
"808080",
|
||||
"ff0000",
|
||||
"00ff00",
|
||||
"ffff00",
|
||||
"0000ff",
|
||||
"ff00ff",
|
||||
"00ffff",
|
||||
"ffffff",
|
||||
"000000",
|
||||
"00005f",
|
||||
"000087",
|
||||
"0000af",
|
||||
"0000d7",
|
||||
"0000ff",
|
||||
"005f00",
|
||||
"005f5f",
|
||||
"005f87",
|
||||
"005faf",
|
||||
"005fd7",
|
||||
"005fff",
|
||||
"008700",
|
||||
"00875f",
|
||||
"008787",
|
||||
"0087af",
|
||||
"0087d7",
|
||||
"0087ff",
|
||||
"00af00",
|
||||
"00af5f",
|
||||
"00af87",
|
||||
"00afaf",
|
||||
"00afd7",
|
||||
"00afff",
|
||||
"00d700",
|
||||
"00d75f",
|
||||
"00d787",
|
||||
"00d7af",
|
||||
"00d7d7",
|
||||
"00d7ff",
|
||||
"00ff00",
|
||||
"00ff5f",
|
||||
"00ff87",
|
||||
"00ffaf",
|
||||
"00ffd7",
|
||||
"00ffff",
|
||||
"5f0000",
|
||||
"5f005f",
|
||||
"5f0087",
|
||||
"5f00af",
|
||||
"5f00d7",
|
||||
"5f00ff",
|
||||
"5f5f00",
|
||||
"5f5f5f",
|
||||
"5f5f87",
|
||||
"5f5faf",
|
||||
"5f5fd7",
|
||||
"5f5fff",
|
||||
"5f8700",
|
||||
"5f875f",
|
||||
"5f8787",
|
||||
"5f87af",
|
||||
"5f87d7",
|
||||
"5f87ff",
|
||||
"5faf00",
|
||||
"5faf5f",
|
||||
"5faf87",
|
||||
"5fafaf",
|
||||
"5fafd7",
|
||||
"5fafff",
|
||||
"5fd700",
|
||||
"5fd75f",
|
||||
"5fd787",
|
||||
"5fd7af",
|
||||
"5fd7d7",
|
||||
"5fd7ff",
|
||||
"5fff00",
|
||||
"5fff5f",
|
||||
"5fff87",
|
||||
"5fffaf",
|
||||
"5fffd7",
|
||||
"5fffff",
|
||||
"870000",
|
||||
"87005f",
|
||||
"870087",
|
||||
"8700af",
|
||||
"8700d7",
|
||||
"8700ff",
|
||||
"875f00",
|
||||
"875f5f",
|
||||
"875f87",
|
||||
"875faf",
|
||||
"875fd7",
|
||||
"875fff",
|
||||
"878700",
|
||||
"87875f",
|
||||
"878787",
|
||||
"8787af",
|
||||
"8787d7",
|
||||
"8787ff",
|
||||
"87af00",
|
||||
"87af5f",
|
||||
"87af87",
|
||||
"87afaf",
|
||||
"87afd7",
|
||||
"87afff",
|
||||
"87d700",
|
||||
"87d75f",
|
||||
"87d787",
|
||||
"87d7af",
|
||||
"87d7d7",
|
||||
"87d7ff",
|
||||
"87ff00",
|
||||
"87ff5f",
|
||||
"87ff87",
|
||||
"87ffaf",
|
||||
"87ffd7",
|
||||
"87ffff",
|
||||
"af0000",
|
||||
"af005f",
|
||||
"af0087",
|
||||
"af00af",
|
||||
"af00d7",
|
||||
"af00ff",
|
||||
"af5f00",
|
||||
"af5f5f",
|
||||
"af5f87",
|
||||
"af5faf",
|
||||
"af5fd7",
|
||||
"af5fff",
|
||||
"af8700",
|
||||
"af875f",
|
||||
"af8787",
|
||||
"af87af",
|
||||
"af87d7",
|
||||
"af87ff",
|
||||
"afaf00",
|
||||
"afaf5f",
|
||||
"afaf87",
|
||||
"afafaf",
|
||||
"afafd7",
|
||||
"afafff",
|
||||
"afd700",
|
||||
"afd75f",
|
||||
"afd787",
|
||||
"afd7af",
|
||||
"afd7d7",
|
||||
"afd7ff",
|
||||
"afff00",
|
||||
"afff5f",
|
||||
"afff87",
|
||||
"afffaf",
|
||||
"afffd7",
|
||||
"afffff",
|
||||
"d70000",
|
||||
"d7005f",
|
||||
"d70087",
|
||||
"d700af",
|
||||
"d700d7",
|
||||
"d700ff",
|
||||
"d75f00",
|
||||
"d75f5f",
|
||||
"d75f87",
|
||||
"d75faf",
|
||||
"d75fd7",
|
||||
"d75fff",
|
||||
"d78700",
|
||||
"d7875f",
|
||||
"d78787",
|
||||
"d787af",
|
||||
"d787d7",
|
||||
"d787ff",
|
||||
"d7af00",
|
||||
"d7af5f",
|
||||
"d7af87",
|
||||
"d7afaf",
|
||||
"d7afd7",
|
||||
"d7afff",
|
||||
"d7d700",
|
||||
"d7d75f",
|
||||
"d7d787",
|
||||
"d7d7af",
|
||||
"d7d7d7",
|
||||
"d7d7ff",
|
||||
"d7ff00",
|
||||
"d7ff5f",
|
||||
"d7ff87",
|
||||
"d7ffaf",
|
||||
"d7ffd7",
|
||||
"d7ffff",
|
||||
"ff0000",
|
||||
"ff005f",
|
||||
"ff0087",
|
||||
"ff00af",
|
||||
"ff00d7",
|
||||
"ff00ff",
|
||||
"ff5f00",
|
||||
"ff5f5f",
|
||||
"ff5f87",
|
||||
"ff5faf",
|
||||
"ff5fd7",
|
||||
"ff5fff",
|
||||
"ff8700",
|
||||
"ff875f",
|
||||
"ff8787",
|
||||
"ff87af",
|
||||
"ff87d7",
|
||||
"ff87ff",
|
||||
"ffaf00",
|
||||
"ffaf5f",
|
||||
"ffaf87",
|
||||
"ffafaf",
|
||||
"ffafd7",
|
||||
"ffafff",
|
||||
"ffd700",
|
||||
"ffd75f",
|
||||
"ffd787",
|
||||
"ffd7af",
|
||||
"ffd7d7",
|
||||
"ffd7ff",
|
||||
"ffff00",
|
||||
"ffff5f",
|
||||
"ffff87",
|
||||
"ffffaf",
|
||||
"ffffd7",
|
||||
"ffffff",
|
||||
"080808",
|
||||
"121212",
|
||||
"1c1c1c",
|
||||
"262626",
|
||||
"303030",
|
||||
"3a3a3a",
|
||||
"444444",
|
||||
"4e4e4e",
|
||||
"585858",
|
||||
"606060",
|
||||
"666666",
|
||||
"767676",
|
||||
"808080",
|
||||
"8a8a8a",
|
||||
"949494",
|
||||
"9e9e9e",
|
||||
"a8a8a8",
|
||||
"b2b2b2",
|
||||
"bcbcbc",
|
||||
"c6c6c6",
|
||||
"d0d0d0",
|
||||
"dadada",
|
||||
"e4e4e4",
|
||||
"eeeeee"
|
||||
]
|
15
app/docker/helpers/logHelper/concatLogsToString.ts
Normal file
15
app/docker/helpers/logHelper/concatLogsToString.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { NEW_LINE_BREAKER } from '@/constants';
|
||||
|
||||
import { FormattedLine } from './types';
|
||||
|
||||
type FormatFunc = (line: FormattedLine) => string;
|
||||
|
||||
export function concatLogsToString(
|
||||
logs: FormattedLine[],
|
||||
formatFunc: FormatFunc = (line) => line.line
|
||||
) {
|
||||
return logs.reduce(
|
||||
(acc, formattedLine) => acc + formatFunc(formattedLine) + NEW_LINE_BREAKER,
|
||||
''
|
||||
);
|
||||
}
|
55
app/docker/helpers/logHelper/formatJSONLogs.ts
Normal file
55
app/docker/helpers/logHelper/formatJSONLogs.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { without } from 'lodash';
|
||||
|
||||
import { FormattedLine, 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
|
||||
): FormattedLine[] {
|
||||
const spans: Span[] = [];
|
||||
const lines: FormattedLine[] = [];
|
||||
let line = '';
|
||||
|
||||
const text = withTimestamps ? rawText.substring(TIMESTAMP_LENGTH) : rawText;
|
||||
|
||||
const json: JSONLogs = JSON.parse(text);
|
||||
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;
|
||||
}
|
141
app/docker/helpers/logHelper/formatLogs.ts
Normal file
141
app/docker/helpers/logHelper/formatLogs.ts
Normal file
|
@ -0,0 +1,141 @@
|
|||
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 (logs.includes('\\n')) {
|
||||
logs = JSON.parse(logs);
|
||||
}
|
||||
|
||||
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);
|
||||
if (
|
||||
(!withTimestamps && text.startsWith('{')) ||
|
||||
(withTimestamps && text.substring(TIMESTAMP_LENGTH).startsWith('{'))
|
||||
) {
|
||||
const lines = formatJSONLine(text, withTimestamps);
|
||||
formattedLogs.push(...lines);
|
||||
} else if (ZerologRegex.test(text)) {
|
||||
const lines = formatZerologLogs(text, withTimestamps);
|
||||
formattedLogs.push(...lines);
|
||||
} else {
|
||||
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 '';
|
||||
}
|
119
app/docker/helpers/logHelper/formatZerologLogs.ts
Normal file
119
app/docker/helpers/logHelper/formatZerologLogs.ts
Normal file
|
@ -0,0 +1,119 @@
|
|||
import {
|
||||
formatCaller,
|
||||
formatKeyValuePair,
|
||||
formatLevel,
|
||||
formatMessage,
|
||||
formatStackTrace,
|
||||
formatTime,
|
||||
} from './formatters';
|
||||
import {
|
||||
FormattedLine,
|
||||
JSONStackTrace,
|
||||
Level,
|
||||
Span,
|
||||
TIMESTAMP_LENGTH,
|
||||
} from './types';
|
||||
|
||||
const dateRegex = /(\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}[AP]M) /; // "2022/02/01 04:30AM "
|
||||
const levelRegex = /(\w{3}) /; // "INF " or "ERR "
|
||||
const callerRegex = /(.+?.go:\d+) /; // "path/to/file.go:line "
|
||||
const chevRegex = /> /; // "> "
|
||||
const messageAndPairsRegex = /(.*)/; // include the rest of the string in a separate group
|
||||
|
||||
const keyRegex = /(\S+=)/g; // ""
|
||||
|
||||
export const ZerologRegex = concatRegex(
|
||||
dateRegex,
|
||||
levelRegex,
|
||||
callerRegex,
|
||||
chevRegex,
|
||||
messageAndPairsRegex
|
||||
);
|
||||
|
||||
function concatRegex(...regs: RegExp[]) {
|
||||
const flags = Array.from(
|
||||
new Set(
|
||||
regs
|
||||
.map((r) => r.flags)
|
||||
.join('')
|
||||
.split('')
|
||||
)
|
||||
).join('');
|
||||
const source = regs.map((r) => r.source).join('');
|
||||
return new RegExp(source, flags);
|
||||
}
|
||||
|
||||
type Pair = {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export function formatZerologLogs(rawText: string, withTimestamps?: boolean) {
|
||||
const spans: Span[] = [];
|
||||
const lines: FormattedLine[] = [];
|
||||
let line = '';
|
||||
|
||||
const text = withTimestamps ? rawText.substring(TIMESTAMP_LENGTH) : rawText;
|
||||
|
||||
const [, date, level, caller, messageAndPairs] =
|
||||
text.match(ZerologRegex) || [];
|
||||
|
||||
const [message, pairs] = extractPairs(messageAndPairs);
|
||||
|
||||
line += formatTime(date, spans, line);
|
||||
line += formatLevel(level as Level, spans, line);
|
||||
line += formatCaller(caller, spans, line);
|
||||
line += formatMessage(message, spans, line, !!pairs.length);
|
||||
|
||||
let stackTrace: JSONStackTrace | undefined;
|
||||
const stackTraceIndex = pairs.findIndex((p) => p.key === 'stack_trace');
|
||||
|
||||
if (stackTraceIndex !== -1) {
|
||||
stackTrace = JSON.parse(pairs[stackTraceIndex].value);
|
||||
pairs.splice(stackTraceIndex);
|
||||
}
|
||||
|
||||
pairs.forEach(({ key, value }, idx) => {
|
||||
line += formatKeyValuePair(
|
||||
key,
|
||||
value,
|
||||
spans,
|
||||
line,
|
||||
idx === pairs.length - 1
|
||||
);
|
||||
});
|
||||
lines.push({ line, spans });
|
||||
|
||||
formatStackTrace(stackTrace, lines);
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
function extractPairs(messageAndPairs: string): [string, Pair[]] {
|
||||
const pairs: Pair[] = [];
|
||||
let [message, rawPairs] = messageAndPairs.split('|');
|
||||
|
||||
if (!messageAndPairs.includes('|') && !rawPairs) {
|
||||
rawPairs = message;
|
||||
message = '';
|
||||
}
|
||||
message = message.trim();
|
||||
rawPairs = rawPairs.trim();
|
||||
|
||||
const matches = [...rawPairs.matchAll(keyRegex)];
|
||||
|
||||
matches.forEach((m, idx) => {
|
||||
const rawKey = m[0];
|
||||
const key = rawKey.slice(0, -1);
|
||||
const start = m.index || 0;
|
||||
const end = idx !== matches.length - 1 ? matches[idx + 1].index : undefined;
|
||||
const value = (
|
||||
end
|
||||
? rawPairs.slice(start + rawKey.length, end)
|
||||
: rawPairs.slice(start + rawKey.length)
|
||||
).trim();
|
||||
pairs.push({ key, value });
|
||||
});
|
||||
|
||||
return [message, pairs];
|
||||
}
|
154
app/docker/helpers/logHelper/formatters.ts
Normal file
154
app/docker/helpers/logHelper/formatters.ts
Normal file
|
@ -0,0 +1,154 @@
|
|||
import { format } from 'date-fns';
|
||||
import { takeRight } from 'lodash';
|
||||
|
||||
import { Span, Level, Colors, JSONStackTrace, FormattedLine } from './types';
|
||||
|
||||
const spaceSpan: Span = { text: ' ' };
|
||||
|
||||
function logLevelToSpan(level: Level): Span {
|
||||
switch (level) {
|
||||
case 'debug':
|
||||
case 'DBG':
|
||||
return {
|
||||
fgColor: Colors.Grey,
|
||||
text: 'DBG',
|
||||
fontWeight: 'bold',
|
||||
};
|
||||
case 'info':
|
||||
case 'INF':
|
||||
return {
|
||||
fgColor: Colors.Green,
|
||||
text: 'INF',
|
||||
fontWeight: 'bold',
|
||||
};
|
||||
case 'warn':
|
||||
case 'WRN':
|
||||
return {
|
||||
fgColor: Colors.Yellow,
|
||||
text: 'WRN',
|
||||
fontWeight: 'bold',
|
||||
};
|
||||
case 'error':
|
||||
case 'ERR':
|
||||
return {
|
||||
fgColor: Colors.Red,
|
||||
text: 'ERR',
|
||||
fontWeight: 'bold',
|
||||
};
|
||||
default:
|
||||
return { text: level };
|
||||
}
|
||||
}
|
||||
|
||||
export function formatTime(
|
||||
time: number | string | undefined,
|
||||
spans: Span[],
|
||||
line: string
|
||||
) {
|
||||
let nl = line;
|
||||
if (time) {
|
||||
let date = '';
|
||||
if (typeof time === 'number') {
|
||||
date = format(new Date(time * 1000), 'Y/MM/dd hh:mmaa');
|
||||
} else {
|
||||
date = time;
|
||||
}
|
||||
spans.push({ fgColor: Colors.Grey, text: date }, spaceSpan);
|
||||
nl += `${date} `;
|
||||
}
|
||||
return nl;
|
||||
}
|
||||
|
||||
export function formatLevel(
|
||||
level: Level | undefined,
|
||||
spans: Span[],
|
||||
line: string
|
||||
) {
|
||||
let nl = line;
|
||||
if (level) {
|
||||
const levelSpan = logLevelToSpan(level);
|
||||
spans.push(levelSpan, spaceSpan);
|
||||
nl += `${levelSpan.text} `;
|
||||
}
|
||||
return nl;
|
||||
}
|
||||
|
||||
export function formatCaller(
|
||||
caller: string | undefined,
|
||||
spans: Span[],
|
||||
line: string
|
||||
) {
|
||||
let nl = line;
|
||||
if (caller) {
|
||||
const trim = takeRight(caller.split('/'), 2).join('/');
|
||||
spans.push(
|
||||
{ fgColor: Colors.Magenta, text: trim, fontWeight: 'bold' },
|
||||
spaceSpan
|
||||
);
|
||||
spans.push({ fgColor: Colors.Blue, text: '>' }, spaceSpan);
|
||||
nl += `${trim} > `;
|
||||
}
|
||||
return nl;
|
||||
}
|
||||
|
||||
export function formatMessage(
|
||||
message: string,
|
||||
spans: Span[],
|
||||
line: string,
|
||||
hasKeys: boolean
|
||||
) {
|
||||
let nl = line;
|
||||
if (message) {
|
||||
spans.push({ fgColor: Colors.Magenta, text: `${message}` }, spaceSpan);
|
||||
nl += `${message} `;
|
||||
|
||||
if (hasKeys) {
|
||||
spans.push({ fgColor: Colors.Magenta, text: `|` }, spaceSpan);
|
||||
nl += '| ';
|
||||
}
|
||||
}
|
||||
return nl;
|
||||
}
|
||||
|
||||
export function formatKeyValuePair(
|
||||
key: string,
|
||||
value: unknown,
|
||||
spans: Span[],
|
||||
line: string,
|
||||
isLastKey: boolean
|
||||
) {
|
||||
let nl = line;
|
||||
|
||||
spans.push(
|
||||
{ fgColor: Colors.Blue, text: `${key}=` },
|
||||
{
|
||||
fgColor: key === 'error' || key === 'ERR' ? Colors.Red : Colors.Magenta,
|
||||
text: value as string,
|
||||
}
|
||||
);
|
||||
if (!isLastKey) spans.push(spaceSpan);
|
||||
nl += `${key}=${value}${!isLastKey ? ' ' : ''}`;
|
||||
|
||||
return nl;
|
||||
}
|
||||
|
||||
export function formatStackTrace(
|
||||
stackTrace: JSONStackTrace | undefined,
|
||||
lines: FormattedLine[]
|
||||
) {
|
||||
if (stackTrace) {
|
||||
stackTrace.forEach(({ func, line: lineNumber, source }) => {
|
||||
const line = ` at ${func} (${source}:${lineNumber})`;
|
||||
const spans: Span[] = [
|
||||
spaceSpan,
|
||||
spaceSpan,
|
||||
spaceSpan,
|
||||
spaceSpan,
|
||||
{ text: 'at ', fgColor: Colors.Grey },
|
||||
{ text: func, fgColor: Colors.Red },
|
||||
{ text: `(${source}:${lineNumber})`, fgColor: Colors.Grey },
|
||||
];
|
||||
lines.push({ line, spans });
|
||||
});
|
||||
}
|
||||
}
|
2
app/docker/helpers/logHelper/index.ts
Normal file
2
app/docker/helpers/logHelper/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { formatLogs } from './formatLogs';
|
||||
export { concatLogsToString } from './concatLogsToString';
|
53
app/docker/helpers/logHelper/types.ts
Normal file
53
app/docker/helpers/logHelper/types.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { FontWeight } from 'xterm';
|
||||
|
||||
import { type TextColor } from './colors';
|
||||
|
||||
export type Token = string | number;
|
||||
|
||||
export type Level =
|
||||
| 'debug'
|
||||
| 'info'
|
||||
| 'warn'
|
||||
| 'error'
|
||||
| 'DBG'
|
||||
| 'INF'
|
||||
| 'WRN'
|
||||
| 'ERR';
|
||||
|
||||
export type JSONStackTrace = {
|
||||
func: string;
|
||||
line: string;
|
||||
source: string;
|
||||
}[];
|
||||
|
||||
export type JSONLogs = {
|
||||
[k: string]: unknown;
|
||||
time: number;
|
||||
level: Level;
|
||||
caller: string;
|
||||
message: string;
|
||||
stack_trace?: JSONStackTrace;
|
||||
};
|
||||
|
||||
export type Span = {
|
||||
fgColor?: TextColor;
|
||||
bgColor?: TextColor;
|
||||
text: string;
|
||||
fontWeight?: FontWeight;
|
||||
};
|
||||
|
||||
export type FormattedLine = {
|
||||
spans: Span[];
|
||||
line: string;
|
||||
};
|
||||
|
||||
export const TIMESTAMP_LENGTH = 31; // 30 for timestamp + 1 for trailing space
|
||||
|
||||
export const Colors = {
|
||||
Grey: 'var(--text-log-viewer-color-json-grey)',
|
||||
Magenta: 'var(--text-log-viewer-color-json-magenta)',
|
||||
Yellow: 'var(--text-log-viewer-color-json-yellow)',
|
||||
Green: 'var(--text-log-viewer-color-json-green)',
|
||||
Red: 'var(--text-log-viewer-color-json-red)',
|
||||
Blue: 'var(--text-log-viewer-color-json-blue)',
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue