From cd8a6f8dcc2599302ea34ed532713b2dabba43d8 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 15 Nov 2022 09:40:51 -0500 Subject: [PATCH] Add winston formatting support for error causes Signed-off-by: Sebastian Malton --- package.json | 1 - src/common/logger-formaters/console-format.ts | 184 ++++++++++++++++++ src/common/logger.ts | 4 +- src/main/context-handler/context-handler.ts | 8 +- .../metrics/add-metrics-route.injectable.ts | 17 +- src/renderer/kube-watch-api/kube-watch-api.ts | 2 +- yarn.lock | 13 +- 7 files changed, 202 insertions(+), 27 deletions(-) create mode 100644 src/common/logger-formaters/console-format.ts diff --git a/package.json b/package.json index 2e5de0545c..d61d4cc26a 100644 --- a/package.json +++ b/package.json @@ -290,7 +290,6 @@ "uuid": "^8.3.2", "win-ca": "^3.5.0", "winston": "^3.8.2", - "winston-console-format": "^1.0.8", "winston-transport-browserconsole": "^1.0.5", "ws": "^8.11.0", "xterm-link-provider": "^1.3.1" diff --git a/src/common/logger-formaters/console-format.ts b/src/common/logger-formaters/console-format.ts new file mode 100644 index 0000000000..1fbe353c3d --- /dev/null +++ b/src/common/logger-formaters/console-format.ts @@ -0,0 +1,184 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { LEVEL, MESSAGE, SPLAT } from "triple-beam"; +import colors from "colors/safe"; +import type { InspectOptions } from "util"; +import { inspect } from "util"; + +// The following license was copied from https://github.com/duccio/winston-console-format/blob/master/LICENSE +// This was modified to support formatting causes + +/* +The MIT License (MIT) + +Copyright (c) 2014-2015 Eugeny Dementev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +export interface ConsoleFormatOptions { + showMeta?: boolean; + metaStrip?: string[]; + inspectOptions?: InspectOptions; +} + +interface TransformableInfo { + level: string; + message: string; + [key: string | symbol]: any; +} + +export class ConsoleFormat { + private static readonly reSpaces = /^\s+/; + private static readonly reSpacesOrEmpty = /^(\s*)/; + // eslint-disable-next-line no-control-regex + private static readonly reColor = /\x1B\[\d+m/; + private static readonly defaultStrip = [LEVEL, MESSAGE, SPLAT, "level", "message", "ms", "stack"]; + private static readonly chars = { + singleLine: "▪", + startLine: "┏", + line: "┃", + endLine: "┗", + }; + + private readonly showMeta: boolean; + private readonly metaStrip: string[]; + private readonly inspectOptions: InspectOptions; + + public constructor(opts?: ConsoleFormatOptions) { + this.showMeta = opts?.showMeta ?? true; + this.metaStrip = opts?.metaStrip ?? []; + this.inspectOptions = opts?.inspectOptions ?? {}; + } + + private getLines(value: unknown): string[] { + return inspect(value, this.inspectOptions).split("\n"); + } + + private message(info: TransformableInfo, chr: string, color: string): string { + const message = info.message.replace( + ConsoleFormat.reSpacesOrEmpty, + `$1${color}${colors.dim(chr)}${colors.reset(" ")}`, + ); + + return `${info.level}:${message}`; + } + + private pad(message?: string): string { + return message?.match(ConsoleFormat.reSpaces)?.[0] ?? ""; + } + + private ms(info: TransformableInfo): string { + if (info.ms) { + return colors.italic(colors.dim(` ${info.ms}`)); + } + + return ""; + } + + private stack(info: TransformableInfo): string[] { + const messages: string[] = []; + + if (info.stack) { + const error = new Error(); + + error.stack = info.stack; + messages.push(...this.getLines(error)); + } + + return messages; + } + + private _cause(source: unknown): string[] { + const messages: string[] = []; + + if (source instanceof Error && source.cause) { + messages.push(`Cause: ${source.cause}`); + messages.push(...this.getLines(source.cause).map(l => ` ${l}`)); + messages.push(...this._cause(source.cause)); + } + + return messages; + } + + private cause(info: TransformableInfo): string[] { + const splats = info[SPLAT]; + + if (Array.isArray(splats)) { + return splats.flatMap(splat => this._cause(splat)); + } + + return []; + } + + private meta(info: TransformableInfo): string[] { + const messages: string[] = []; + const stripped = { ...info }; + + ConsoleFormat.defaultStrip.forEach((e) => delete stripped[e]); + this.metaStrip?.forEach((e) => delete stripped[e]); + + if (Object.keys(stripped).length > 0) { + messages.push(...this.getLines(stripped)); + } + + return messages; + } + + private getColor(info: TransformableInfo): string { + return info.level.match(ConsoleFormat.reColor)?.[0] ?? ""; + } + + private write(info: TransformableInfo, messages: string[], color: string): void { + const pad = this.pad(info.message); + + messages.forEach((line, index, arr) => { + const lineNumber = colors.dim(`[${(index + 1).toString().padStart(arr.length.toString().length, " ")}]`); + let chr = ConsoleFormat.chars.line; + + if (index === arr.length - 1) { + chr = ConsoleFormat.chars.endLine; + } + info[MESSAGE] += `\n${colors.dim(info.level)}:${pad}${color}${colors.dim(chr)}${colors.reset(" ")}`; + info[MESSAGE] += `${lineNumber} ${line}`; + }); + } + + public transform(info: TransformableInfo): TransformableInfo { + const messages: string[] = []; + + if (this.showMeta) { + messages.push(...this.stack(info)); + messages.push(...this.meta(info)); + messages.push(...this.cause(info)); + } + + const color = this.getColor(info); + + info[MESSAGE] = this.message(info, ConsoleFormat.chars[messages.length > 0 ? "startLine" : "singleLine"], color); + info[MESSAGE] += this.ms(info); + + this.write(info, messages, color); + + return info; + } +} diff --git a/src/common/logger.ts b/src/common/logger.ts index 231729cc44..2a485960ff 100644 --- a/src/common/logger.ts +++ b/src/common/logger.ts @@ -6,9 +6,9 @@ import { app, ipcMain } from "electron"; import winston, { format } from "winston"; import type Transport from "winston-transport"; -import { consoleFormat } from "winston-console-format"; import { isDebugging, isTestEnv } from "./vars"; import BrowserConsole from "winston-transport-browserconsole"; +import { ConsoleFormat } from "./logger-formaters/console-format"; export interface Logger { info: (message: string, ...args: any) => void; @@ -37,7 +37,7 @@ if (ipcMain) { format.colorize({ level: true, message: false }), format.padLevels(), format.ms(), - consoleFormat({ + new ConsoleFormat({ showMeta: true, inspectOptions: { depth: 4, diff --git a/src/main/context-handler/context-handler.ts b/src/main/context-handler/context-handler.ts index 563c7afecc..63a4cfd0b1 100644 --- a/src/main/context-handler/context-handler.ts +++ b/src/main/context-handler/context-handler.ts @@ -111,14 +111,12 @@ export class ContextHandler implements ClusterContextHandler { const potentialServices = await Promise.allSettled( providers.map(provider => provider.getPrometheusService(apiClient)), ); - const errors: any[] = []; + const errors = []; for (const res of potentialServices) { switch (res.status) { case "rejected": - if (res.reason) { - errors.push(String(res.reason)); - } + errors.push(res.reason); break; case "fulfilled": @@ -128,7 +126,7 @@ export class ContextHandler implements ClusterContextHandler { } } - throw Object.assign(new Error("No Prometheus service found"), { cause: errors }); + throw new Error("No Prometheus service found", { cause: errors }); } async resolveAuthProxyUrl(): Promise { diff --git a/src/main/routes/metrics/add-metrics-route.injectable.ts b/src/main/routes/metrics/add-metrics-route.injectable.ts index e55fc8bec8..a11d406737 100644 --- a/src/main/routes/metrics/add-metrics-route.injectable.ts +++ b/src/main/routes/metrics/add-metrics-route.injectable.ts @@ -28,14 +28,17 @@ const loadMetricsFor = (getMetrics: GetMetrics) => async (promQueries: string[], try { return await getMetrics(cluster, prometheusPath, { query, ...queryParams }); } catch (error) { - if (isRequestError(error)) { - if (lastAttempt || (error.statusCode && error.statusCode >= 400 && error.statusCode < 500)) { - throw new Error("Metrics not available", { cause: error }); - } - } else if (error instanceof Error) { + if ( + !isRequestError(error) + || lastAttempt + || ( + !lastAttempt && ( + typeof error.statusCode === "number" && + 400 <= error.statusCode && error.statusCode < 500 + ) + ) + ) { throw new Error("Metrics not available", { cause: error }); - } else { - throw new Error("Metrics not available"); } await new Promise(resolve => setTimeout(resolve, (attempt + 1) * 1000)); // add delay before repeating request diff --git a/src/renderer/kube-watch-api/kube-watch-api.ts b/src/renderer/kube-watch-api/kube-watch-api.ts index 95c0442fcb..c3eca74d29 100644 --- a/src/renderer/kube-watch-api/kube-watch-api.ts +++ b/src/renderer/kube-watch-api/kube-watch-api.ts @@ -107,7 +107,7 @@ export class KubeWatchApi { unsubscribe.push(store.subscribe({ onLoadFailure, abortController: childController })); } catch (error) { if (!(error instanceof DOMException)) { - this.log(Object.assign(new Error("Loading stores has failed"), { cause: error }), { + this.log(new Error("Loading stores has failed", { cause: error }), { meta: { store, namespaces }, }); } diff --git a/yarn.lock b/yarn.lock index 11944a6d68..d23619a32b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4118,7 +4118,7 @@ colors@1.0.3: resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs= -colors@^1.3.3, colors@^1.4.0: +colors@^1.3.3: version "1.4.0" resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== @@ -8462,7 +8462,7 @@ lodash@^4.17.10, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17 resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -logform@^2.2.0, logform@^2.3.2, logform@^2.4.0: +logform@^2.3.2, logform@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/logform/-/logform-2.4.0.tgz#131651715a17d50f09c2a2c1a524ff1a4164bcfe" integrity sha512-CPSJw4ftjf517EhXZGGvTHHkYobo7ZCc0kvwUoOYcjfR2UVrI66RHj8MCrfAdEitdmFqbu2BYdYs8FHHZSb6iw== @@ -12851,15 +12851,6 @@ win-ca@^3.5.0: node-forge "^1.2.1" split "^1.0.1" -winston-console-format@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/winston-console-format/-/winston-console-format-1.0.8.tgz#591adc8e9567c3397a3fa2e29e596d56e48db840" - integrity sha512-dq7t/E0D0QRi4XIOwu6HM1+5e//WPqylH88GVjKEhQVrzGFg34MCz+G7pMJcXFBen9C0kBsu5GYgbYsE2LDwKw== - dependencies: - colors "^1.4.0" - logform "^2.2.0" - triple-beam "^1.3.0" - winston-transport-browserconsole@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/winston-transport-browserconsole/-/winston-transport-browserconsole-1.0.5.tgz#8ef1bc32da5fb0a66604f2b8b6f127ed725108c9"