nestjs-log-decorator 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,14 +25,15 @@ TypeScript decorators that eliminate logging boilerplate from NestJS application
25
25
 
26
26
  ## Description
27
27
 
28
- `@Log()` decorator replaces the try-catch logging pattern in NestJS service methods by automatically logging method invocation on return or error.
28
+ `@Log()` decorator replaces the try-catch logging pattern in NestJS service methods by automatically logging method success and errors, with optional invocation and result logging.
29
29
 
30
30
  **Key Features**
31
31
 
32
32
  - By default uses structured output
33
33
  - Prettifies Axios errors
34
34
  - Zero configuration
35
- - Zero dependencies
35
+ - Minimal dependencies
36
+ - Uses default `@nestjs/common` Logger instance
36
37
 
37
38
  ## Installation
38
39
 
@@ -42,19 +43,17 @@ npm install nestjs-log-decorator @nestjs/common
42
43
 
43
44
  ## Quick Start
44
45
 
46
+ Simply apply `@Log()` to your class or method:
47
+
45
48
  ```typescript
46
- import { Logger } from '@nestjs/common';
47
49
  import { Log } from 'nestjs-log-decorator';
48
50
 
49
51
  class UserService {
50
- readonly logger = new Logger(UserService.name);
51
-
52
52
  @Log()
53
53
  createUser(name: string, email: string) {
54
54
  return { id: 1, name, email };
55
55
  }
56
56
  }
57
-
58
57
  ```
59
58
 
60
59
  Once a service method is called, it will log the method invocation with all arguments.
@@ -104,18 +103,43 @@ const result = await resultPromise;
104
103
  // [UserService] { method: 'createUser', state: 'success', args: { name: 'John' } }
105
104
  ```
106
105
 
106
+ ### Result Logging
107
+
108
+ If you want to include method results in success logs, use the `result` option.
109
+
110
+ ```typescript
111
+ class UserService {
112
+ // Logs result as-is
113
+ @Log({ result: true })
114
+ findUser(id: number) {
115
+ return { id, name: 'John', email: 'john@example.com' };
116
+ }
117
+
118
+ // Logs formatted result
119
+ @Log({
120
+ result: (res: { id: number; name: string; email: string }) => ({ id: res.id, name: res.name }),
121
+ })
122
+ findPublicUser(id: number) {
123
+ return { id, name: 'John', email: 'john@example.com' };
124
+ }
125
+ }
126
+ ```
127
+
128
+ Example success output:
129
+
130
+ ```typescript
131
+ // [UserService] { method: 'findUser', state: 'success', args: { id: 1 }, result: { id: 1, name: 'John', email: 'john@example.com' } }
132
+ // [UserService] { method: 'findPublicUser', state: 'success', args: { id: 1 }, result: { id: 1, name: 'John' } }
133
+ ```
134
+
107
135
  ### Complete Example
108
136
 
109
- After installation, no additional configuration is needed, just make sure that your class has a public `logger` property that implements the default NestJS Logger interface.
137
+ After installation, no additional configuration is needed. If the class has `logger` properly, the `@Log()` decorator will use it log method. If the logger is missing, the decorator will inject `@nestjs/common` Logger instance using the class name as the context.
110
138
 
111
139
  ```typescript
112
- import { Logger } from '@nestjs/common';
113
140
  import { Log } from 'nestjs-log-decorator';
114
141
 
