From f3721d913e76a367a9cef635fefad846d94a0003 Mon Sep 17 00:00:00 2001 From: Iago Elesbao Date: Sat, 6 Dec 2025 02:33:16 -0300 Subject: [PATCH 1/2] feat: enhance CatchException decorator with log level support --- .../catch-exception/log-level.decorator.md | 132 ++++++++++++++++++ package-lock.json | 4 +- package.json | 2 +- .../catch-exception.decorator.ts | 51 ++++++- .../catch-exception/catch-exception.test.ts | 6 +- src/lib/core/dtos/index.ts | 2 +- src/lib/core/dtos/registered-error.dto.ts | 9 ++ .../catch-exception.function.ts | 8 +- .../catch-exception/catch-exception.test.ts | 6 +- .../async-trace/async-trace.storage.ts | 9 +- .../types/catch-exception-options.types.ts | 22 +++ 11 files changed, 228 insertions(+), 23 deletions(-) create mode 100644 doc/examples/catch-exception/log-level.decorator.md diff --git a/doc/examples/catch-exception/log-level.decorator.md b/doc/examples/catch-exception/log-level.decorator.md new file mode 100644 index 0000000..b0ca65e --- /dev/null +++ b/doc/examples/catch-exception/log-level.decorator.md @@ -0,0 +1,132 @@ +# level - decorator + +The `level` option allows you to specify the log level when an exception is caught. This is useful for differentiating between critical errors and expected exceptions like business rule violations. + +## Available Levels + +- `'error'` (default): For critical errors that require immediate attention +- `'warn'`: For expected exceptions that should be monitored (e.g., business rule violations) +- `'info'`: For informational exceptions that are part of normal flow +- `'debug'`: For debugging purposes + +--- + +## Use case + +Not all exceptions are critical errors. For example, when a user tries to perform an action that violates a business rule, it's an expected exception that should be logged but not treated as a critical error. + +## Usage with fixed level + +```typescript +import { CatchException } from '@daki/logr' +import { BusinessRuleError } from './errors' + +export class PaymentService { + + // Business rule violation - use 'warn' instead of 'error' + @CatchException({ + kind: 'Domain', + level: 'warn' + }) + public async processPayment(amount: number, userId: string): Promise { + if (amount < 0) { + throw new BusinessRuleError('Payment amount cannot be negative') + } + + if (amount > 10000) { + throw new BusinessRuleError('Payment amount exceeds limit') + } + + // Process payment... + } + + // Critical system error - use 'error' (default) + @CatchException({ + kind: 'Infrastructure' + }) + public async connectToPaymentGateway(): Promise { + // Connection logic that might fail critically + } +} +``` + +## Usage with dynamic level (function) + +You can also provide a function that determines the log level based on the exception: + +```typescript +import { CatchException } from '@daki/logr' +import { BusinessRuleError, ValidationError, SystemError } from './errors' + +export class OrderService { + + @CatchException({ + kind: 'Domain', + level: (error) => { + // Business rules - expected exceptions + if (error instanceof BusinessRuleError) return 'warn' + + // Validation errors - informational + if (error instanceof ValidationError) return 'info' + + // System errors - critical + return 'error' + } + }) + public async createOrder(orderData: any): Promise { + // Validation + if (!orderData.items || orderData.items.length === 0) { + throw new ValidationError('Order must have at least one item') + } + + // Business rule + if (orderData.totalAmount > this.getUserLimit(orderData.userId)) { + throw new BusinessRuleError('Order exceeds user limit') + } + + // System operation that might fail + return await this.repository.save(orderData) + } +} +``` + +## Log output examples + +### With level: 'warn' +```text +User not found { + "timestamp": "2023-09-12T22:45:13.468Z", + "level": "warn", + "logger": { + "name": "PaymentService", + "method_name": "processPayment", + "params": [-100, "user123"] + }, + "error": { + "name": "BusinessRuleError", + "message": "Payment amount cannot be negative", + "stack": {ErrorStack}, + "kind": "Domain" + } +} +``` + +### With level: 'error' (default) +```text +Database connection failed { + "timestamp": "2023-09-12T22:45:13.468Z", + "level": "error", + "logger": { + "name": "PaymentService", + "method_name": "connectToPaymentGateway", + "params": [] + }, + "error": { + "name": "ConnectionError", + "message": "Database connection failed", + "stack": {ErrorStack}, + "kind": "Infrastructure" + } +} +``` + diff --git a/package-lock.json b/package-lock.json index d670820..7bb171c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@jokr-services/logr", - "version": "3.1.1", + "version": "3.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@jokr-services/logr", - "version": "3.1.1", + "version": "3.2.0", "license": "MIT", "dependencies": { "dd-trace": "^4.5.0", diff --git a/package.json b/package.json index 935a8cb..3021178 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@jokr-services/logr", - "version": "3.1.1", + "version": "3.2.0", "main": "index.js", "types": "*/**/*.d.ts", "license": "MIT", diff --git a/src/lib/core/decorators/catch-exception/catch-exception.decorator.ts b/src/lib/core/decorators/catch-exception/catch-exception.decorator.ts index f2a77bb..9cf1ff7 100644 --- a/src/lib/core/decorators/catch-exception/catch-exception.decorator.ts +++ b/src/lib/core/decorators/catch-exception/catch-exception.decorator.ts @@ -1,3 +1,4 @@ +import { LogLevel } from '@core/dtos'; import { catchExceptionFactory } from '@core/factories'; import { getLogParams, persistsMetadata } from '@core/helpers'; import { LoggerService } from '@core/interfaces'; @@ -33,13 +34,24 @@ export function CatchException( methodName: methodName }; + const level: LogLevel = + typeof options?.level === 'function' + ? options.level.call(this, error, this, ...args) + : options?.level || 'error'; + if (AsyncTraceStorage.outsideAsyncContext) { - logger.error(error, title, ...params); + logWithLevel(logger, level, error, title, params); return; } if (options?.typeErrorHandling === 'REGISTER') { - AsyncTraceStorage.registeredError = { error, trigger: logger.trigger, title, params }; + AsyncTraceStorage.registeredError = { + error, + trigger: logger.trigger, + title, + params, + level + }; return; } @@ -48,11 +60,12 @@ export function CatchException( logger.trigger = AsyncTraceStorage.registeredError.trigger; } - logger.error( - AsyncTraceStorage.registeredError?.error ?? error, - AsyncTraceStorage.registeredError?.title ?? title, - ...(AsyncTraceStorage.registeredError?.params ?? params) - ); + const finalLevel = AsyncTraceStorage.registeredError?.level ?? level; + const finalError = AsyncTraceStorage.registeredError?.error ?? error; + const finalTitle = AsyncTraceStorage.registeredError?.title ?? title; + const finalParams = AsyncTraceStorage.registeredError?.params ?? params; + + logWithLevel(logger, finalLevel, finalError, finalTitle, finalParams); AsyncTraceStorage.clearRegisteredError(); } @@ -66,3 +79,27 @@ export function CatchException( return descriptor; }; } + +function logWithLevel( + logger: LoggerService, + level: LogLevel, + error: any, + title: string, + params: any[] +): void { + switch (level) { + case 'warn': + logger.warn(title || error.message, error, ...params); + break; + case 'info': + logger.info(title || error.message, error, ...params); + break; + case 'debug': + logger.debug(title || error.message, error, ...params); + break; + case 'error': + default: + logger.error(error, title, ...params); + break; + } +} diff --git a/src/lib/core/decorators/catch-exception/catch-exception.test.ts b/src/lib/core/decorators/catch-exception/catch-exception.test.ts index 4df6b7f..6ccc502 100644 --- a/src/lib/core/decorators/catch-exception/catch-exception.test.ts +++ b/src/lib/core/decorators/catch-exception/catch-exception.test.ts @@ -84,7 +84,8 @@ describe('@CatchException', () => { }, title: '', params: [], - error: ErrorMock + error: ErrorMock, + level: 'error' }); }); }); @@ -99,7 +100,8 @@ describe('@CatchException', () => { methodName: 'customMethod' }, params: ['param1', 'param2'], - title: 'some title' + title: 'some title', + level: 'error' }; const clearRegisteredErrorSpy = jest.spyOn(AsyncTraceStorage, 'clearRegisteredError'); diff --git a/src/lib/core/dtos/index.ts b/src/lib/core/dtos/index.ts index 97f1389..1980e02 100644 --- a/src/lib/core/dtos/index.ts +++ b/src/lib/core/dtos/index.ts @@ -1,4 +1,4 @@ export type { ErrorDTO } from './error.dto'; export type { ErrorPatternDTO, LogPatternDTO } from './patterns.dto'; -export type { RegisteredErrorDTO } from './registered-error.dto'; +export type { LogLevel, RegisteredErrorDTO } from './registered-error.dto'; export type { TriggerInDTO, TriggerOutDTO } from './trigger.dto'; diff --git a/src/lib/core/dtos/registered-error.dto.ts b/src/lib/core/dtos/registered-error.dto.ts index 8b4acae..c815fdc 100644 --- a/src/lib/core/dtos/registered-error.dto.ts +++ b/src/lib/core/dtos/registered-error.dto.ts @@ -1,5 +1,7 @@ import { TriggerInDTO } from '@core/dtos/trigger.dto'; +export type LogLevel = 'error' | 'warn' | 'info' | 'debug'; + export type RegisteredErrorDTO = { /** * The catched exception. @@ -20,4 +22,11 @@ export type RegisteredErrorDTO = { params: any[]; title: string; + + /** + * The log level to use when logging the error. + * + * @type {LogLevel} + */ + level: LogLevel; }; diff --git a/src/lib/core/functions/catch-exception/catch-exception.function.ts b/src/lib/core/functions/catch-exception/catch-exception.function.ts index 1a17cb0..85778bd 100644 --- a/src/lib/core/functions/catch-exception/catch-exception.function.ts +++ b/src/lib/core/functions/catch-exception/catch-exception.function.ts @@ -23,7 +23,13 @@ export function catchException any>( } if (options?.typeErrorHandling === 'REGISTER') { - AsyncTraceStorage.registeredError = { error, trigger: logger.trigger, title, params }; + AsyncTraceStorage.registeredError = { + error, + trigger: logger.trigger, + title, + params, + level: 'error' + }; return; } diff --git a/src/lib/core/functions/catch-exception/catch-exception.test.ts b/src/lib/core/functions/catch-exception/catch-exception.test.ts index 460e6c4..8897575 100644 --- a/src/lib/core/functions/catch-exception/catch-exception.test.ts +++ b/src/lib/core/functions/catch-exception/catch-exception.test.ts @@ -55,7 +55,8 @@ describe('catchException', () => { error: ErrorMock, title: '', params: [], - trigger: { className: 'mockConstructor', kind: undefined } + trigger: { className: 'mockConstructor', kind: undefined }, + level: 'error' }; await AsyncTraceStorage.run({}, () => { @@ -84,7 +85,8 @@ describe('catchException', () => { className: 'mockConstructor', kind: undefined }, - params: ['param1', 'param2'] + params: ['param1', 'param2'], + level: 'error' } }, () => { diff --git a/src/lib/core/storages/async-trace/async-trace.storage.ts b/src/lib/core/storages/async-trace/async-trace.storage.ts index 7e6d66b..e1c1b91 100644 --- a/src/lib/core/storages/async-trace/async-trace.storage.ts +++ b/src/lib/core/storages/async-trace/async-trace.storage.ts @@ -1,7 +1,7 @@ import { AsyncTrace } from '@core/types'; import { AsyncLocalStorage } from 'async_hooks'; -import { RegisteredErrorDTO, TriggerInDTO } from '../../dtos'; +import { RegisteredErrorDTO } from '../../dtos'; export class AsyncTraceStorage { private static instance: AsyncTraceStorage; @@ -60,12 +60,7 @@ export class AsyncTraceStorage { } } - public static set registeredError(dto: { - error: any; - trigger: TriggerInDTO; - title: string; - params: any[]; - }) { + public static set registeredError(dto: RegisteredErrorDTO) { const store = this.getStore(); if (!store) { return; diff --git a/src/lib/core/types/catch-exception-options.types.ts b/src/lib/core/types/catch-exception-options.types.ts index 24008f5..1f1de79 100644 --- a/src/lib/core/types/catch-exception-options.types.ts +++ b/src/lib/core/types/catch-exception-options.types.ts @@ -1,3 +1,4 @@ +import { LogLevel } from '@core/dtos'; import { CommonOptions } from '@core/types/common-options.types'; export type CatchExceptionOptions = CommonOptions & { @@ -53,4 +54,25 @@ export type CatchExceptionOptions = CommonOptions & { * @type {"LOG"|"REGISTER"} */ typeErrorHandling?: 'LOG' | 'REGISTER'; + + /** + * @description Specifies the log level to use when logging the exception. Defaults to 'error'. + * This allows you to differentiate between critical errors and expected exceptions like business rule violations. + * + * @example + * // Business rule violation - use 'warn' or 'info' + * @CatchException({ level: 'warn' }) + * validateUserAge(age: number) { + * if (age < 18) throw new Error('User must be 18 or older'); + * } + * + * // Critical system error - use 'error' (default) + * @CatchException({ level: 'error' }) + * connectToDatabase() { + * // ... + * } + * + * @type {LogLevel | ((exception: any, context?: any, ...params: any[]) => LogLevel)} + */ + level?: LogLevel | ((exception: any, context?: any, ...params: any[]) => LogLevel); }; From a5aa8cb7f48d3ff3b36f9f299feb6119e34834f8 Mon Sep 17 00:00:00 2001 From: Iago Elesbao Date: Thu, 18 Dec 2025 10:52:17 -0300 Subject: [PATCH 2/2] docs: mark catchException function as deprecated in favor of @CatchException decorator --- .../functions/catch-exception/catch-exception.function.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lib/core/functions/catch-exception/catch-exception.function.ts b/src/lib/core/functions/catch-exception/catch-exception.function.ts index 85778bd..c4d561f 100644 --- a/src/lib/core/functions/catch-exception/catch-exception.function.ts +++ b/src/lib/core/functions/catch-exception/catch-exception.function.ts @@ -4,6 +4,10 @@ import { Logr } from '@core/services'; import { AsyncTraceStorage } from '@core/storages'; import { CatchExceptionOptions } from '@core/types'; +/** + * @deprecated Support for the catchException high-order function will be discontinued soon. + * It is recommended to use the @CatchException decorator for exception handling instead. + */ export function catchException any>( fn: Fn, options?: CatchExceptionOptions,