feat(app/logs): format Zerolog in logs viewer [EE-4226] (#7685)

* feat(app/logs): format Zerolog in logs viewer

* fix(app/logs): trim caller to only last 2 segments
pull/7715/head
LP B 2022-09-22 00:34:58 +02:00 committed by GitHub
parent 6063f368ea
commit 1b0db4971f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 120 additions and 9 deletions

View File

@ -88,6 +88,13 @@
--BE-only: var(--ui-warning-7);
--text-log-viewer-color-json-grey: var(--text-log-viewer-color);
--text-log-viewer-color-json-magenta: var(--text-log-viewer-color);
--text-log-viewer-color-json-yellow: var(--text-log-viewer-color);
--text-log-viewer-color-json-green: var(--text-log-viewer-color);
--text-log-viewer-color-json-red: var(--text-log-viewer-color);
--text-log-viewer-color-json-blue: var(--text-log-viewer-color);
/* Default Theme */
--bg-card-color: var(--white-color);
--bg-main-color: var(--white-color);
@ -265,6 +272,13 @@
/* Dark Theme */
[theme='dark'] {
--text-log-viewer-color-json-grey: var(--text-log-viewer-color);
--text-log-viewer-color-json-magenta: var(--text-log-viewer-color);
--text-log-viewer-color-json-yellow: var(--text-log-viewer-color);
--text-log-viewer-color-json-green: var(--text-log-viewer-color);
--text-log-viewer-color-json-red: var(--text-log-viewer-color);
--text-log-viewer-color-json-blue: var(--text-log-viewer-color);
--bg-body-color: var(--grey-2);
--bg-btn-default-color: var(--grey-3);
--bg-blocklist-hover-color: var(--ui-gray-iron-10);
@ -445,6 +459,13 @@
/* High Contrast Theme */
[theme='highcontrast'] {
--text-log-viewer-color-json-grey: var(--text-log-viewer-color);
--text-log-viewer-color-json-magenta: var(--text-log-viewer-color);
--text-log-viewer-color-json-yellow: var(--text-log-viewer-color);
--text-log-viewer-color-json-green: var(--text-log-viewer-color);
--text-log-viewer-color-json-red: var(--text-log-viewer-color);
--text-log-viewer-color-json-blue: var(--text-log-viewer-color);
--bg-card-color: var(--black-color);
--bg-main-color: var(--black-color);
--bg-body-color: var(--black-color);

View File

@ -86,7 +86,7 @@
<div class="row" style="height: 54%">
<div class="col-sm-12" style="height: 100%">
<pre ng-class="{ wrap_lines: $ctrl.state.wrapLines }" class="log_viewer" scroll-glue="$ctrl.state.autoScroll" force-glue>
<div ng-repeat="log in $ctrl.state.filteredLogs = ($ctrl.data | filter:{ 'line': $ctrl.state.search }) track by $index" class="line" ng-if="log.line"><p class="inner_line" ng-click="$ctrl.selectLine(log.line)" ng-class="{ 'line_selected': $ctrl.state.selectedLines.indexOf(log.line) > -1 }"><span ng-repeat="span in log.spans" ng-style="{ 'color': span.foregroundColor, 'background-color': span.backgroundColor }">{{ span.text }}</span></p></div>
<div ng-repeat="log in $ctrl.state.filteredLogs = ($ctrl.data | filter:{ 'line': $ctrl.state.search }) track by $index" class="line" ng-if="log.line"><p class="inner_line" ng-click="$ctrl.selectLine(log.line)" ng-class="{ 'line_selected': $ctrl.state.selectedLines.indexOf(log.line) > -1 }"><span ng-repeat="span in log.spans track by $index" ng-style="{ 'color': span.foregroundColor, 'background-color': span.backgroundColor, 'font-weight': span.fontWeight }">{{ span.text }}</span></p></div>
<div ng-if="!$ctrl.state.filteredLogs.length" class="line"><p class="inner_line">No log line matching the '{{ $ctrl.state.search }}' filter</p></div>
<div ng-if="$ctrl.state.filteredLogs.length === 1 && !$ctrl.state.filteredLogs[0].line" class="line"><p class="inner_line">No logs available</p></div>
</pre>

View File

@ -1,5 +1,7 @@
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],
@ -39,6 +41,8 @@ const BACKGROUND_COLORS_BY_ANSI = {
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';
@ -76,8 +80,9 @@ angular.module('portainer.docker').factory('LogHelper', [
}
// Return an array with each log including a line and styled spans for each entry.
// If the skipHeaders param is specified, it will strip the 8 first characters of each line.
helper.formatLogs = function (logs, skipHeaders) {
// 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);
}
@ -120,9 +125,12 @@ angular.module('portainer.docker').factory('LogHelper', [
}
const text = stripEscapeCodes(tokenLines[i]);
line += text;
spans.push({ foregroundColor, backgroundColor, text });
if ((!withTimestamps && text.startsWith('{')) || (withTimestamps && text.substring(TIMESTAMP_LENGTH).startsWith('{'))) {
line += JSONToFormattedLine(text, spans, withTimestamps);
} else {
spans.push({ foregroundColor, backgroundColor, text });
line += text;
}
}
}
}
@ -137,3 +145,79 @@ angular.module('portainer.docker').factory('LogHelper', [
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;
}

View File

@ -159,7 +159,7 @@ function ContainerServiceFactory($q, Container, LogHelper, $timeout, EndpointPro
Container.logs(parameters)
.$promise.then(function success(data) {
var logs = LogHelper.formatLogs(data.logs, stripHeaders);
var logs = LogHelper.formatLogs(data.logs, { stripHeaders, withTimestamps: !!timestamps });
deferred.resolve(logs);
})
.catch(function error(err) {

View File

@ -88,7 +88,7 @@ angular.module('portainer.docker').factory('ServiceService', [
Service.logs(parameters)
.$promise.then(function success(data) {
var logs = LogHelper.formatLogs(data.logs, true);
var logs = LogHelper.formatLogs(data.logs, { stripHeaders: true, withTimestamps: !!timestamps });
deferred.resolve(logs);
})
.catch(function error(err) {

View File

@ -54,7 +54,7 @@ angular.module('portainer.docker').factory('TaskService', [
Task.logs(parameters)
.$promise.then(function success(data) {
var logs = LogHelper.formatLogs(data.logs, true);
var logs = LogHelper.formatLogs(data.logs, { stripHeaders: true, withTimestamps: !!timestamps });
deferred.resolve(logs);
})
.catch(function error(err) {

View File

@ -103,6 +103,7 @@
"clsx": "^1.1.1",
"codemirror": "~5.64.0",
"core-js": "^3.19.3",
"date-fns": "^2.29.3",
"fast-json-patch": "^3.1.0",
"file-saver": "^2.0.5",
"filesize": "~3.3.0",

View File

@ -8034,6 +8034,11 @@ date-fns@^2.21.3:
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2"
integrity sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==
date-fns@^2.29.3:
version "2.29.3"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8"
integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==
dateformat@~3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"