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

View File

@@ -26,6 +26,9 @@ const handlerAsync = async (req: NextRequest) => {
endpoint: "/",
router: appRouter,
createContext: () => createTRPCContext({ session, headers: req.headers }),
onError({ error, path, type }) {
logger.error(new Error(`tRPC Error with ${type} on '${path}'`, { cause: error.cause }));
},
});
};

View File

@@ -31,9 +31,7 @@ const handler = auth(async (req) => {
req,
createContext: () => createTRPCContext({ session: req.auth, headers: req.headers }),
onError({ error, path, type }) {
logger.error(
`tRPC Error with ${type} on '${path}': (${error.code}) - ${error.message}\n${error.stack}\n${error.cause}`,
);
logger.error(new Error(`tRPC Error with ${type} on '${path}'`, { cause: error.cause }));
},
});

View File

@@ -1,5 +1,3 @@
import { formatError } from "pretty-print-error";
import { decryptSecret } from "@homarr/common/server";
import type { Integration } from "@homarr/db/schema";
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
@@ -41,7 +39,10 @@ export const testConnectionAsync = async (
};
} catch (error) {
logger.warn(
`Failed to decrypt secret from database integration="${integration.name}" secretKind="${secret.kind}"\n${formatError(error)}`,
new Error(
`Failed to decrypt secret from database integration="${integration.name}" secretKind="${secret.kind}"`,
{ cause: error },
),
);
return null;
}

View File

@@ -1,5 +1,3 @@
import { formatError } from "pretty-print-error";
import { logger } from "@homarr/log";
import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker";
@@ -12,7 +10,7 @@ export const updateCheckerRouter = createTRPCRouter({
const data = await handler.getCachedOrUpdatedDataAsync({});
return data.data.availableUpdates;
} catch (error) {
logger.error(`Failed to get available updates\n${formatError(error)}`);
logger.error(new Error("Failed to get available updates", { cause: error }));
return undefined; // We return undefined to not show the indicator in the UI
}
}),

View File

@@ -2,7 +2,6 @@ import type { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapte
import { cookies } from "next/headers";
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
import { formatError } from "pretty-print-error";
import { db } from "@homarr/db";
import type { SupportedAuthProvider } from "@homarr/definitions";
@@ -36,8 +35,7 @@ export const createConfiguration = (
return;
}
logger.error(formatError(error));
logger.error(formatError(error.cause));
logger.error(error);
},
},
trustHost: true,

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(" ");
};

View File

@@ -1,16 +1,16 @@
import { formatError } from "pretty-print-error";
import type { fetch } from "undici";
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
import { extractErrorMessage } from "@homarr/common";
import { logger } from "@homarr/log";
export const sendPingRequestAsync = async (url: string) => {
try {
return await fetchWithTimeoutAndCertificates(url).then((response) => ({ statusCode: response.status }));
} catch (error) {
logger.error("packages/ping/src/index.ts:", formatError(error));
logger.error(new Error(`Failed to send ping request to "${url}"`, { cause: error }));
return {
error: formatError(error),
error: extractErrorMessage(error),
};
}
};

View File

@@ -1,4 +1,3 @@
import { formatError } from "pretty-print-error";
import SuperJSON from "superjson";
import { hashObjectBase64, Stopwatch } from "@homarr/common";
@@ -107,7 +106,9 @@ export const createRequestIntegrationJobHandler = <
);
} catch (error) {
logger.error(
`Failed to run integration job integration=${integrationId} inputHash='${inputHash}' error=${formatError(error)}`,
new Error(`Failed to run integration job integration=${integrationId} inputHash='${inputHash}'`, {
cause: error,
}),
);
}
}