4 min read
Implementing a custom logger in NestJS

NestJS comes with a built in text-based logger that’s quite useful but for production grade logging you can implement your own system fairly easily. Express apps typically use winston, morgan or a combination of the two and there are several guides on how to implement this integration. While there are many guides available, finding one that integrates both for a NestJS application can be challenging. This guide demonstrates one approach to implementing this integration.

If you haven’t used these libraries before winston is a very flexible logger that supports logging to multiple transports (output streams, basically), while morgan is specifically an HTTP request logger that’s enabled as middleware in your app.

npm i winston morgan @types/morgan

Following the standard NestJS pattern, implement your logger as a module that can be injected as a dependency wherever needed. Per the docs, to enable dependency injection for your custom logger, create a class that implements LoggerService and register that class as a provider.

src/logger/logger.ts

import {
  createLogger,
  transports,
  format,
  Logger as InternalLogger,
} from 'winston'

@Injectable
export class WinstonLogger implements LoggerService {
  private readonly logger: InternalLogger

  constructor() {
    this.logger = createLogger({
      level: 'debug',
      format: format.json(),
      transports: [new transports.Console()],
    })
  }

  log(message: any, ...optionalParams: any[]) {
    this.logger.info(message, optionalParams)
  }
  error(message: any, ...optionalParams: any[]) {
    this.logger.error(message, optionalParams)
  }
  warn(message: any, ...optionalParams: any[]) {
    this.logger.warn(message, optionalParams)
  }
  debug?(message: any, ...optionalParams: any[]) {
    this.logger.debug(message, optionalParams)
  }
  verbose?(message: any, ...optionalParams: any[]) {
    this.logger.verbose(message, optionalParams)
  }
  fatal?(message: any, ...optionalParams: any[]) {
    this.logger.error(message, optionalParams)
  }
  setLogLevels?(levels: LogLevel[]) {}
}

This creates a winston logger with a basic configuration. Note that this should be customized based on production or dev environments. Extending LoggerService requires providing an implementation for log methods, which you can pass through to winston. To use morgan on top of this for standard HTTP logging, add the following.

src/logger/httpLogger.middleware.ts

import { Injectable, NestMiddleware } from '@nestjs/common'
import { Request, Response, NextFunction } from 'express'
import { WinstonLogger } from './logger'
import * as morgan from 'morgan'

@Injectable()
export class HTTPLoggerMiddleware implements NestMiddleware {
  constructor(private readonly logger: WinstonLogger) {}

  use(req: Request, res: Response, next: NextFunction) {
    morgan('tiny', {
      skip: (req, res) => res.statusCode < 400 || res.statusCode >= 500,
      stream: {
        write: (message) => {
          this.logger.warn(message.trim())
        },
      },
    })(req, res, next)

    morgan('tiny', {
      skip: (req, res) => res.statusCode < 200 || res.statusCode >= 400,
      stream: {
        write: (message) => {
          this.logger.log(message.trim())
        },
      },
    })(req, res, next)

    morgan('tiny', {
      skip: (req, res) => res.statusCode < 500 || res.statusCode >= 600,
      stream: {
        write: (message) => {
          this.logger.error(message.trim())
        },
      },
    })(req, res, next)
  }
}

HTTPLoggerMiddleware extends NestMiddleware and passes the request and response through morgan. This implementation creates several morgan logger functions based on the HTTP status code of the response and uses a specific log function. While optional, this approach is useful if you’ve customized your winston logger to log successes and failures to separate files, or if you’ve enabled the colorization functionality. Note that this also uses a predefined log format, tiny, but that can also be customized if needed.

To complete this implementation, update your AppModule to use the new middleware.

@Module({
  imports: [
    // Other modules
    LoggerModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(HTTPLoggerMiddleware).forRoutes('*')
  }
}

and in your main.ts, update the app instance to use your new logger.

const app = await NestFactory.create(AppModule, { bufferLogs: true })
app.useLogger(new WinstonLogger())

Note that bufferLogs is set to true so that service initialization logs that are written before WinstonLogger is created are still available. If WinstonLogger creation fails at this point NestJS falls back to the default logger.