diff --git a/.changeset/chilly-cycles-own.md b/.changeset/chilly-cycles-own.md new file mode 100644 index 000000000..9b6b50052 --- /dev/null +++ b/.changeset/chilly-cycles-own.md @@ -0,0 +1,12 @@ +--- +"@opennextjs/aws": minor +--- + +feat: Add support for OPEN_NEXT_ERROR_LOG_LEVEL + +OPEN_NEXT_ERROR_LOG_LEVEL is the minimal error level from which internal errors are logged. +It can be set to: + +- "0" / "debug" +- "1" / "warn" (default) +- "2" / "error" diff --git a/packages/open-next/src/adapters/logger.ts b/packages/open-next/src/adapters/logger.ts index 7571a4f9e..b037dbd38 100644 --- a/packages/open-next/src/adapters/logger.ts +++ b/packages/open-next/src/adapters/logger.ts @@ -1,4 +1,4 @@ -import type { BaseOpenNextError } from "utils/error"; +import { type BaseOpenNextError, isOpenNextError } from "utils/error"; export function debug(...args: any[]) { if (globalThis.openNextDebug) { @@ -42,28 +42,34 @@ const isDownplayedErrorLog = (errorLog: AwsSdkClientCommandErrorLog) => export function error(...args: any[]) { // we try to catch errors from the aws-sdk client and downplay some of them - if ( - args.some((arg: AwsSdkClientCommandErrorLog) => isDownplayedErrorLog(arg)) - ) { + if (args.some((arg) => isDownplayedErrorLog(arg))) { debug(...args); - } else if (args.some((arg) => arg.__openNextInternal)) { + } else if (args.some((arg) => isOpenNextError(arg))) { // In case of an internal error, we log it with the appropriate log level - const error = args.find( - (arg) => arg.__openNextInternal, - ) as BaseOpenNextError; - if (error.logLevel === 0) { - debug(...args); + const error = args.find((arg) => isOpenNextError(arg))!; + if (error.logLevel < getOpenNextErrorLogLevel()) { return; } + if (error.logLevel === 0) { + // Display the name and the message instead of full Open Next errors. + // console.log is used so that logging does not depend on openNextDebug. + return console.log( + ...args.map((arg) => + isOpenNextError(arg) ? `${arg.name}: ${arg.message}` : arg, + ), + ); + } if (error.logLevel === 1) { - warn(...args); - return; + // Display the name and the message instead of full Open Next errors. + return warn( + ...args.map((arg) => + isOpenNextError(arg) ? `${arg.name}: ${arg.message}` : arg, + ), + ); } - console.error(...args); - return; - } else { - console.error(...args); + return console.error(...args); } + console.error(...args); } export const awsLogger = { @@ -73,3 +79,23 @@ export const awsLogger = { warn, error, }; + +/** + * Retrieves the log level for internal errors from the + * OPEN_NEXT_ERROR_LOG_LEVEL environment variable. + * + * @returns The numerical log level 0 (debug), 1 (warn), or 2 (error) + */ +function getOpenNextErrorLogLevel(): number { + const strLevel = process.env.OPEN_NEXT_ERROR_LOG_LEVEL ?? "1"; + switch (strLevel.toLowerCase()) { + case "debug": + case "0": + return 0; + case "error": + case "2": + return 2; + default: + return 1; + } +} diff --git a/packages/open-next/src/utils/error.ts b/packages/open-next/src/utils/error.ts index 2aae61c7b..c1e8dc9c0 100644 --- a/packages/open-next/src/utils/error.ts +++ b/packages/open-next/src/utils/error.ts @@ -39,3 +39,11 @@ export class FatalError extends Error implements BaseOpenNextError { this.name = "FatalError"; } } + +export function isOpenNextError(e: any): e is BaseOpenNextError & Error { + try { + return "__openNextInternal" in e; + } catch { + return false; + } +} diff --git a/packages/tests-unit/tests/adapters/cache.test.ts b/packages/tests-unit/tests/adapters/cache.test.ts index 242c4cb53..f5624b8ca 100644 --- a/packages/tests-unit/tests/adapters/cache.test.ts +++ b/packages/tests-unit/tests/adapters/cache.test.ts @@ -70,10 +70,7 @@ describe("S3Cache", () => { beforeEach(() => { vi.clearAllMocks(); - cache = new S3Cache({ - _appDir: false, - _requestHeaders: undefined as never, - }); + cache = new S3Cache(); globalThis.disableIncrementalCache = false; globalThis.isNextAfter15 = false; diff --git a/packages/tests-unit/tests/adapters/logger.test.ts b/packages/tests-unit/tests/adapters/logger.test.ts new file mode 100644 index 000000000..8491d1699 --- /dev/null +++ b/packages/tests-unit/tests/adapters/logger.test.ts @@ -0,0 +1,86 @@ +import * as logger from "@opennextjs/aws/adapters/logger.js"; +import { + FatalError, + IgnorableError, + RecoverableError, +} from "@opennextjs/aws/utils/error.js"; +import { vi } from "vitest"; + +describe("logger adapter", () => { + describe("Open Next errors", () => { + const debug = vi.spyOn(console, "log").mockImplementation(() => null); + const warn = vi.spyOn(console, "warn").mockImplementation(() => null); + const error = vi.spyOn(console, "error").mockImplementation(() => null); + + beforeEach(() => { + debug.mockClear(); + warn.mockClear(); + error.mockClear(); + }); + + const ignorableError = new IgnorableError("ignorable"); + const recoverableError = new RecoverableError("recoverable"); + const fatalError = new FatalError("fatal"); + + it("default to warn when OPEN_NEXT_ERROR_LOG_LEVEL is undefined", () => { + delete process.env.OPEN_NEXT_ERROR_LOG_LEVEL; + logger.error(ignorableError); + logger.error(recoverableError); + logger.error(fatalError); + expect(debug).not.toHaveBeenCalled(); + expect(warn).toHaveBeenCalledWith("RecoverableError: recoverable"); + expect(error).toHaveBeenCalledWith(fatalError); + }); + + it("OPEN_NEXT_ERROR_LOG_LEVEL is 'debug'/'0'", () => { + process.env.OPEN_NEXT_ERROR_LOG_LEVEL = "0"; + logger.error(ignorableError); + logger.error(recoverableError); + logger.error(fatalError); + expect(debug).toHaveBeenCalledWith("IgnorableError: ignorable"); + expect(warn).toHaveBeenCalledWith("RecoverableError: recoverable"); + expect(error).toHaveBeenCalledWith(fatalError); + process.env.OPEN_NEXT_ERROR_LOG_LEVEL = "debug"; + logger.error(ignorableError); + logger.error(recoverableError); + logger.error(fatalError); + expect(debug).toHaveBeenCalledWith("IgnorableError: ignorable"); + expect(warn).toHaveBeenCalledWith("RecoverableError: recoverable"); + expect(error).toHaveBeenCalledWith(fatalError); + }); + + it("OPEN_NEXT_ERROR_LOG_LEVEL is 'warn'/'1'", () => { + process.env.OPEN_NEXT_ERROR_LOG_LEVEL = "1"; + logger.error(ignorableError); + logger.error(recoverableError); + logger.error(fatalError); + expect(debug).not.toHaveBeenCalled(); + expect(warn).toHaveBeenCalledWith("RecoverableError: recoverable"); + expect(error).toHaveBeenCalledWith(fatalError); + process.env.OPEN_NEXT_ERROR_LOG_LEVEL = "warn"; + logger.error(ignorableError); + logger.error(recoverableError); + logger.error(fatalError); + expect(debug).not.toHaveBeenCalled(); + expect(warn).toHaveBeenCalledWith("RecoverableError: recoverable"); + expect(error).toHaveBeenCalledWith(fatalError); + }); + + it("OPEN_NEXT_ERROR_LOG_LEVEL is 'error'/'2'", () => { + process.env.OPEN_NEXT_ERROR_LOG_LEVEL = "2"; + logger.error(ignorableError); + logger.error(recoverableError); + logger.error(fatalError); + expect(debug).not.toHaveBeenCalled(); + expect(warn).not.toHaveBeenCalled(); + expect(error).toHaveBeenCalledWith(fatalError); + process.env.OPEN_NEXT_ERROR_LOG_LEVEL = "error"; + logger.error(ignorableError); + logger.error(recoverableError); + logger.error(fatalError); + expect(debug).not.toHaveBeenCalled(); + expect(warn).not.toHaveBeenCalled(); + expect(error).toHaveBeenCalledWith(fatalError); + }); + }); +});