Advance Your NestJS Application with Winston Logger: A Step-by-Step Guide

Advance Your NestJS Application with Winston Logger: A Step-by-Step Guide

Introduction

You're a developer in a team responsible for a large-scale fintech application used by millions of users. The app provides seamless financial transactions, account management, and notification services. It's a critical platform that people rely on for their day-to-day financial activities.

One fateful day, the app experiences a sudden surge in user activity due to a major financial event, and therefore, chaos ensues. Users start reporting issues such as failed transactions, missing funds, no notifications etc. The app's support team is overwhelmed with complaints, and the reputation of the company is at stake.

As the development team, there's an urgent need to quickly identify the root cause of these issues and resolve them promptly. This is where a proper logging mechanism becomes a lifesaver.

Understanding Winston Logger

Logging plays a vital role in modern, production-grade application development, enabling developers to track, monitor, and troubleshoot their code effectively.

With Winston, a versatile logging library for Node.js, you can easily manage and customize your logging needs. From logging errors, warnings, and information to writing logs data to consoles, files, databases, and even third-party services, Winston offers a rich feature set that makes it a popular choice among developers.

By implementing a robust logging solution like Winston Logger, the team gains a crucial tool for tracking down the problem. The logs show information about the application's internal processes, including transactional data, error messages, and stack traces. With this, the developers can trace the flow of transactions, identify potential bottlenecks, and pinpoint the exact moments where failures occur. Armed with this information, the team can confidently investigate the underlying issues, whether it's a misconfigured service, a database inconsistency, or a problematic integration with a third-party provider. With the ability to drill down into the logs, they can isolate and resolve the problems swiftly, ensuring minimal disruption to the users and restoring trust in the fintech app.

Setting up Winston Logger in a NestJS Project

1. Install the required dependencies

To get started with using Winston Logger, we need to first install the following required dependencies.

npm install --save nest-winston winston

2. Next, create a directory and a file in it to hold our log levels and transport configs. I will be creating a directory named 'logger' and a file named 'winston.logger.ts'. It is customary to put this directory just outside, on the same level as your Nestjs 'src' directory.

cd MyNestProject && mkdir logger && cd logger && touch winston.logger.ts

3. Now inside our winston.logger.ts file, we are going to set up the logging configurations.

import { createLogger, format, transports } from "winston";

// custom log display format
const customFormat = format.printf(({timestamp, level, stack, message}) => {
    return `${timestamp} - [${level.toUpperCase().padEnd(7)}] - ${stack || message}`
})

const options = {
    file: {
        filename: 'error.log',
        level: 'error'
    },
    console: {
        level: 'silly'
    }
}

// for development environment
const devLogger = {
    format: format.combine(
        format.timestamp(),
        format.errors({stack: true}),
        customFormat
    ),
    transports: [new transports.Console(options.console)]
}

// for production environment
const prodLogger = {
    format: format.combine(
        format.timestamp(),
        format.errors({stack: true}),
        format.json()
    ),
    transports: [
        new transports.File(options.file),
        new transports.File({
            filename: 'combine.log',
            level: 'info'
        })
    ]
}

// export log instance based on the current environment
const instanceLogger = (process.env.NODE_ENV === 'production') ? prodLogger : devLogger

export const instance = createLogger(instanceLogger)

A brief explanation of the code.

First, the code imports necessary modules from the Winston library: createLogger, format, and transports.

Next, a custom log format is defined using the format.printf() function. This format specifies how each log entry should be displayed. In this case, the log entry includes a timestamp, log level, and either the stack trace or the log message itself.

After defining the log format, an options object is created. It specifies the configuration for different transports, which determines where the logs will be stored or displayed.

The devLogger object is defined, which represents the logger configuration for a development environment. It uses the previously defined log format and sets the console transport to log all levels of messages (silly level). The options.console object within the options specifies the configuration for the console transport.

The prodLogger object is defined, which represents the logger configuration for the production environment. It also uses the same log format and includes two transports: a file transport for error logs (error.log) and a file transport for combined logs (combine.log), both at different levels (error and info, respectively).

The instanceLogger variable is set based on the value of the NODE_ENV environment variable. If it is set to 'production', the prodLogger is used; otherwise, the devLogger is used.

Finally, the code exports a logger instance created using createLogger() function, passing the instanceLogger configuration object. This instance can then be used throughout the application to log messages based on the selected environment configuration.

