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: "/",
|
endpoint: "/",
|
||||||
router: appRouter,
|
router: appRouter,
|
||||||
createContext: () => createTRPCContext({ session, headers: req.headers }),
|
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,
|
req,
|
||||||
createContext: () => createTRPCContext({ session: req.auth, headers: req.headers }),
|
createContext: () => createTRPCContext({ session: req.auth, headers: req.headers }),
|
||||||
onError({ error, path, type }) {
|
onError({ error, path, type }) {
|
||||||
logger.error(
|
logger.error(new Error(`tRPC Error with ${type} on '${path}'`, { cause: error.cause }));
|
||||||
`tRPC Error with ${type} on '${path}': (${error.code}) - ${error.message}\n${error.stack}\n${error.cause}`,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { formatError } from "pretty-print-error";
|
|
||||||
|
|
||||||
import { decryptSecret } from "@homarr/common/server";
|
import { decryptSecret } from "@homarr/common/server";
|
||||||
import type { Integration } from "@homarr/db/schema";
|
import type { Integration } from "@homarr/db/schema";
|
||||||
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
|
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
|
||||||
@@ -41,7 +39,10 @@ export const testConnectionAsync = async (
|
|||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { formatError } from "pretty-print-error";
|
|
||||||
|
|
||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker";
|
import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker";
|
||||||
|
|
||||||
@@ -12,7 +10,7 @@ export const updateCheckerRouter = createTRPCRouter({
|
|||||||
const data = await handler.getCachedOrUpdatedDataAsync({});
|
const data = await handler.getCachedOrUpdatedDataAsync({});
|
||||||
return data.data.availableUpdates;
|
return data.data.availableUpdates;
|
||||||
} catch (error) {
|
} 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
|
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 { cookies } from "next/headers";
|
||||||
import NextAuth from "next-auth";
|
import NextAuth from "next-auth";
|
||||||
import Credentials from "next-auth/providers/credentials";
|
import Credentials from "next-auth/providers/credentials";
|
||||||
import { formatError } from "pretty-print-error";
|
|
||||||
|
|
||||||
import { db } from "@homarr/db";
|
import { db } from "@homarr/db";
|
||||||
import type { SupportedAuthProvider } from "@homarr/definitions";
|
import type { SupportedAuthProvider } from "@homarr/definitions";
|
||||||
@@ -36,8 +35,7 @@ export const createConfiguration = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.error(formatError(error));
|
logger.error(error);
|
||||||
logger.error(formatError(error.cause));
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
trustHost: true,
|
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 winston, { format, transports } from "winston";
|
||||||
|
|
||||||
import { env } from "./env";
|
import { env } from "./env";
|
||||||
|
import { formatErrorCause, formatErrorStack } from "./error";
|
||||||
|
import { formatMetadata } from "./metadata";
|
||||||
import { RedisTransport } from "./redis-transport";
|
import { RedisTransport } from "./redis-transport";
|
||||||
|
|
||||||
/**
|
const logMessageFormat = format.printf(({ level, message, timestamp, cause, stack, ...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
|
|
||||||
*/
|
|
||||||
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 }) => {
|
|
||||||
if (!cause && !stack) {
|
if (!cause && !stack) {
|
||||||
return `${timestamp as string} ${level}: ${message as string}`;
|
return `${timestamp as string} ${level}: ${message as string}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatedStack = formatErrorStack(stack as string | undefined);
|
||||||
|
|
||||||
if (!cause) {
|
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()];
|
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 type { fetch } from "undici";
|
||||||
|
|
||||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||||
|
import { extractErrorMessage } from "@homarr/common";
|
||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
|
|
||||||
export const sendPingRequestAsync = async (url: string) => {
|
export const sendPingRequestAsync = async (url: string) => {
|
||||||
try {
|
try {
|
||||||
return await fetchWithTimeoutAndCertificates(url).then((response) => ({ statusCode: response.status }));
|
return await fetchWithTimeoutAndCertificates(url).then((response) => ({ statusCode: response.status }));
|
||||||
} catch (error) {
|
} 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 {
|
return {
|
||||||
error: formatError(error),
|
error: extractErrorMessage(error),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { formatError } from "pretty-print-error";
|
|
||||||
import SuperJSON from "superjson";
|
import SuperJSON from "superjson";
|
||||||
|
|
||||||
import { hashObjectBase64, Stopwatch } from "@homarr/common";
|
import { hashObjectBase64, Stopwatch } from "@homarr/common";
|
||||||
@@ -107,7 +106,9 @@ export const createRequestIntegrationJobHandler = <
|
|||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.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