mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-27 00:40:58 +01:00
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:
@@ -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 }));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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 }));
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -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
50
packages/log/src/error.ts
Normal 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");
|
||||
@@ -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()];
|
||||
|
||||
8
packages/log/src/metadata.ts
Normal file
8
packages/log/src/metadata.ts
Normal 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(" ");
|
||||
};
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user