diff --git a/app/assets/css/theme.css b/app/assets/css/theme.css index e0827377d..d917f54c8 100644 --- a/app/assets/css/theme.css +++ b/app/assets/css/theme.css @@ -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); diff --git a/app/docker/components/log-viewer/logViewer.html b/app/docker/components/log-viewer/logViewer.html index c72453296..61b0ccb24 100644 --- a/app/docker/components/log-viewer/logViewer.html +++ b/app/docker/components/log-viewer/logViewer.html @@ -86,7 +86,7 @@
-diff --git a/app/docker/helpers/logHelper.js b/app/docker/helpers/logHelper.js index 0c939c011..d2214b3c2 100644 --- a/app/docker/helpers/logHelper.js +++ b/app/docker/helpers/logHelper.js @@ -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; +} diff --git a/app/docker/services/containerService.js b/app/docker/services/containerService.js index 84b1ed6fd..fc1514b6f 100644 --- a/app/docker/services/containerService.js +++ b/app/docker/services/containerService.js @@ -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) { diff --git a/app/docker/services/serviceService.js b/app/docker/services/serviceService.js index d2caa7e69..cab03024b 100644 --- a/app/docker/services/serviceService.js +++ b/app/docker/services/serviceService.js @@ -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) { diff --git a/app/docker/services/taskService.js b/app/docker/services/taskService.js index e5e6d4708..5c38a316f 100644 --- a/app/docker/services/taskService.js +++ b/app/docker/services/taskService.js @@ -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) { diff --git a/package.json b/package.json index 3b23a24a7..97bfa0cd3 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index eb4c3a25a..48f47049b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"+{{ span.text }}
{{ span.text }}
No log line matching the '{{ $ctrl.state.search }}' filter
No logs available