Skip to content

WinstonModule

The WinstonModule for NestJS provides a convenient way to integrate the popular logging library Winston into your NestJS application.

How to install WinstonModule to my app

To install the WinstonModule to your app, follow these steps:

  1. First, install the required dependencies by running the following command in your terminal:

    bash
    npm install --save winston

    Logs Writer

    You need to setup Logs Writer role for your service account in Google Cloud IAM.

  2. Set up the WinstonConfig using the provided createWinstonConfig method:

    ts
    // config/index.ts
    export const WinstonConfig = createWinstonConfig({
      appName: 'Jetstream', // Display in log messages
      useConsole: process.env.APP_ENV === AppEnv.LOCAL,
      useGoogleCloudLogging: process.env.APP_ENV !== AppEnv.LOCAL,
      googleCloudLoggingOptions: {
        logName: 'jetstream_core', // Google Logging source name
      },
    });
  3. Next, import the WinstonModule into your main AppModule:

    ts
    // src/app.module.ts
    import { WinstonModule } from '@deeepvision/nest-kit';
    import { ConfigModule, ConfigType } from '@nestjs/config';
    import * as configs from '@/config';
    
    @Module({
      imports:[
        // Other modules
    
        // Configure the WinstonModule with Winston config
        WinstonModule.forRootAsync({
          inject: [configs.WinstonConfig.KEY],
          useFactory: async (opts: ConfigType<typeof configs.WinstonConfig>) => opts,
        }),
      ],
      ...
    })
    export class AppModule {}

How to use WinstonLogger

NestKit offers the WinstonLogger class for enhanced logging capabilities. You can create a logger instance using a factory within your service or resolver to streamline logging throughout your application.

ts
@Injectable()
export class BooksService {
  logger = this.loggerFactory.create({
    scope: BooksService.name,
  });

  constructor(
    @InjectWinstonLoggerFactory() private readonly loggerFactory: WinstonLoggerFactory,
  ) {}

  async getOne(id: string): Promise<MaybeNull<Book>> {
    return this.bookRepository.findOneBy({
      id,
    });
  }

  async getOneOrFail(id: string, ctx: ServiceMethodContext): Promise<Book> {
    const logger = this.logger.forMethod(this.getOneOrFail.name, ctx, {
      id,
    });

    const book = await this.getOne(id);

    if (!book) {
      throw new NotFoundException({
        message: `Book with id ${id} not found`,
        key: ErrorKeys.LIB_BOOKS_NOT_FOUND,
        context: logger.getContext(),
      });
    }

    return book;
  }

How to provide logging context

When providing logging context for each action, you can create a child logger with static context data. For example, WinstonLogger.forMethod serves as a convenient shorthand for the WinstonLogger.child method, allowing you to streamline the process of creating context-aware loggers for specific actions in your application.

ts
  const logger = this.logger.forMethod(this.getOneOrFail.name, ctx, {
    id,
  });

  // is the same as...

