portainer/app/docker/helpers/logHelper.js

224 lines
6.8 KiB
JavaScript

import tokenize from '@nxmix/tokenize-ansi';
import x256 from 'x256';
import { takeRight, without } from 'lodash';
import { format } from 'date-fns';
const FOREGROUND_COLORS_BY_ANSI = {
black: x256.colors[0],
red: x256.colors[1],
green: x256.colors[2],
yellow: x256.colors[3],
blue: x256.colors[4],
magenta: x256.colors[5],
cyan: x256.colors[6],
white: x256.colors[7],
brightBlack: x256.colors[8],
brightRed: x256.colors[9],
brightGreen: x256.colors[10],
brightYellow: x256.colors[11],
brightBlue: x256.colors[12],
brightMagenta: x256.colors[13],
brightCyan: x256.colors[14],
brightWhite: x256.colors[15],
};
const BACKGROUND_COLORS_BY_ANSI = {
bgBlack: x256.colors[0],
bgRed: x256.colors[1],
bgGreen: x256.colors[2],
bgYellow: x256.colors[3],
bgBlue: x256.colors[4],
bgMagenta: x256.colors[5],
bgCyan: x256.colors[6],
bgWhite: x256.colors[7],
bgBrightBlack: x256.colors[8],
bgBrightRed: x256.colors[9],
bgBrightGreen: x256.colors[10],
bgBrightYellow: x256.colors[11],
bgBrightBlue: x256.colors[12],
bgBrightMagenta: x256.colors[13],
bgBrightCyan: x256.colors[14],
bgBrightWhite: x256.colors[15],
};
const TIMESTAMP_LENGTH = 31; // 30 for timestamp + 1 for trailing space
angular.module('portainer.docker').factory('LogHelper', [
function LogHelperFactory() {
'use strict';
var helper = {};
function stripHeaders(logs) {
logs = logs.substring(8);
logs = logs.replace(/\r?\n(.{8})/g, '\n');
return logs;
}
function stripEscapeCodes(logs) {
return logs.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
}
function cssColorFromRgb(rgb) {
const [r, g, b] = rgb;
return `rgb(${r}, ${g}, ${b})`;
}
function extendedColorForToken(token) {
const colorMode = token[1];
if (colorMode === 2) {
return cssColorFromRgb(token.slice(2));
}
if (colorMode === 5 && x256.colors[token[2]]) {
return cssColorFromRgb(x256.colors[token[2]]);
}
return '';
}
// Return an array with each log including a line and styled spans for each entry.
// If the stripHeaders param is specified, it will strip the 8 first characters of each line.
// withTimestamps param is needed to find the start of JSON for Zerolog logs parsing
helper.formatLogs = function (logs, { stripHeaders: skipHeaders, withTimestamps }) {
if (skipHeaders) {
logs = stripHeaders(logs);
}
const tokens = tokenize(logs);
const formattedLogs = [];
let foregroundColor = null;
let backgroundColor = null;
let line = '';
let spans = [];
for (const token of tokens) {
const type = token[0];
if (FOREGROUND_COLORS_BY_ANSI[type]) {
foregroundColor = cssColorFromRgb(FOREGROUND_COLORS_BY_ANSI[type]);
} else if (type === 'moreColor') {
foregroundColor = extendedColorForToken(token);
} else if (type === 'fgDefault') {
foregroundColor = null;
} else if (BACKGROUND_COLORS_BY_ANSI[type]) {
backgroundColor = cssColorFromRgb(BACKGROUND_COLORS_BY_ANSI[type]);
} else if (type === 'bgMoreColor') {
backgroundColor = extendedColorForToken(token);
} else if (type === 'bgDefault') {
backgroundColor = null;
} else if (type === 'reset') {
foregroundColor = null;
backgroundColor = null;
} else if (type === 'text') {
const tokenLines = token[1].split('\n');
for (let i = 0; i < tokenLines.length; i++) {
if (i !== 0) {
formattedLogs.push({ line, spans });
line = '';
spans = [];
}
const text = stripEscapeCodes(tokenLines[i]);
if ((!withTimestamps && text.startsWith('{')) || (withTimestamps && text.substring(TIMESTAMP_LENGTH).startsWith('{'))) {
line += JSONToFormattedLine(text, spans, withTimestamps);
} else {
spans.push({ foregroundColor, backgroundColor, text });
line += text;
}
}
}
}
if (line) {
formattedLogs.push({ line, spans });
}
return formattedLogs;
};
return helper;
},
]);
const JSONColors = {
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)',
};
const spaceSpan = { text: ' ' };
function logLevelToSpan(level) {
switch (level) {
case 'debug':
return { foregroundColor: JSONColors.Grey, text: 'DBG', fontWeight: 'bold' };
case 'info':
return { foregroundColor: JSONColors.Green, text: 'INF', fontWeight: 'bold' };
case 'warn':
return { foregroundColor: JSONColors.Yellow, text: 'WRN', fontWeight: 'bold' };
case 'error':
return { foregroundColor: JSONColors.Red, text: 'ERR', fontWeight: 'bold' };
default:
return { text: level };
}
}
function JSONToFormattedLine(rawText, spans, withTimestamps) {
const text = withTimestamps ? rawText.substring(TIMESTAMP_LENGTH) : rawText;
const json = JSON.parse(text);
const { level, caller, message, time } = json;
let line = '';
if (withTimestamps) {
const timestamp = rawText.substring(0, TIMESTAMP_LENGTH);
spans.push({ text: timestamp });
line += `${timestamp}`;
}
if (time) {
const date = format(new Date(time * 1000), 'Y/MM/dd hh:mmaa');
spans.push({ foregroundColor: JSONColors.Grey, text: date }, spaceSpan);
line += `${date} `;
}
if (level) {
const levelSpan = logLevelToSpan(level);
spans.push(levelSpan, spaceSpan);
line += `${levelSpan.text} `;
}
if (caller) {
const trimmedCaller = takeRight(caller.split('/'), 2).join('/');
spans.push({ foregroundColor: JSONColors.Magenta, text: trimmedCaller, fontWeight: 'bold' }, spaceSpan);
spans.push({ foregroundColor: JSONColors.Blue, text: '>' }, spaceSpan);
line += `${trimmedCaller} > `;
}
const keys = without(Object.keys(json), 'time', 'level', 'caller', 'message');
if (message) {
spans.push({ foregroundColor: JSONColors.Magenta, text: `${message}` }, spaceSpan);
line += `${message} `;
if (keys.length) {
spans.push({ foregroundColor: JSONColors.Magenta, text: `|` }, spaceSpan);
line += '| ';
}
}
keys.forEach((key) => {
const value = json[key];
spans.push({ foregroundColor: JSONColors.Blue, text: `${key}=` });
spans.push({ foregroundColor: key === 'error' ? JSONColors.Red : JSONColors.Magenta, text: value });
spans.push(spaceSpan);
line += `${key}=${value} `;
});
return line;
}