Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions doc/examples/catch-exception/log-level.decorator.md
Original file line number Diff line number Diff line change
@@ -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<void> {
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<void> {
// 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<Order> {
// 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"
}
}
```

4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@jokr-services/logr",
"version": "3.1.1",
"version": "3.2.0",
"main": "index.js",
"types": "*/**/*.d.ts",
"license": "MIT",
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
}
Expand All @@ -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();
}
Expand All @@ -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);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🌴 Acredito que aqui as informações críticas do erro (stack trace, nome, kind) são perdidas quando logadas com níveis warn, info ou debug. O erro passado como parâmetro não será formatado corretamente no log, pois os métodos logger.warn(), logger.info() e logger.debug() esperam apenas message: string e ...params: any[], não um objeto de erro.

Para níveis não-error, precisamos incluir as informações do erro no log de forma adequada, similar ao que é feito no nível error com getErrorPattern(). Como podemos garantir que essas informações sejam preservadas?

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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ describe('@CatchException', () => {
},
title: '',
params: [],
error: ErrorMock
error: ErrorMock,
level: 'error'
});
});
});
Expand All @@ -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');
Expand Down
2 changes: 1 addition & 1 deletion src/lib/core/dtos/index.ts
Original file line number Diff line number Diff line change
@@ -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';
9 changes: 9 additions & 0 deletions src/lib/core/dtos/registered-error.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { TriggerInDTO } from '@core/dtos/trigger.dto';

export type LogLevel = 'error' | 'warn' | 'info' | 'debug';

export type RegisteredErrorDTO = {
/**
* The catched exception.
Expand All @@ -20,4 +22,11 @@ export type RegisteredErrorDTO = {
params: any[];

title: string;

/**
* The log level to use when logging the error.
*
* @type {LogLevel}
*/
level: LogLevel;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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<Fn extends (...args: any[]) => any>(
fn: Fn,
options?: CatchExceptionOptions,
Expand All @@ -23,7 +27,13 @@ export function catchException<Fn extends (...args: any[]) => any>(
}

if (options?.typeErrorHandling === 'REGISTER') {
AsyncTraceStorage.registeredError = { error, trigger: logger.trigger, title, params };
AsyncTraceStorage.registeredError = {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🌴 A função catchException (apesar de já estar deprecated) ainda não foi atualizada para suportar a opção level. Atualmente, ela sempre utiliza logger.error() e registra os logs com level: 'error'.

Isso acaba gerando uma inconsistência em relação ao decorator @CatchException. Além disso, usuários que ainda dependem de catchException não conseguem se beneficiar da nova funcionalidade de configuração de nível de log.

Alguns pontos que podemos avaliar:

  • Como garantir que a função catchException respeite a opção level quando ela for informada?

  • Existe a possibilidade de reutilizar a lógica que já está implementada no decorator @CatchException?

  • Mesmo sendo deprecated, faz sentido manter consistência de comportamento entre as duas abordagens?

error,
trigger: logger.trigger,
title,
params,
level: 'error'
};

return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({}, () => {
Expand Down Expand Up @@ -84,7 +85,8 @@ describe('catchException', () => {
className: 'mockConstructor',
kind: undefined
},
params: ['param1', 'param2']
params: ['param1', 'param2'],
level: 'error'
}
},
() => {
Expand Down
9 changes: 2 additions & 7 deletions src/lib/core/storages/async-trace/async-trace.storage.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
Expand Down
22 changes: 22 additions & 0 deletions src/lib/core/types/catch-exception-options.types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { LogLevel } from '@core/dtos';
import { CommonOptions } from '@core/types/common-options.types';

export type CatchExceptionOptions = CommonOptions & {
Expand Down Expand Up @@ -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);
};