115
-
116
142
  class PaymentService {
117
- // `logger` property will be used by decorator
118
- readonly logger = new Logger(PaymentService.name);
119
143
 
120
144
  @Log()
121
145
  async processPayment(amount: number, currency: string) {
@@ -130,7 +154,25 @@ class PaymentService {
130
154
  }
131
155
  ```
132
156
 
133
- The `logger` property is public due to TypeScript validation limitations. TS cannot check if a private property is valid, but it should still work even if the property is private.
157
+ #### Explicit Logger (Optional)
158
+
159
+ If you need a custom logger (e.g., for testing or a different context), you can still define your own:
160
+
161
+ ```typescript
162
+ import { Logger } from '@nestjs/common';
163
+ import { Log } from 'nestjs-log-decorator';
164
+
165
+ @Log()
166
+ class PaymentService {
167
+ // Explicit logger takes precedence over auto-injected one
168
+ readonly logger = new Logger('CustomPaymentContext');
169
+
170
+ async processPayment(amount: number, currency: string) {
171
+ // Logs using the explicit logger with 'CustomPaymentContext' context
172
+ return await this.gateway.processPayment(amount, currency);
173
+ }
174
+ }
175
+ ```
134
176
 
135
177
  ## How It Works
136
178
 
@@ -141,38 +183,38 @@ The `@Log()` decorator wraps your methods with automatic try-catch logging. It e
141
183
  │ @Log() Decorator Flow │
142
184
  └─────────────────────────────────────────────────────────────────────────────┘
143
185
 
144
- Method Call
145
-
146
-
147
- ┌──────────────────┐
148
- │ Extract Args │ ──▶ { id: 1, name: 'John' }
149
- │ (auto or custom) │
150
- └────────┬─────────┘
151
-
152
-
153
- ┌──────────────────┐ ┌─────────────────────────────────────┐
154
- │ onInvoke: true? │──YES─▶│ logger.log({ state: 'invoked' }) │
155
- └────────┬─────────┘ └─────────────────────────────────────┘
156
- │ NO
157
-
158
- ┌──────────────────────┐
159
- │ Execute Original │
160
- │ Method (sync/async) │
161
- └────────┬─────────────┘
162
-
163
- ┌────┴────┐
164
- ▼ ▼
165
- SUCCESS ERROR
166
- │ │
167
- ▼ ▼
168
- ┌────────┐ ┌──────────────────────────────────────────────────────┐
169
- │log() │ │ logger.error({ state: 'error', error: prettify(e) })│
170
- │success │ │ (Axios errors auto-prettified) │
171
- └────────┘ └──────────────────────────────────────────────────────┘
172
- │ │
173
- ▼ ▼
174
- Return Re-throw
175
- Result Error
186
+ Method Call
187
+
188
+
189
+ ┌──────────────────┐
190
+ │ Extract Args │ ──▶ { id: 1, name: 'John' }
191
+ │ (auto or custom) │
192
+ └────────┬─────────┘
193
+
194
+
195
+ ┌──────────────────┐ ┌─────────────────────────────────────┐
196
+ │ onInvoke: true? │──YES─▶│ logger.log({ state: 'invoked' }) │
197
+ └────────┬─────────┘ └─────────────────────────────────────┘
198
+ │ NO
199
+
200
+ ┌──────────────────────┐
201
+ │ Execute Original │
202
+ │ Method (sync/async) │
203
+ └────────┬─────────────┘
204
+
205
+ ┌────┴────┐
206
+ ▼ ▼
207
+ SUCCESS ERROR
208
+ │ │
209
+ ▼ ▼
210
+ ┌────────┐ ┌──────────────────────────────────────────────────────┐
211
+ │log() │ │ logger.error({ state: 'error', error: prettify(e) })│
212
+ │success │ │ (Axios errors auto-prettified) │
213
+ └────────┘ └──────────────────────────────────────────────────────┘
214
+ │ │
215
+ ▼ ▼
216
+ Return Re-throw
217
+ Result Error
176
218
  ```
177
219
 
178
220
  ## Usage
@@ -182,9 +224,9 @@ The `@Log()` decorator wraps your methods with automatic try-catch logging. It e
182
224
  Apply `@Log()` to specific methods for granular control:
183
225
 
184
226
  ```typescript
185
- class DataService {
186
- readonly logger = new Logger(DataService.name);
227
+ import { Log } from 'nestjs-log-decorator';
187
228
 
229
+ class DataService {
188
230
  @Log()
189
231
  async fetchData(id: number) {
190
232
  // This method is logged
@@ -203,14 +245,11 @@ class DataService {
203
245
  If you want to log all methods in a class, use the `@Log()` decorator on its definition:
204
246
 
205
247
  ```typescript
206
- import { Logger } from '@nestjs/common';
207
248
  import { Log } from 'nestjs-log-decorator';
208
249
 
209
250
  @Log()
210
251
  @Injectable()
211
252
  class PaymentService {
212
- readonly logger = new Logger(PaymentService.name);
213
-
214
253
  processPayment(amount: number, currency: string) {
215
254
  // Automatically logged on success or error
216
255
  return { status: 'completed', amount, currency };
@@ -232,8 +271,6 @@ import { Log, NoLog } from 'nestjs-log-decorator';
232
271
 
233
272
  @Log()
234
273
  class UserService {
235
- readonly logger = new Logger(UserService.name);
236
-
237
274
  createUser(name: string) {
238
275
  // Logged
239
276
  return { name };
@@ -268,8 +305,6 @@ Class-level with `onInvoke`:
268
305
  ```typescript
269
306
  @Log({ onInvoke: true })
270
307
  class ApiService {
271
- readonly logger = new Logger(ApiService.name);
272
-
273
308
  // All methods will log invocation + completion
274
309
  }
275
310
  ```
@@ -289,8 +324,6 @@ interface LargePayload {
289
324
  }
290
325
 
291
326
  class SyncService {
292
- readonly logger = new Logger(SyncService.name);
293
-
294
327
  // Only log the ID, exclude the large payload
295
328
  @Log({ args: (id: number, _payload: LargePayload) => ({ id }) })
296
329
  async syncData(id: number, payload: LargePayload) {
@@ -319,6 +352,31 @@ class SyncService {
319
352
  [SyncService] { method: 'lookupUser', state: 'success', args: '1:John' }
320
353
  ```
321
354
 
355
+ ### `result` — Success Result Logging
356
+
357
+ Control how return values are included in success logs:
358
+
359
+ - `result: true` logs the raw return value
360
+ - `result: (value) => ...` logs a formatted value
361
+
362
+ ```typescript
363
+ class PaymentService {
364
+ // Log full return value
365
+ @Log({ result: true })
366
+ createPayment(id: number) {
367
+ return { id, status: 'success', cardToken: 'tok_123' };
368
+ }
369
+
370
+ // Log only safe result fields
371
+ @Log({
372
+ result: (res: { id: number; status: string; cardToken: string }) => ({ id: res.id, status: res.status }),
373
+ })
374
+ createPaymentSafe(id: number) {
375
+ return { id, status: 'success', cardToken: 'tok_123' };
376
+ }
377
+ }
378
+ ```
379
+
322
380
  ## Log Format
323
381
 
324
382
  All logs are structured JSON objects:
@@ -329,7 +387,9 @@ All logs are structured JSON objects:
329
387
  {
330
388
  method: 'methodName',
331
389
  state: 'success',
332
- args: { param1: value1, param2: value2 }
390
+ args: { param1: value1, param2: value2 },
391
+ // Present only when `result` option is configured
392
+ result: { any: 'value' }
333
393
  }
334
394
  ```
335
395
 
@@ -437,12 +497,13 @@ async fetchData(url: string) {
437
497
 
438
498
  ### `Log(options?)`
439
499
 
440
- Decorator that can be applied to classes or methods.
500
+ Decorator that can be applied to classes or methods. When applied to a class, by default all methods are logged.
441
501
 
442
502
  | Option | Type | Default | Description |
443
503
  |--------|------|---------|-------------|
444
504
  | `onInvoke` | `boolean` | `false` | Log method invocation before execution |
445
505
  | `args` | `(...args) => any` | `undefined` | Custom function to format logged arguments |
506
+ | `result` | `true \| (result) => any` | `undefined` | Include and optionally format successful method result |
446
507
 
447
508
  ### `NoLog()`
448
509
 
@@ -459,7 +520,7 @@ import { Log, NoLog, LogOptions, Loggable, isLoggable } from 'nestjs-log-decorat
459
520
  | `Log` | Decorator | Main logging decorator |
460
521
  | `NoLog` | Decorator | Exclude method from logging |
461
522
  | `LogOptions` | Interface | Options for `@Log()` decorator |
462
- | `Loggable` | Interface | Interface for classes with a `logger` property |
523
+ | `Loggable` | Interface | Interface for classes with a `logger` property (optional) |
463
524
  | `isLoggable` | Function | Type guard to check if instance has logger |
464
525
 
465
526
  ## Advanced Example
@@ -471,6 +532,7 @@ import { Log, NoLog } from 'nestjs-log-decorator';
471
532
  @Log()
472
533
  @Injectable()
473
534
  export class OrderService {
535
+ // Optional: explicit logger takes precedence over auto-injected one
474
536
  readonly logger = new Logger(OrderService.name);
475
537
 
476
538
  constructor(
@@ -485,9 +547,9 @@ export class OrderService {
485
547
  }
486
548
 
487
549
  // Logged with invocation + custom args (exclude sensitive card data)
488
- @Log({
489
- onInvoke: true,
490
- args: (orderId: number, _cardDetails: CardDetails) => ({ orderId })
550
+ @Log({
551
+ onInvoke: true,
552
+ args: (orderId: number, _cardDetails: CardDetails) => ({ orderId })
491
553
  })
492
554
  async processPayment(orderId: number, cardDetails: CardDetails) {
493
555
  const result = await this.paymentGateway.charge(orderId, cardDetails);
package/dist/index.cjs CHANGED
@@ -1,4 +1,5 @@
1
- require("@nestjs/common");
1
+ let base_decorators = require("base-decorators");
2
+ let _nestjs_common = require("@nestjs/common");
2
3
 
3
4
  //#region src/axios/axios.stub.ts
4
5
  /**
@@ -67,35 +68,40 @@ var LogWrapper = class {
67
68
  this.method = method;
68
69
  this.args = args;
69
70
  }
70
- invoked() {
71
+ invoked(message) {
71
72
  this.logger.log({
72
73
  method: this.method,
73
74
  state: "invoked",
74
- args: this.args
75
+ args: this.args,
76
+ ...message ? { message } : {}
75
77
  });
76
78
  }
77
- success() {
79
+ success(message, additionalFields = {}) {
78
80
  this.logger.log({
79
81
  method: this.method,
80
82
  state: "success",
81
- args: this.args
83
+ args: this.args,
84
+ ...message ? { message } : {},
85
+ ...additionalFields
82
86
  });
83
87
  }
84
- error(error) {
88
+ error(error, message) {
85
89
  try {
86
90
  const pretifiedError = prettifyAxiosError(error);
87
91
  this.logger.error({
88
92
  method: this.method,
89
93
  state: "error",
90
94
  args: this.args,
91
- error: pretifiedError
95
+ error: pretifiedError,
96
+ ...message ? { message } : {}
92
97
  });
93
98
  } catch {
94
99
  this.logger.error({
95
100
  method: this.method,
96
101
  state: "error",
97
102
  args: this.args,
98
- error
103
+ error,
104
+ ...message ? { message } : {}
99
105
  });
100
106
  }
101
107
  }
@@ -104,95 +110,24 @@ const isLoggable = (instance) => {
104
110
  return typeof instance === "object" && instance !== null && "logger" in instance;
105
111
  };
106
112
  /**
107
- * Creates a LogWrapper instance with validated logger and built args object.
108
- * @internal
109
- */
110
- const createLogWrapper = (instance, className, methodName, argsObject) => {
111
- if (!isLoggable(instance)) throw new Error(`Logger not found in ${className}. Please add: readonly logger = new Logger(${className}.name)`);
112
- return new LogWrapper(instance.logger, methodName, argsObject);
113
- };
114
- /**
115
- * Builds an object mapping parameter names to their values.
116
- *
117
- * Creates a record where keys are parameter names and values are the
118
- * corresponding argument values passed to the function.
119
- *
120
- * @param parameterNames - Array of parameter names
121
- * @param args - Array of argument values
122
- * @returns Object mapping parameter names to values
123
- *
124
- * @example
125
- * buildArgsObject(['id', 'name'], [1, 'John'])
126
- * // Returns: { id: 1, name: 'John' }
127
- *
128
- * @internal
129
- */
130
- const buildArgsObject = (parameterNames, args) => {
131
- if (args.length === 0 && parameterNames.length === 0) return;
132
- const argsObject = {};
133
- parameterNames.forEach((paramName, index) => {
134
- if (index < args.length) argsObject[paramName] = args[index];
135
- });
136
- return argsObject;
137
- };
138
-
139
- //#endregion
140
- //#region src/decorate/applyToMethod.ts
141
- /**
142
- * Applies logging to a single method.
143
- * @internal
144
- */
145
- const applyToMethod = (target, propertyKey, descriptor, { onInvoke: shouldLogInvoke = false, args: formatArgs } = {}) => {
146
- const originalMethod = descriptor.value;
147
- const parameterNames = getParameterNames(originalMethod);
148
- descriptor.value = function(...args) {
149
- const argsObject = formatArgs ? formatArgs(...args) : buildArgsObject(parameterNames, args);
150
- const logWrapper = createLogWrapper(this, target.constructor.name, propertyKey, argsObject);
151
- if (shouldLogInvoke) logWrapper.invoked();
152
- try {
153
- const result = originalMethod.apply(this, args);
154
- if (result instanceof Promise) return handleAsyncExecution(result, logWrapper);
155
- logWrapper.success();
156
- return result;
157
- } catch (error) {
158
- logWrapper.error(error);
159
- throw error;
160
- }
161
- };
162
- return descriptor;
163
- };
164
- /**
165
- * Extracts parameter names from a function signature.
113
+ * Creates a LogWrapper instance for structured method logging.
166
114
  *
167
- * This function parses the function's string representation to extract
168
- * parameter names, handling TypeScript type annotations and default values.
115
+ * When the target instance already has a `logger` property (i.e., satisfies
116
+ * the {@link Loggable} interface), that logger is used directly. Otherwise,
117
+ * a new `Logger` from `@nestjs/common` is created with `className` as its
118
+ * context and assigned to `instance.logger`, so subsequent calls reuse it.
169
119
  *
170
- * @param func - The function to extract parameter names from
171
- * @returns Array of parameter names
172
- *
173
- * @example
174
- * function example(id: number, name: string = 'default') {}
175
- * getParameterNames(example) // Returns: ['id', 'name']
120
+ * @param instance - The class instance whose method is being logged
121
+ * @param className - The name of the class (used as Logger context if auto-injected)
122
+ * @param methodName - The name of the method being logged
123
+ * @param argsObject - Formatted arguments to include in the log entry
124
+ * @returns A configured {@link LogWrapper} ready for invoked/success/error calls
176
125
  *
177
126
  * @internal
178
127
  */
179
- const getParameterNames = (func) => {
180
- const match = func.toString().match(/\(([^)]*)\)/);
181
- if (!match?.[1]) return [];
182
- return match[1].split(",").map((param) => param.trim().split(/[=:]/)[0].trim()).filter((param) => param.length > 0);
183
- };
184
- /**
185
- * Handles execution of async methods with proper logging.
186
- * @internal
187
- */
188
- const handleAsyncExecution = (result, logWrapper) => {
189
- return result.then((value) => {
190
- logWrapper.success();
191
- return value;
192
- }).catch((error) => {
193
- logWrapper.error(error);
194
- throw error;
195
- });
128
+ const createLogWrapper = (instance, className, methodName, argsObject) => {
129
+ if (!isLoggable(instance)) instance.logger = new _nestjs_common.Logger(className);
130
+ return new LogWrapper(instance.logger, methodName, argsObject);
196
131
  };
197
132
 
198
133
  //#endregion
@@ -203,23 +138,6 @@ const handleAsyncExecution = (result, logWrapper) => {
203
138
  */
204
139
  const NO_LOG_METADATA_KEY = Symbol("noLog");
205
140
 
206
- //#endregion
207
- //#region src/decorate/applyToClass.ts
208
- /**
209
- * Applies logging to all methods in a class.
210
- * @internal
211
- */
212
- const applyToClass = (target, options) => {
213
- Object.getOwnPropertyNames(target.prototype).forEach((propertyName) => {
214
- if (propertyName === "constructor") return;
215
- const descriptor = Object.getOwnPropertyDescriptor(target.prototype, propertyName);
216
- if (!descriptor || typeof descriptor.value !== "function") return;
217
- if (descriptor.value[NO_LOG_METADATA_KEY] === true) return;
218
- applyToMethod(target.prototype, propertyName, descriptor, options);
219
- Object.defineProperty(target.prototype, propertyName, descriptor);
220
- });
221
- };
222
-
223
141
  //#endregion
224
142
  //#region src/log.decorator.ts
225
143
  /**
@@ -229,6 +147,7 @@ const applyToClass = (target, options) => {
229
147
  *
230
148
  * **Usage on Classes:**
231
149
  * When applied to a class, wraps all methods in the class with logging.
150
+ * A `logger` property is auto-injected if not already defined.
232
151
  *
233
152
  * **Usage on Methods:**
234
153
  * When applied to a method, wraps only that specific method with logging.
@@ -238,13 +157,10 @@ const applyToClass = (target, options) => {
238
157
  * - Successful completion with arguments
239
158
  * - Errors with arguments and error details (Axios errors are automatically prettified)
240
159
  *
241
- * **Requirements:**
242
- * The class must have a `logger` property (typically a NestJS Logger instance).
243
- *
244
160
  * **Log Format:**
245
161
  * All logs are output as structured objects with the following format:
246
162
  * - Invocation: `{ method: string, state: 'invoked', args: Record<string, any> }` (only when `onInvoke: true`)
247
- * - Success: `{ method: string, state: 'success', args: Record<string, any> }`
163
+ * - Success: `{ method: string, state: 'success', args: Record<string, any>, result?: unknown }`
248
164
  * - Error: `{ method: string, state: 'error', args: Record<string, any>, error: Error | PrettifiedAxiosError }`
249
165
  *
250
166
  * **Custom Argument Formatting:**
@@ -253,6 +169,11 @@ const applyToClass = (target, options) => {
253
169
  * - Logging only specific arguments
254
170
  * - Transforming sensitive data before logging
255
171
  *
172
+ * **Result Logging:**
173
+ * Use the `result` option to include successful return values in success logs.
174
+ * - `result: true` logs the raw returned value
175
+ * - `result: (value) => ...` logs a formatted value
176
+ *
256
177
  * **Error Handling:**
257
178
  * - Axios errors are automatically prettified using `prettifyAxiosError` to provide structured error information
258
179
  * - Regular errors are logged as-is
@@ -265,11 +186,9 @@ const applyToClass = (target, options) => {
265
186
  * - Methods with no arguments
266
187
  *
267
188
  * @example
268
- * // Class-level usage - logs all methods
189
+ * // Class-level usage - logs all methods, logger auto-injected
269
190
  * @Log()
270
191
  * class UserService {
271
- * readonly logger = new Logger(UserService.name)
272
- *
273
192
  * createUser(name: string, email: string) {
274
193
  * return { id: 1, name, email }
275
194
  * }
@@ -282,8 +201,6 @@ const applyToClass = (target, options) => {
282
201
  * @example
283
202
  * // Method-level usage - logs only specific method
284
203
  * class DataService {
285
- * readonly logger = new Logger(DataService.name)
286
- *
287
204
  * @Log({ onInvoke: true })
288
205
  * async fetchData(id: number) {
289
206
  * const data = await this.repository.findById(id)
@@ -295,8 +212,7 @@ const applyToClass = (target, options) => {
295
212
  * return 'helper'
296
213
  * }
297
214
  * }
298
- *
299
- * @example
215
+ *
300
216
  * // Error handling with regular errors
301
217
  * class PaymentService {
302
218
  * readonly logger = new Logger(PaymentService.name)
@@ -342,17 +258,15 @@ const applyToClass = (target, options) => {
342
258
  * @example
343
259
  * // Custom argument formatting - exclude large objects from logs
344
260
  * class SyncService {
345
- * readonly logger = new Logger(SyncService.name)
346
- *
347
261
  * @Log({ args: (loanId: number) => ({ loanId }) })
348
- * async syncLoan(loanId: number, loanData?: CloudbankinLoan) {
262
+ * async syncLoan(loanId: number, loanData?: unknown) {
349
263
  * // loanData is excluded from logs due to large size
350
264
  * // Only loanId will be logged
351
265
  * return this.processLoan(loanId, loanData)
352
266
  * }
353
- *
267
+ *
354
268
  * @Log({ args: (loanId: number, transactionId: number) => ({ loanId, transactionId }) })
355
- * async syncPayment(loanId: number, transactionId: number, loanData?: CloudbankinLoan) {
269
+ * async syncPayment(loanId: number, transactionId: number, loanData?: unknown) {
356
270
  * // Only loanId and transactionId are logged, loanData is excluded
357
271
  * return this.processPayment(loanId, transactionId, loanData)
358
272
  * }
@@ -361,22 +275,26 @@ const applyToClass = (target, options) => {
361
275
  * // Logs output:
362
276
  * // [SyncService] { method: 'syncLoan', state: 'success', args: { loanId: 123 } }
363
277
  * // [SyncService] { method: 'syncPayment', state: 'success', args: { loanId: 123, transactionId: 456 } }
364
- *
278
+ *
365
279
  * @param options - Configuration options for the decorator
366
- * @throws {Error} If the logger property is not found in the class instance
367
- *
368
280
  * @returns Decorator function that can be applied to classes or methods
369
281
  */
370
- const Log = (options = {}) => {
371
- return ((target, propertyKey, descriptor) => {
372
- if (propertyKey === void 0) {
373
- applyToClass(target, options);
374
- return target;
282
+ const Log = ({ onInvoke: shouldLogInvoke = false, args: formatArgs, result: formatResult } = {}) => (0, base_decorators.Effect)(({ args, argsObject, target, propertyKey, className }) => {
283
+ const formattedArgs = formatArgs ? formatArgs(...args) : argsObject;
284
+ const resultFormatter = typeof formatResult === "function" ? formatResult : (value) => value;
285
+ const logger = createLogWrapper(target, className, String(propertyKey), formattedArgs);
286
+ return {
287
+ onInvoke: shouldLogInvoke ? () => logger.invoked() : void 0,
288
+ onReturn: ({ result }) => {
289
+ logger.success(void 0, formatResult ? { result: resultFormatter(result) } : void 0);
290
+ return result;
291
+ },
292
+ onError: ({ error }) => {
293
+ logger.error(error);
294
+ throw error;
375
295
  }
376
- if (descriptor !== void 0) return applyToMethod(target, propertyKey, descriptor, options);
377
- throw new Error("Log decorator can only be applied to classes or methods");
378
- });
379
- };
296
+ };
297
+ }, NO_LOG_METADATA_KEY);
380
298
  /**
381
299
  * Method decorator that prevents logging when used with class-level @Log()
382
300
  *
@@ -388,8 +306,6 @@ const Log = (options = {}) => {
388
306
  * @example
389
307
  * @Log()
390
308
  * class UserService {
391
- * readonly logger = new Logger(UserService.name)
392
- *
393
309
  * createUser(name: string) {
394
310
  * // This will be logged
395
311
  * return { name }
@@ -425,27 +341,23 @@ const Log = (options = {}) => {
425
341
  * return 'helper'
426
342
  * }
427
343
  * }
428
- *
344
+ *
429
345
  * @returns {MethodDecorator} The method decorator function
430
346
  */
431
- const NoLog = () => {
432
- return (_target, _propertyKey, descriptor) => {
433
- const value = descriptor.value;
434
- value[NO_LOG_METADATA_KEY] = true;
435
- return descriptor;
436
- };
437
- };
347
+ const NoLog = () => (0, base_decorators.SetMeta)(NO_LOG_METADATA_KEY, true);
348
+
349
+ //#endregion
350
+ //#region src/axios/isTimoutError.ts
351
+ function isTimeoutError(error) {
352
+ return error.code === "ECONNABORTED" || error.code === "ETIMEDOUT";
353
+ }
438
354
 
439
355
  //#endregion
440
356
  exports.Log = Log;
441
357
  exports.LogWrapper = LogWrapper;
442
358
  exports.NO_LOG_METADATA_KEY = NO_LOG_METADATA_KEY;
443
359
  exports.NoLog = NoLog;
444
- exports.applyToClass = applyToClass;
445
- exports.applyToMethod = applyToMethod;
446
- exports.buildArgsObject = buildArgsObject;
447
360
  exports.createLogWrapper = createLogWrapper;
448
- exports.getParameterNames = getParameterNames;
449
- exports.handleAsyncExecution = handleAsyncExecution;
450
361
  exports.isLoggable = isLoggable;
362
+ exports.isTimeoutError = isTimeoutError;
451
363
  exports.prettifyAxiosError = prettifyAxiosError;
package/dist/index.d.cts CHANGED
@@ -2,10 +2,11 @@ import { Logger } from "@nestjs/common";
2
2
 
3
3
  //#region src/types.d.ts
4
4
  type LogArgsFormatter<TArgs extends unknown[]> = (...args: TArgs) => string | number | Record<string, unknown> | undefined;
5
+ type LogResultFormatter<TResult = unknown> = (result: TResult) => unknown;
5
6
  /**
6
7
  * Configuration options for the Log decorator.
7
8
  */
8
- interface LogOptions<TArgs extends unknown[] = unknown[]> {
9
+ interface LogOptions<TArgs extends unknown[] = unknown[], TResult = unknown> {
9
10
  /**
10
11
  * When true, logs method invocation with arguments.
11
12
  * When false or not set, only logs success and error states.
@@ -28,6 +29,21 @@ interface LogOptions<TArgs extends unknown[] = unknown[]> {
28
29
  * @returns Formatted arguments as string, number, object, or undefined
29
30
  */
30
31
  args?: LogArgsFormatter<TArgs>;
32
+ /**
33
+ * Controls how successful method result is logged.
34
+ *
35
+ * - `true`: logs the returned result value as-is
36
+ * - formatter function: receives the method result and returns a formatted value
37
+ *
38
+ * @example
39
+ * // Log raw result
40
+ * @Log({ result: true })
41
+ *
42
+ * @example
43
+ * // Log only selected result fields
44
+ * @Log({ result: (res: { id: number; data: unknown }) => ({ id: res.id }) })
45
+ */
46
+ result?: true | LogResultFormatter<TResult>;
31
47
  }
32
48
  /**
33
49
  * Symbol used to mark methods that should not be logged
@@ -35,59 +51,23 @@ interface LogOptions<TArgs extends unknown[] = unknown[]> {
35
51
  */
36
52
  declare const NO_LOG_METADATA_KEY: unique symbol;
37
53
  //#endregion
38
- //#region src/LogWrapper.d.ts
39
- /**
40
- * Wrapper class for logging operations with consistent format.
41
- * @internal
42
- */
43
- declare class LogWrapper {
44
- private readonly logger;
45
- private readonly method;
46
- private readonly args;
47
- constructor(logger: Logger, method: string, args: string | number | Record<string, unknown> | undefined);
48
- invoked(): void;
49
- success(): void;
50
- error(error: unknown): void;
51
- }
52
- /** If you see it, then you probably forgot to add `readonly logger = new Logger(YourClass.name)` to your class */
53
- interface Loggable {
54
- logger: Logger;
55
- }
56
- declare const isLoggable: (instance: unknown) => instance is Loggable;
57
- /**
58
- * Creates a LogWrapper instance with validated logger and built args object.
59
- * @internal
60
- */
61
- declare const createLogWrapper: (instance: unknown, className: string, methodName: string, argsObject: string | number | Record<string, unknown> | undefined) => LogWrapper;
62
- /**
63
- * Builds an object mapping parameter names to their values.
64
- *
65
- * Creates a record where keys are parameter names and values are the
66
- * corresponding argument values passed to the function.
67
- *
68
- * @param parameterNames - Array of parameter names
69
- * @param args - Array of argument values
70
- * @returns Object mapping parameter names to values
71
- *
72
- * @example
73
- * buildArgsObject(['id', 'name'], [1, 'John'])
74
- * // Returns: { id: 1, name: 'John' }
75
- *
76
- * @internal
77
- */
78
- declare const buildArgsObject: (parameterNames: string[], args: unknown[]) => Record<string, unknown> | undefined;
79
- //#endregion
80
54
  //#region src/log.decorator.d.ts
81
- type LoggableConstructor = new (...args: any[]) => Loggable;
82
55
  /**
83
56
  * Decorator function that can be applied to classes or methods.
84
57
  * Uses overloads to provide type checking for class decorators while
85
58
  * keeping method decorators flexible.
86
59
  */
87
60
  interface LogDecorator {
88
- /** Class decorator - enforces that class instances have a `logger` property */
89
- <T extends LoggableConstructor>(target: T): T;
90
- /** Method decorator - no compile-time constraint on `this` */
61
+ /**
62
+ * Class decorator - applies logging to all methods in the class.
63
+ * The class no longer needs to define a `logger` property; one will
64
+ * be auto-injected if missing.
65
+ */
66
+ <T extends new (...args: any[]) => unknown>(target: T): T;
67
+ /**
68
+ * Method decorator - no compile-time constraint on `this`.
69
+ * Wraps only the specific method with logging.
70
+ */
91
71
  (target: object, propertyKey: string | symbol, descriptor: PropertyDescriptor): PropertyDescriptor;
92
72
  }
93
73
  /**
@@ -97,6 +77,7 @@ interface LogDecorator {
97
77
  *
98
78
  * **Usage on Classes:**
99
79
  * When applied to a class, wraps all methods in the class with logging.
80
+ * A `logger` property is auto-injected if not already defined.
100
81
  *
101
82
  * **Usage on Methods:**
102
83
  * When applied to a method, wraps only that specific method with logging.
@@ -106,13 +87,10 @@ interface LogDecorator {
106
87
  * - Successful completion with arguments
107
88
  * - Errors with arguments and error details (Axios errors are automatically prettified)
108
89
  *
109
- * **Requirements:**
110
- * The class must have a `logger` property (typically a NestJS Logger instance).
111
- *
112
90
  * **Log Format:**
113
91
  * All logs are output as structured objects with the following format:
114
92
  * - Invocation: `{ method: string, state: 'invoked', args: Record<string, any> }` (only when `onInvoke: true`)
115
- * - Success: `{ method: string, state: 'success', args: Record<string, any> }`
93
+ * - Success: `{ method: string, state: 'success', args: Record<string, any>, result?: unknown }`
116
94
  * - Error: `{ method: string, state: 'error', args: Record<string, any>, error: Error | PrettifiedAxiosError }`
117
95
  *
118
96
  * **Custom Argument Formatting:**
@@ -121,6 +99,11 @@ interface LogDecorator {
121
99
  * - Logging only specific arguments
122
100
  * - Transforming sensitive data before logging
123
101
  *
102
+ * **Result Logging:**
103
+ * Use the `result` option to include successful return values in success logs.
104
+ * - `result: true` logs the raw returned value
105
+ * - `result: (value) => ...` logs a formatted value
106
+ *
124
107
  * **Error Handling:**
125
108
  * - Axios errors are automatically prettified using `prettifyAxiosError` to provide structured error information
126
109
  * - Regular errors are logged as-is
@@ -133,11 +116,9 @@ interface LogDecorator {
133
116
  * - Methods with no arguments
134
117
  *
135
118
  * @example
136
- * // Class-level usage - logs all methods
119
+ * // Class-level usage - logs all methods, logger auto-injected
137
120
  * @Log()
138
121
  * class UserService {
139
- * readonly logger = new Logger(UserService.name)
140
- *
141
122
  * createUser(name: string, email: string) {
142
123
  * return { id: 1, name, email }
143
124
  * }
@@ -150,8 +131,6 @@ interface LogDecorator {
150
131
  * @example
151
132
  * // Method-level usage - logs only specific method
152
133
  * class DataService {
153
- * readonly logger = new Logger(DataService.name)
154
- *
155
134
  * @Log({ onInvoke: true })
156
135
  * async fetchData(id: number) {
157
136
  * const data = await this.repository.findById(id)
@@ -164,7 +143,6 @@ interface LogDecorator {
164
143
  * }
165
144
  * }
166
145
  *
167
- * @example
168
146
  * // Error handling with regular errors
169
147
  * class PaymentService {
170
148
  * readonly logger = new Logger(PaymentService.name)
@@ -210,17 +188,15 @@ interface LogDecorator {
210
188
  * @example
211
189
  * // Custom argument formatting - exclude large objects from logs
212
190
  * class SyncService {
213
- * readonly logger = new Logger(SyncService.name)
214
- *
215
191
  * @Log({ args: (loanId: number) => ({ loanId }) })
216
- * async syncLoan(loanId: number, loanData?: CloudbankinLoan) {
192
+ * async syncLoan(loanId: number, loanData?: unknown) {
217
193
  * // loanData is excluded from logs due to large size
218
194
  * // Only loanId will be logged
219
195
  * return this.processLoan(loanId, loanData)
220
196
  * }
221
197
  *
222
198
  * @Log({ args: (loanId: number, transactionId: number) => ({ loanId, transactionId }) })
223
- * async syncPayment(loanId: number, transactionId: number, loanData?: CloudbankinLoan) {
199
+ * async syncPayment(loanId: number, transactionId: number, loanData?: unknown) {
224
200
  * // Only loanId and transactionId are logged, loanData is excluded
225
201
  * return this.processPayment(loanId, transactionId, loanData)
226
202
  * }
@@ -231,11 +207,13 @@ interface LogDecorator {
231
207
  * // [SyncService] { method: 'syncPayment', state: 'success', args: { loanId: 123, transactionId: 456 } }
232
208
  *
233
209
  * @param options - Configuration options for the decorator
234
- * @throws {Error} If the logger property is not found in the class instance
235
- *
236
210
  * @returns Decorator function that can be applied to classes or methods
237
211
  */
238
- declare const Log: <TArgs extends unknown[]>(options?: LogOptions<TArgs>) => LogDecorator;
212
+ declare const Log: <TArgs extends unknown[], TResult = unknown>({
213
+ onInvoke: shouldLogInvoke,
214
+ args: formatArgs,
215
+ result: formatResult
216
+ }?: LogOptions<TArgs, TResult>) => LogDecorator;
239
217
  /**
240
218
  * Method decorator that prevents logging when used with class-level @Log()
241
219
  *
@@ -247,8 +225,6 @@ declare const Log: <TArgs extends unknown[]>(options?: LogOptions<TArgs>) => Log
247
225
  * @example
248
226
  * @Log()
249
227
  * class UserService {
250
- * readonly logger = new Logger(UserService.name)
251
- *
252
228
  * createUser(name: string) {
253
229
  * // This will be logged
254
230
  * return { name }
@@ -287,51 +263,91 @@ declare const Log: <TArgs extends unknown[]>(options?: LogOptions<TArgs>) => Log
287
263
  *
288
264
  * @returns {MethodDecorator} The method decorator function
289
265
  */
290
- declare const NoLog: () => (_target: unknown, _propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
266
+ declare const NoLog: () => MethodDecorator;
291
267
  //#endregion
292
- //#region src/axios/axios.logger.d.ts
293
- declare function prettifyAxiosError(error: unknown): unknown;
294
- //#endregion
295
- //#region src/decorate/applyToMethod.d.ts
296
- type MethodFunction = (...args: unknown[]) => unknown;
268
+ //#region src/LogWrapper.d.ts
297
269
  /**
298
- * Applies logging to a single method.
270
+ * Wrapper class for logging operations with consistent format.
299
271
  * @internal
300
272
  */
301
- declare const applyToMethod: (target: object, propertyKey: string, descriptor: PropertyDescriptor, {
302
- onInvoke: shouldLogInvoke,
303
- args: formatArgs
304
- }?: LogOptions) => PropertyDescriptor;
273
+ declare class LogWrapper {
274
+ private readonly logger;
275
+ private readonly method;
276
+ private readonly args;
277
+ constructor(logger: Logger, method: string, args: string | number | Record<string, unknown> | undefined);
278
+ invoked(message?: string): void;
279
+ success(message?: string, additionalFields?: Record<string, unknown>): void;
280
+ error(error: unknown, message?: string): void;
281
+ }
305
282
  /**
306
- * Extracts parameter names from a function signature.
283
+ * Interface for classes that expose a NestJS Logger instance.
307
284
  *
308
- * This function parses the function's string representation to extract
309
- * parameter names, handling TypeScript type annotations and default values.
285
+ * Implementing this interface is optional when using the `@Log()` decorator.
286
+ * If a class does not define a `logger` property, the decorator will
287
+ * automatically inject a `new Logger(ClassName)` instance at runtime.
310
288
  *
311
- * @param func - The function to extract parameter names from
312
- * @returns Array of parameter names
313
- *
314
- * @example
315
- * function example(id: number, name: string = 'default') {}
316
- * getParameterNames(example) // Returns: ['id', 'name']
317
- *
318
- * @internal
289
+ * You may still implement this interface explicitly to provide a custom logger
290
+ * (e.g., a mock for testing, or a logger with a non-default context).
319
291
  */
320
- declare const getParameterNames: (func: MethodFunction) => string[];
292
+ interface Loggable {
293
+ logger: Logger;
294
+ }
295
+ declare const isLoggable: (instance: unknown) => instance is Loggable;
321
296
  /**
322
- * Handles execution of async methods with proper logging.
297
+ * Creates a LogWrapper instance for structured method logging.
298
+ *
299
+ * When the target instance already has a `logger` property (i.e., satisfies
300
+ * the {@link Loggable} interface), that logger is used directly. Otherwise,
301
+ * a new `Logger` from `@nestjs/common` is created with `className` as its
302
+ * context and assigned to `instance.logger`, so subsequent calls reuse it.
303
+ *
304
+ * @param instance - The class instance whose method is being logged
305
+ * @param className - The name of the class (used as Logger context if auto-injected)
306
+ * @param methodName - The name of the method being logged
307
+ * @param argsObject - Formatted arguments to include in the log entry
308
+ * @returns A configured {@link LogWrapper} ready for invoked/success/error calls
309
+ *
323
310
  * @internal
324
311
  */
325
- declare const handleAsyncExecution: (result: Promise<unknown>, logWrapper: LogWrapper) => Promise<unknown>;
312
+ declare const createLogWrapper: (instance: unknown, className: string, methodName: string, argsObject: string | number | Record<string, unknown> | undefined) => LogWrapper;
326
313
  //#endregion
327
- //#region src/decorate/applyToClass.d.ts
328
- interface Constructor {
329
- prototype: Record<string, unknown>;
330
- }
314
+ //#region src/axios/axios.logger.d.ts
315
+ declare function prettifyAxiosError(error: unknown): unknown;
316
+ //#endregion
317
+ //#region src/axios/axios.stub.d.ts
331
318
  /**
332
- * Applies logging to all methods in a class.
333
- * @internal
334
- */
335
- declare const applyToClass: (target: Constructor, options: LogOptions) => void;
319
+ * Stub for axios types and utils, to avoid ading axios as a dependency
320
+ *
321
+ * Please avoid adding business logic to this file.
322
+ * */
323
+ interface AxiosError extends Error {
324
+ name: string;
325
+ message: string;
326
+ stack?: string;
327
+ isAxiosError: true;
328
+ config: AxiosRequestConfig;
329
+ response: AxiosResponse;
330
+ code?: string;
331
+ request?: any;
332
+ status?: number;
333
+ }
334
+ interface AxiosRequestConfig {
335
+ url?: string;
336
+ method?: string;
337
+ baseURL?: string;
338
+ path?: string;
339
+ headers?: unknown;
340
+ data?: unknown;
341
+ params?: unknown;
342
+ }
343
+ interface AxiosResponse {
344
+ status: number;
345
+ statusText: string;
346
+ data: unknown;
347
+ headers: unknown;
348
+ }
349
+ //#endregion
350
+ //#region src/axios/isTimoutError.d.ts
351
+ declare function isTimeoutError(error: AxiosError): boolean;
336
352
  //#endregion
337
- export { Log, LogArgsFormatter, LogOptions, LogWrapper, Loggable, NO_LOG_METADATA_KEY, NoLog, applyToClass, applyToMethod, buildArgsObject, createLogWrapper, getParameterNames, handleAsyncExecution, isLoggable, prettifyAxiosError };
353
+ export { Log, LogArgsFormatter, LogOptions, LogResultFormatter, LogWrapper, Loggable, NO_LOG_METADATA_KEY, NoLog, createLogWrapper, isLoggable, isTimeoutError, prettifyAxiosError };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "nestjs-log-decorator",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Decorator that removes try catch boilerplate logging from NestJS service methods",
5
- "author": "Vlad Goncharov <leovs010@gmail.com>",
5
+ "author": "Vlad Goncharov <vlad.goncharov@neolab.com>",
6
6
  "license": "AGPL-3.0",
7
7
  "homepage": "https://github.com/NeoLabHQ/nestjs-log-decorator#readme",
8
8
  "repository": {
@@ -24,13 +24,18 @@
24
24
  "scripts": {
25
25
  "build": "tsdown",
26
26
  "build:watch": "tsdown --watch",
27
- "dev": "concurrently -c auto --names build,test npm:build:watch npm:test",
28
- "test": "vitest",
27
+ "dev": "concurrently -c auto --names build,test npm:build:watch npm:test:watch",
28
+ "lint": "npm run typecheck",
29
+ "test": "vitest --run",
30
+ "test:watch": "vitest",
29
31
  "typecheck": "tsc --noEmit",
30
32
  "prepublishOnly": "npm run build",
31
33
  "commit": "cz help",
32
34
  "cz": "git add . && cz"
33
35
  },
36
+ "dependencies": {
37
+ "base-decorators": "^0.1.1"
38
+ },
34
39
  "devDependencies": {
35
40
  "@semantic-release/git": "^10.0.1",
36
41
  "@types/node": "^25.0.3",
@@ -49,5 +54,14 @@
49
54
  "commitizen": {
50
55
  "path": "./node_modules/cz-conventional-changelog"
51
56
  }
52
- }
57
+ },
58
+ "keywords": [
59
+ "decorator",
60
+ "decorators",
61
+ "library",
62
+ "nestjs",
63
+ "typescript",
64
+ "utility",
65
+ "logger"
66
+ ]
53
67
  }