Overall, this code sets up a flexible and configurable logger using Winston, allowing developers to define different logging behaviors for development and production environments. You can as well modify these configurations to fit your use case.

4. Now inside the main.ts file of your Nestjs app, we need to enable logging and register it to use the Winston logger instance we've created.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

// imports for Winston logger
import { WinstonModule } from 'nest-winston';
import { instance } from 'logger/winston.logger';

const PORT = process.env.PORT || 3000;

// Enable Winston Logging
async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    logger: WinstonModule.createLogger({
      instance: instance,
    }),
  });

  await app.listen(PORT);
}
bootstrap();

5. Next up is to set the Logger module from @nestjs/common as a provider, first in your Nestjs app.module.ts file and then also in any other module file of your project resource that you want to use the logging functionality, say for example, user.module.ts

import { Logger, Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { UserModule } from './user/user.module';

@Module({
  imports: [
    UserModule,
  ],
  providers: [Logger],
})

export class AppModule {}

6. Finally, to use the logger for application logging, we instantiate the Logger class in each of our services. Below is a sample service for creating a user account and illustrated the use of the logger.

import {
  HttpException,
  HttpStatus,
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import { SignUpDto } from './dto';
import { InjectModel } from '@nestjs/sequelize';
import { User } from './../user/user.model';

@Injectable()
export class UserService {
  constructor(
    @InjectModel(User) private userModel: typeof User,
    private readonly logger: Logger, // instantiate logger
  ) {}

  SERVICE: string = UserService.name;

  // SIGNUP A NEW USER
  async signup(dto: SignUpDto): Promise<User> {
    try {
      // hash the password
      const hash_pass = await this.hasher(dto.password);

      const user = await this.userModel.create({
        password: hash_pass,
        email: dto.email,
        name: dto.name,
      });

      // user logger
      this.logger.log(`User created successfully - ${user.id}`, this.SERVICE);

      return user;
    } catch (error) {
      // user logger
      this.logger.error('Unable to create user', error.stack, this.SERVICE);

      if (error.name === 'SequelizeUniqueConstraintError')
        throw new HttpException('Email already exist', HttpStatus.CONFLICT);

      throw new HttpException(
        `${error.message}`,
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }
  }
}

And that's it guys if you start your app in a development environment, you will see your app logs in the console, and if you start it in a production environment, you will see your error logs in the error.log file, while all other logs including the error log will be in combine.log file in the root of your application. Here are sample-generated logs in the combile.log file:

{"context":"NestFactory","level":"info","message":"Starting Nest application...","timestamp":"2023-04-27T02:31:45.209Z"}
{"context":"InstanceLoader","level":"info","message":"TypeOrmModule dependencies initialized","timestamp":"2023-04-27T02:31:45.245Z"}
{"context":"InstanceLoader","level":"info","message":"AppModule dependencies initialized","timestamp":"2023-04-27T02:31:45.246Z"}
{"context":"InstanceLoader","level":"info","message":"DeveloperModule dependencies initialized","timestamp":"2023-04-27T02:31:45.588Z"}
{"context":"RoutesResolver","level":"info","message":"DeveloperController {/api/v1/developers}:","timestamp":"2023-04-27T02:31:45.629Z"}
{"context":"NestApplication","level":"info","message":"Nest application successfully started","timestamp":"2023-04-27T10:28:56.074Z"}
{"context":"DeveloperService","level":"info","message":"Developer created successfully","timestamp":"2023-04-27T10:29:56.297Z"}
{"context":"DeveloperService","level":"info","message":"Developer created successfully","timestamp":"2023-04-27T10:31:17.197Z"}
{"context":"DeveloperService","level":"info","message":"Developer created successfully","timestamp":"2023-04-27T10:32:34.568Z"}
{"context":"DeveloperService","level":"info","message":"Fetch all developers successfully","timestamp":"2023-04-27T10:33:04.738Z"}

In Conclusion

Adding Winston Logger to your Nest.js project is a powerful way to enhance your application's logging capabilities. By implementing a proper logging mechanism, you gain valuable insights into the internal workings of your application, enabling you to troubleshoot issues more efficiently. Moreover, the comprehensive logs generated by Winston Logger can serve as invaluable evidence for auditing and compliance purposes. So, don't underestimate the power of logging and take the next step to incorporate Winston Logger into your Nest.js project today. Happy logging!