feat(logs): improve logs by logging errors with causes and metadata (#2703)

* feat(logs): improve logs by logging errors with causes and metadata

* fix: deepsource issue
This commit is contained in:
Meier Lukas
2025-03-26 21:53:51 +01:00
committed by GitHub
parent 3e1c000d51
commit 579dd5763d
10 changed files with 81 additions and 45 deletions

50
packages/log/src/error.ts Normal file
View File

@@ -0,0 +1,50 @@
import { formatMetadata } from "./metadata";
/**
* Formats the cause of an error in the format
* @example caused by Error: {message}
* {stack-trace}
* @param cause next cause in the chain
* @param iteration current iteration of the function
* @returns formatted and stacked causes
*/
export const formatErrorCause = (cause: unknown, iteration = 0): string => {
// Prevent infinite recursion
if (iteration > 5) {
return "";
}
if (cause instanceof Error) {
if (!cause.cause) {
return `\ncaused by ${formatErrorTitle(cause)}\n${formatErrorStack(cause.stack)}`;
}
return `\ncaused by ${formatErrorTitle(cause)}\n${formatErrorStack(cause.stack)}${formatErrorCause(cause.cause, iteration + 1)}`;
}
return `\ncaused by ${cause as string}`;
};
const ignoredErrorProperties = ["stack", "message", "name", "cause"];
/**
* Formats the title of an error
* @example {name}: {message} {metadata}
* @param error error to format title from
* @returns formatted error title
*/
export const formatErrorTitle = (error: Error) => {
const title = error.message.length === 0 ? error.name : `${error.name}: ${error.message}`;
const metadata = formatMetadata(error, ignoredErrorProperties);
return `${title} ${metadata}`;
};
/**
* Formats the stack trance of an error
* We remove the first line as it contains the error name and message
* @param stack stack trace
* @returns formatted stack trace
*/
export const formatErrorStack = (stack: string | undefined) => (stack ? removeFirstLine(stack) : "");
const removeFirstLine = (stack: string) => stack.split("\n").slice(1).join("\n");

View File

@@ -2,43 +2,22 @@ import type { transport as Transport } from "winston";
import winston, { format, transports } from "winston";
import { env } from "./env";
import { formatErrorCause, formatErrorStack } from "./error";
import { formatMetadata } from "./metadata";
import { RedisTransport } from "./redis-transport";
/**
* Formats the cause of an error in the format
* @example caused by Error: {message}
* {stack-trace}
* @param cause next cause in the chain
* @param iteration current iteration of the function
* @returns formatted and stacked causes
*/
const formatCause = (cause: unknown, iteration = 0): string => {
// Prevent infinite recursion
if (iteration > 5) {
return "";
}
if (cause instanceof Error) {
if (!cause.cause) {
return `\ncaused by ${cause.stack}`;
}
return `\ncaused by ${cause.stack}${formatCause(cause.cause, iteration + 1)}`;
}
return `\ncaused by ${cause as string}`;
};
const logMessageFormat = format.printf(({ level, message, timestamp, cause, stack }) => {
const logMessageFormat = format.printf(({ level, message, timestamp, cause, stack, ...metadata }) => {
if (!cause && !stack) {
return `${timestamp as string} ${level}: ${message as string}`;
}
const formatedStack = formatErrorStack(stack as string | undefined);
if (!cause) {
return `${timestamp as string} ${level}: ${message as string}\n${stack as string}`;
return `${timestamp as string} ${level}: ${message as string} ${formatMetadata(metadata)}\n${formatedStack}`;
}
return `${timestamp as string} ${level}: ${message as string}\n${stack as string}${formatCause(cause)}`;
return `${timestamp as string} ${level}: ${message as string} ${formatMetadata(metadata)}\n${formatedStack}${formatErrorCause(cause)}`;
});
const logTransports: Transport[] = [new transports.Console()];

View File

@@ -0,0 +1,8 @@
export const formatMetadata = (metadata: Record<string, unknown> | Error, ignoreKeys?: string[]) => {
const filteredMetadata = Object.keys(metadata)
.filter((key) => !ignoreKeys?.includes(key))
.map((key) => ({ key, value: metadata[key as keyof typeof metadata] }))
.filter(({ value }) => typeof value !== "object" && typeof value !== "function");
return filteredMetadata.map(({ key, value }) => `${key}="${value as string}"`).join(" ");
};