Simplify Your NestJS Logging with a Single Decorator
Tired of writing repetitive logging code across your NestJS app? This guide shows you how to streamline logging with a single decorator that logs method arguments, results, and execution time—plus a simple way to exclude sensitive return data.
Introduction
I remember when I first started building NestJS applications, one of the most annoying parts was keeping my logging consistent. I had logging sprinkled all over the place—some console.log
statements here, a custom logger call there—and it always felt messy. On top of that, I sometimes needed to log the start and end of method executions, arguments passed in, the results returned out, and even how long each method took to run.
Eventually, I thought: There’s got to be a better way. That’s when I discovered how powerful decorators can be. Instead of cluttering my code with logger.info()
calls everywhere, I could write a single decorator to handle all the logging for me, both at the class and method level. Even better, with a little bit of metadata, I could turn off return-value logging for sensitive methods easily.
In this post, I’ll show you exactly how I solved this problem so you can simplify logging in your own NestJS projects.
Why Use a Decorator for Logging?
Decorators in TypeScript are basically functions you can attach to classes or methods. They allow you to inject extra behavior without touching the core logic. For logging, this is perfect: you just slap @LogActivity()
on your class or method, and suddenly every invocation is logged.
This approach means:
- You don’t need repetitive logger calls in every method.
- You can easily toggle logging on or off at a broad (class) or fine-grained (method) level.
- You keep your business logic clean and focus on what it does, not how it’s logged.
Meet @LogActivity()
and @ExcludeReturnDataLog()
I’ve created two decorators:
@LogActivity()
:
Apply this to a class to automatically log all its methods or apply it to a single method to log just that one. It logs the method name, arguments, execution time, and return value by default.@ExcludeReturnDataLog()
:
If you have a method that returns sensitive or huge amounts of data you don’t want in your logs, just add this decorator, and@LogActivity()
will skip logging the return value for that method.
The Code
Here’s the code for the decorators. Feel free to copy and paste it into a file like log-activity.decorator.ts
in your project. Make sure you have a CustomLogger
class (you can replace it with your own logging solution if you prefer).
log-activity.decorator.ts
/* eslint-disable @typescript-eslint/no-explicit-any */
import { SetMetadata, applyDecorators } from '@nestjs/common';
import 'reflect-metadata';
import { CustomLogger } from 'src/common/customLogger';
const EXCLUDE_RETURN_DATA_LOG_METADATA_KEY = 'excludeReturnDataLog';
export function ExcludeReturnDataLog(): MethodDecorator {
return applyDecorators(
SetMetadata(EXCLUDE_RETURN_DATA_LOG_METADATA_KEY, true),
);
}
export function LogActivity(
options: {
excludeReturnDataLog?: boolean;
} = {},
): any {
const logger = new CustomLogger('LogActivity');
return function (
target: any,
propertyKey?: string,
descriptor?: PropertyDescriptor,
) {
if (!descriptor) {
// If you put @LogActivity() above a class, we apply it to all methods.
for (const key of Object.getOwnPropertyNames(target.prototype)) {
const method = target.prototype[key];
if (typeof method === 'function' && key !== 'constructor') {
const methodDescriptor = Object.getOwnPropertyDescriptor(
target.prototype,
key,
);
if (methodDescriptor) {
Object.defineProperty(
target.prototype,
key,
LogActivity(options)(target, key, methodDescriptor),
);
}
}
}
} else {
// If you put @LogActivity() on a method, we just wrap that method.
const originalMethod = descriptor.value;
const wrappedMethod = function (...args: any[]): any {
const className =
target.constructor &&
target.constructor.name &&
target.constructor.name !== 'Function'
? target.constructor.name
: target.name || 'UnknownClass';
const methodName = propertyKey || 'unknownMethod';
logger.logInfo(`${className}.${methodName} : execution`, 'Starting');
logger.logInfo(`${className}.${methodName} : arguments`, args);
const start = Date.now();
const executeMethod = async (): Promise<any> => {
try {
const result = await originalMethod.apply(this, args);
const excludeResponseLog = Reflect.getMetadata(
EXCLUDE_RETURN_DATA_LOG_METADATA_KEY,
originalMethod,
);
if (!excludeResponseLog && !options.excludeReturnDataLog) {
logger.logInfo(`${className}.${methodName} : result`, result);
}
logger.logInfo(`${className}.${methodName} : execution`, 'Finished');
return result;
} catch (error) {
logger.logInfo(`${className}.${methodName} : error`, {
error: error?.message,
});
logger.error(`${className}.${methodName}: error`, error);
throw error;
} finally {
const executionTime = Date.now() - start;
logger.logInfo(
`${className}.${methodName} : time taken (ms)`,
executionTime,
);
}
};
const executeSyncMethod = (): any => {
try {
const result = originalMethod.apply(this, args);
const excludeResponseLog = Reflect.getMetadata(
EXCLUDE_RETURN_DATA_LOG_METADATA_KEY,
originalMethod,
);
if (!excludeResponseLog && !options.excludeReturnDataLog) {
logger.logInfo(`${className}.${methodName} : result`, result);
}
logger.logInfo(`${className}.${methodName} : execution`, 'Finished');
return result;
} catch (error) {
logger.logInfo(`${className}.${methodName} : error`, {
error: error?.message,
});
logger.error(`${className}.${methodName}: error`, error);
throw error;
} finally {
const executionTime = Date.now() - start;
logger.logInfo(
`${className}.${methodName} : time taken (ms)`,
executionTime,
);
}
};
return originalMethod.constructor.name === 'AsyncFunction'
? executeMethod()
: executeSyncMethod();
};
const metadataKeys = Reflect.getMetadataKeys(originalMethod) || [];
metadataKeys.forEach((key) => {
const metadata = Reflect.getMetadata(key, originalMethod);
Reflect.defineMetadata(key, metadata, wrappedMethod);
});
descriptor.value = wrappedMethod;
return descriptor;
}
return undefined;
};
}
Using the Decorators in Your Code
Apply at the Class Level:
import { Injectable } from '@nestjs/common';
import { LogActivity, ExcludeReturnDataLog } from './log-activity.decorator';
@LogActivity() // This logs every method in the class
@Injectable()
export class UserService {
getUserById(id: number) {
// ... your code
return { id, name: 'John Doe' };
}
@ExcludeReturnDataLog()
updatePassword(userId: number, newPassword: string) {
// ... your code
return { success: true, message: 'Password updated!' };
}
}
In UserService
, both getUserById
and updatePassword
get logged. But since updatePassword
is decorated with @ExcludeReturnDataLog()
, its return data is never logged.
Apply at the Method Level:
import { Injectable } from '@nestjs/common';
import { LogActivity } from './log-activity.decorator';
@Injectable()
export class ProductService {
constructor(private readonly productRepository: ProductRepository) {}
@LogActivity() // Only logs this method
async findAllProducts() {
return await this.productRepository.findAll();
}
// Any other methods in this class won't be logged unless you decorate them too
}
Why This Makes Your Life Easier
No More Messy Logging:
You won’t have to remember to log arguments or results each time you write a new method. Just apply the decorator and focus on your logic.
Easy to Turn On/Off:
If you want to stop logging return values globally, you could modify the @LogActivity()
options. Or if you suddenly need extra logs for debugging, just apply the decorator to a class or method, and it’s done.
Safe for Sensitive Data:
Don’t want to log passwords or tokens? Use @ExcludeReturnDataLog()
, and you’re covered.
Cleaner and More Maintainable Code:
As your codebase grows, keeping logging consistent and clean becomes harder. This approach scales beautifully because you centralize logging logic in one place.
Tips
- If you’re dealing with highly sensitive data, ensure that no logs are ever stored insecurely. Consider masking or hashing sensitive fields before logging.
- You can customize the
CustomLogger
to send logs to external services like ElasticSearch, Amazon CloudWatch, or any logging platform you prefer. - For extremely high-traffic methods, consider logging asynchronously or sampling logs to manage performance.
Conclusion
Dealing with scattered, repetitive logging calls was driving me crazy until I discovered I could centralize it all with a couple of decorators. By using @LogActivity()
and @ExcludeReturnDataLog()
, I’ve made my code cleaner, my logs more consistent, and my life a whole lot easier.
Give these decorators a try in your NestJS application and see how much simpler it is to keep track of what’s going on under the hood—without drowning in console.log
statements.
Comments