  const logger = this.logger.child({
    action: 'getOneOrFail',
    requestId: ctx.requestId,
    userId: ctx.user.id,
    id,
  });

When logging a message like this:

ts
logger.info(`Try to find book with id ${id}`);

You will see the following output:

plaintext
[DeepVision Library]  - 23.11.2022, 18:23:00      INFO [BooksService -> getOneOrFail] Try to find book with id libb:j8gk36hvn3h -- {
  id: 'libb:j8gk36hvn3h',
  requestId: 'libreq:xxkcowkdf1012gjhg023g330vlhope',
  userId: 'libu:jhkgrl2vnmy'
}

Adding Context Data to Logger Instance

To add context data to a logger instance, you can use the setContext method as shown in the example below:

ts
const logger = this.logger.forMethod(this.transfer.name, ctx, {
  id,
  toLibraryId: opts.libraryId
});

const book = await this.getOneOrFail(id, ctx);

// Adding context data to logger instance
logger.setContext({
  fromLibraryId: book.libraryId,
});

logger.info(`Transferring book ${id}...`);

You will see the following output:

plaintext
[DeepVision Library]  - 23.11.2022, 18:26:00      INFO [BooksService -> transfer] Transfer book libb:j8gk36hvn3h... -- {
  id: 'libb:j8gk36hvn3h',
  requestId: 'libreq:xnbcowkdf1012gjh2cbn3pjlpe',
  userId: 'libu:jhkgrl2vnmy'
  fromLibraryId: libl:kg829576gkp2,
  toLibraryId: 'libl:mbirot785vd'
}

In this example, we first create a logger instance using the forMethod method provided by the logger service. Then, we obtain a book object by calling the getOneOrFail method.

After that, we use the setContext method to add context data to the logger instance. The setContext method takes an object as its input parameter, which should contain the context data to be added.

Finally, we log an info message using the logger instance, which includes the book ID that is being transferred.

Alternatively, you can pass context data as the second argument to the WinstonLogger.info method. This data will be printed in the log message, but it won't be saved in the static context.

For example:

ts
this.logger.info(`User ${userId} successfully logged in`, { ip: userIp });

In this example, we log a message using the info method provided by the WinstonLogger instance. The first argument is the log message, and the second argument is an object literal that contains the context data we want to add to the log message. Here, we include the IP address of the user who logged in.

Errors logging

To catch errors and log them using the WinstonLogger.error, you can use the following approach:

ts
try {
  await this.bookRepository.save(book);
} catch (error) {
  throw new InternalServerErrorException({
    message: 'Failed to create book',
    key: ErrorKeys.LIB_BOOKS_CREATE_FAILED,
    context: logger.getContext(),
    error,
  });
}

In this example, we catch an error that occurred while saving a book to the database. We then throw an InternalServerErrorException with an object literal as its input parameter, which contains the error details along with other contextual information. This exception will be caught and handled by the HttpExceptionFilter.

To set the HttpExceptionFilter as a global filter in the main.ts file, you can use the following code:

ts
import { HttpAdapterHost, NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import {
  NESTKIT_WINSTON_LOGGER_FACTORY_PROVIDER,
  NESTKIT_WINSTON_SYSTEM_LOGGER_PROVIDER,
  HttpExceptionFilter,
} from '@deeepvision/nest-kit';
import { config as useDotEnv } from '@deeepvision/dotenv-yaml';

if (process.env.APP_ENV === AppEnv.LOCAL) {
  useDotEnv();
}

const bootstrap = async () => {
  const app = await NestFactory.create(AppModule, serverOptions);
  app.useLogger(app.get(NESTKIT_WINSTON_SYSTEM_LOGGER_PROVIDER));

  app.enableCors({
    methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
  });

  const { httpAdapter } = app.get(HttpAdapterHost);
  const loggerFactory = app.get(NESTKIT_WINSTON_LOGGER_FACTORY_PROVIDER);

  // Set HttpExceptionFilter from NestKit for errors interception
  app.useGlobalFilters(new HttpExceptionFilter(httpAdapter, loggerFactory));

  ...
}

In this example, we obtain an instance of the httpAdapter and loggerFactory, and then we set the HttpExceptionFilter as a global filter using the app.useGlobalFilters method. This will allow the filter to catch and handle exceptions that are thrown from any part of the application, including the GraphQL or REST interfaces.

How to catch errors that occur outside of requests

To catch errors that occur outside of requests, such as those in eventemitter handlers or internal worker loops, you can use the logger.error method. Here's an example:

ts
  @OnEvent(UserUpdatedEvent.id)
  async handleUserUpdatedEvent(event: UserUpdatedEvent) {
    const { user, ctx } = event;
    const logger = this.logger.forMethod(this.handleUserUpdatedEvent.name, ctx, {
      updatedUserId: user.id
    });

    ...

    try {
      ...
    } catch (error) {
      if (error instanceof Error) {
        logger.error(error);

        // or 

        logger.error('Failed to handle user update', error);
      }
    }
  }

In this example, we have an OnEvent decorator that listens to a UserUpdatedEvent. We obtain the user and context data from the event object, and then we create a logger instance using the forMethod method.

Inside the handleUserUpdatedEvent method, we perform some operations that might throw errors. If an error occurs, we catch it using a try...catch block, and then we log it using the logger.error method. This method takes an error object or a string message as its input parameter. Here, we use the error object as the input parameter, but you can also pass a custom error message along with the error object if needed.

By logging errors using the logger.error method, you can easily track down issues that occur outside of requests and ensure that they are properly handled.

Created by DeepVision Software.