3

I am new to NestJS and would like to customise the log messages to include the x-request-id/x-correlation-id and the name of the file the log message originated but am not sure if there is anything in NestJS to do that.

My application is using NestJS with the Fastify adapter and has the following configuration in the bootstrap() function

  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter(),
    {
        logger: WinstonModule.createLogger(winston.createLogger({
          exitOnError: false,
          level: 'debug',
          handleExceptions: true,
          format: winston.format.combine(
            winston.format.timestamp(),
            winston.format.ms(),
            winston.format.colorize(),
            winston.format.align(),
            winston.format.splat(),
            winston.format.printf((info) => {
                return `${info.timestamp} [ ${info.level} ] : ${info.message}`;
            }),
          ),
          transports: [
            new (winston.transports.Console)()
          ]
        }),
      )
    }
  );

This seems to format the logs using winston as expected.

2022-03-09T11:21:22.131Z [ info ] : Starting Nest application...

However, I would also like to include the request/correlation id in the message and the name of the file the log message occurred e.g.

2022-03-09T11:21:22.131Z 2cfd4eee-ca2b-4869-b66b-2b7da291f567 [ info ] [ Main.ts ]: Starting Nest application...

Is there anything in NestJS itself to allow this or any external libraries that I could use to achieve the desired result ?

mh377
  • 1,656
  • 5
  • 22
  • 41

3 Answers3

9

I managed to get it working using the nest-pino library:

// main.ts

import { Logger } from 'nestjs-pino';

async function bootstrap() {

const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter(),
    { bufferLogs: true }
  );

  app.useLogger(app.get(Logger));
  
}
bootstrap();

// app.module.ts

import { LoggerModule } from 'nestjs-pino';

@Module({
  imports: [
    LoggerModule.forRoot({
      pinoHttp: {
        level: process.env.LOG_LEVEL || 'debug',
        redact: ['request.headers.authorization'],
        prettyPrint: {
          colorize: true,
          singleLine: true,
          levelFirst: false,
          translateTime: "yyyy-MM-dd'T'HH:mm:ss.l'Z'",
          messageFormat: "{req.headers.x-correlation-id} [{context}] {msg}",
          ignore: "pid,hostname,context,req,res,responseTime",
          errorLikeObjectKeys: ['err', 'error']
        }
      }
    }),
  ],
  controllers: [MyController],
})
export class AppModule {}

// my.controller.ts
import { Controller, Get, Param, Logger } from '@nestjs/common';

@Controller()
export class MyController {
    private readonly logger: Logger = new Logger(MyController.name);

    @Get('/:id')
    async getCustomerDetails(@Headers() headers, @Param('id') id: string): Promise<Customer> {
        this.logger.log(`Accepted incoming request with id: ${id}`);

        // Do some processing ....

        return customer;
    }
}

[2022-11-14T11:03:07.100Z] INFO: 428f0df9-d12b-4fca-9b11-805a13ff41be [MyController] Accepted incoming request with id: 1

****** UPDATE **********

I also managed to update the redacted fields to be configurable from a .yaml file

// app.module.ts

import { ConfigModule, ConfigService } from '@nestjs/config';
import { LoggerModule } from 'nestjs-pino';

@Module({
  imports: [
    LoggerModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: async (configService: ConfigService) => ({
        pinoHttp: {
          level: process.env.LOG_LEVEL || 'info',
          redact: configService.get<string[]>('logger.redacted.fields'),
          prettyPrint: {
            colorize: false,
            singleLine: true,
            levelFirst: false,
            translateTime: "yyyy-mm-dd'T'HH:MM:ss.l'Z'",
            messageFormat: '{req.headers.x-correlation-id} [{context}] {msg}',
            ignore: 'pid,hostname,context,req,res,responseTime',
            errorLikeObjectKeys: ['err', 'error'],
          },
        },
      }),
      inject: [ConfigService],
    }),
  ],
  controllers: [MyController],
})
export class AppModule {}

dev.yaml

logger:
    redacted:
        fields:
            - 'headers.Authorization'
            - 'headers["X-Api-Key"]'
mh377
  • 1,656
  • 5
  • 22
  • 41
  • The above answer is correct. But if anyone wants to use Pino as their logger and not the default Logger provided by NestJS, then add the below line in the constructor of your controller or provider: `@InjectPinoLogger(MyController.name) private readonly logger: PinoLogger ` Now simply using `this.logger.info({msg: 'controller data'})` would work like a charm. – Khushal Vyas Jan 01 '23 at 18:43
5
  1. Assign a request ID. Make this your first middleware.
  2. Bind the request context with logger. Make a class of Logger, bind the request context to logger. It will act basically as wrapper for WinstonLogger. Override all the methods of winston logger to print the request ID in form an manner which you want (preferred way is using JSON as it will be easier for you to query in logs).
Aditya Gaddhyan
  • 354
  • 1
  • 14
  • 1
    Underrated answer – Alex C May 24 '22 at 05:31
  • 1
    Do you mind providing a snipped on how to do this: "Make a class of Logger, bind the request context to logger." ? :) – Daniel Eberl Sep 01 '22 at 18:50
  • @DanielEberl some of the snippets are here: https://github.com/Adityagaddhyan/logger-snippet-node They are in JS but you can write your own on same lines in NestJS or TS. Nest has library function for request context. – Aditya Gaddhyan Sep 09 '22 at 12:18
0

If you are using winston,

Use logFormatter from winston to add more fields to each logs:-

One example below:

const { format} = require("winston");
var reqId = '123123' //function or const request id
const addRequestId = format((info, opts) => {
    if(reqId)
        info.reqId= reqId;
    return info;
});

Then add below config while creating logger

var config = {
format: format.combine(
  addRequestId(),
  format.timestamp(new Date().toISOString()),
  format.json(),
),
transports: [new transports.Console()],
level: 'debug'
 }
const logger = createLogger(config);

To get file name, use (new Error()).trace property. This will give you full stack trace containing file names and line number in each file.

Tukesh
  • 73
  • 7