mirror of https://github.com/portainer/portainer
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 segmentspull/7715/head
parent
6063f368ea
commit
1b0db4971f
|
@ -88,6 +88,13 @@
|
||||||
|
|
||||||
--BE-only: var(--ui-warning-7);
|
--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 */
|
/* Default Theme */
|
||||||
--bg-card-color: var(--white-color);
|
--bg-card-color: var(--white-color);
|
||||||
--bg-main-color: var(--white-color);
|
--bg-main-color: var(--white-color);
|
||||||
|
@ -265,6 +272,13 @@
|
||||||
|
|
||||||
/* Dark Theme */
|
/* Dark Theme */
|
||||||
[theme='dark'] {
|
[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-body-color: var(--grey-2);
|
||||||
--bg-btn-default-color: var(--grey-3);
|
--bg-btn-default-color: var(--grey-3);
|
||||||
--bg-blocklist-hover-color: var(--ui-gray-iron-10);
|
--bg-blocklist-hover-color: var(--ui-gray-iron-10);
|
||||||
|
@ -445,6 +459,13 @@
|
||||||
|
|
||||||
/* High Contrast Theme */
|
/* High Contrast Theme */
|
||||||
[theme='highcontrast'] {
|
[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-card-color: var(--black-color);
|
||||||
--bg-main-color: var(--black-color);
|
--bg-main-color: var(--black-color);
|
||||||
--bg-body-color: var(--black-color);
|
--bg-body-color: var(--black-color);
|
||||||
|
|
|
@ -86,7 +86,7 @@
|
||||||
<div class="row" style="height: 54%">
|
<div class="row" style="height: 54%">
|
||||||
<div class="col-sm-12" style="height: 100%">
|
<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>
|
<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" 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>
|
<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>
|
</pre>
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import tokenize from '@nxmix/tokenize-ansi';
|
import tokenize from '@nxmix/tokenize-ansi';
|
||||||
import x256 from 'x256';
|
import x256 from 'x256';
|
||||||
|
import { takeRight, without } from 'lodash';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
const FOREGROUND_COLORS_BY_ANSI = {
|
const FOREGROUND_COLORS_BY_ANSI = {
|
||||||
black: x256.colors[0],
|
black: x256.colors[0],
|
||||||
|
@ -39,6 +41,8 @@ const BACKGROUND_COLORS_BY_ANSI = {
|
||||||
bgBrightWhite: x256.colors[15],
|
bgBrightWhite: x256.colors[15],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const TIMESTAMP_LENGTH = 31; // 30 for timestamp + 1 for trailing space
|
||||||
|
|
||||||
angular.module('portainer.docker').factory('LogHelper', [
|
angular.module('portainer.docker').factory('LogHelper', [
|
||||||
function LogHelperFactory() {
|
function LogHelperFactory() {
|
||||||
'use strict';
|
'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.
|
// 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.
|
// If the stripHeaders param is specified, it will strip the 8 first characters of each line.
|
||||||
helper.formatLogs = function (logs, skipHeaders) {
|
// withTimestamps param is needed to find the start of JSON for Zerolog logs parsing
|
||||||
|
helper.formatLogs = function (logs, { stripHeaders: skipHeaders, withTimestamps }) {
|
||||||
if (skipHeaders) {
|
if (skipHeaders) {
|
||||||
logs = stripHeaders(logs);
|
logs = stripHeaders(logs);
|
||||||
}
|
}
|
||||||
|
@ -120,9 +125,12 @@ angular.module('portainer.docker').factory('LogHelper', [
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = stripEscapeCodes(tokenLines[i]);
|
const text = stripEscapeCodes(tokenLines[i]);
|
||||||
|
if ((!withTimestamps && text.startsWith('{')) || (withTimestamps && text.substring(TIMESTAMP_LENGTH).startsWith('{'))) {
|
||||||
line += text;
|
line += JSONToFormattedLine(text, spans, withTimestamps);
|
||||||
spans.push({ foregroundColor, backgroundColor, text });
|
} else {
|
||||||
|
spans.push({ foregroundColor, backgroundColor, text });
|
||||||
|
line += text;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -137,3 +145,79 @@ angular.module('portainer.docker').factory('LogHelper', [
|
||||||
return helper;
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -159,7 +159,7 @@ function ContainerServiceFactory($q, Container, LogHelper, $timeout, EndpointPro
|
||||||
|
|
||||||
Container.logs(parameters)
|
Container.logs(parameters)
|
||||||
.$promise.then(function success(data) {
|
.$promise.then(function success(data) {
|
||||||
var logs = LogHelper.formatLogs(data.logs, stripHeaders);
|
var logs = LogHelper.formatLogs(data.logs, { stripHeaders, withTimestamps: !!timestamps });
|
||||||
deferred.resolve(logs);
|
deferred.resolve(logs);
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
|
|
|
@ -88,7 +88,7 @@ angular.module('portainer.docker').factory('ServiceService', [
|
||||||
|
|
||||||
Service.logs(parameters)
|
Service.logs(parameters)
|
||||||
.$promise.then(function success(data) {
|
.$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);
|
deferred.resolve(logs);
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
|
|
|
@ -54,7 +54,7 @@ angular.module('portainer.docker').factory('TaskService', [
|
||||||
|
|
||||||
Task.logs(parameters)
|
Task.logs(parameters)
|
||||||
.$promise.then(function success(data) {
|
.$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);
|
deferred.resolve(logs);
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
|
|
|
@ -103,6 +103,7 @@
|
||||||
"clsx": "^1.1.1",
|
"clsx": "^1.1.1",
|
||||||
"codemirror": "~5.64.0",
|
"codemirror": "~5.64.0",
|
||||||
"core-js": "^3.19.3",
|
"core-js": "^3.19.3",
|
||||||
|
"date-fns": "^2.29.3",
|
||||||
"fast-json-patch": "^3.1.0",
|
"fast-json-patch": "^3.1.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"filesize": "~3.3.0",
|
"filesize": "~3.3.0",
|
||||||
|
|
|
@ -8034,6 +8034,11 @@ date-fns@^2.21.3:
|
||||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2"
|
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2"
|
||||||
integrity sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==
|
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:
|
dateformat@~3.0.3:
|
||||||
version "3.0.3"
|
version "3.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
|
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
|
||||||
|
|
Loading…
Reference in New Issue