My team and I have been deeply contemplating the best location for writing a debug-level log during our development process.
We are utilizing winston
in conjunction with winston-daily-rotate-file
to separate out different aspects of logging, as well as nest-winston
, a Nest module wrapper for winston logger.
After much consideration, we have decided to create a customizable logger service by extending the built-in Logger class.
@Injectable()
export class LoggerService extends Logger {
constructor(
@Inject('winston')
private readonly logger: winston.Logger,
) { super(); }
info(message: string, context?: string): void {
this.logger.info(message, { context });
super.log(message, context);
}
debug(message: string, context?: string): void {
// To delegate the call to the parent class, no winston used.
super.debug(message, context);
}
error(message: string, trace: string, context?: string): void {
this.logger.error(message, { context });
super.error(message, trace, context);
}
}
One key aspect is that we have intentionally not configured the storage device (Transports) at the debug level. This ensures that logs are only printed to the console during development.
With our custom LoggerService in place, it can now be utilized anywhere within the same context. For example,
@Controller('users')
export class UsersController {
constructor(private readonly logger: LoggerService) {}
}
@Injectable()
export class UsersService {
constructor(private readonly logger: LoggerService) {}
// Within a method, perform debugging logic.
this.logger.debug(message, UsersService.name);
}
While this approach seems adequate initially, it may lead to code becoming messy if overused elsewhere.
To address this concern, we contemplated centralizing the debugging process and arrived at the idea of utilizing interceptors to handle this task.
import { LoggerService } from '../../logger/logger.service';
@Injectable()
export class DebuggingInterceptor implements NestInterceptor {
constructor(private readonly logger: LoggerService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const ctx = `${context.getClass().name} ➜ ${context.getHandler().name}()`;
return next
.handle()
.pipe(
tap((response) => {
if (process.env.NODE_ENV === 'development') {
this.logger.debug(response, ctx);
}
}),
);
}
}
The act of checking the environment before printing the debug log seems clunky to me.
I have concerns that perhaps relying on the interceptor approach above might not be entirely correct?
How could I go about addressing this issue in a more efficient manner?