Winston: Production Level Logger in Javascript

Amir Mustafa
7 min readMar 9, 2024

--

→ In today’s article we will learn about a production-level logger Winston that is used widely these days.

What makes Winston so special?

→ If we are working on a project that is live and has a continuous workflow, logging is essential.

→ Javascript already has console.log, console. error, etc. Winston helps us make the log level set easily.

Official SDK: https://www.npmjs.com/package/winston

{
error: 0,
warn: 1,
info: 2,
http: 3,
verbose: 4,
debug: 5,
silly: 6
}

Setting Logs Configuration in Application Environment Specific:

→ Now if we in:

a. DEV Environment: if we set log level debug (only debug, verbose, HTTP, info, warn, error logs will display)

b. UAT Environment: log level info (only info, warn, and error logs will display)

c. Production Environment: log level warn (only warn, error logs will display)

NOTE: Whatever log level we set — that and above logs level logs will be visible

Eg: 1: if level in code — info

Info, warn, and error logs will print

Eg: 2: if level in code — error

Only error log will print

→ Second important feature is to export into a file or log in one line code

a. DEV: Log in to theterminal

b. UAT: Log in to the terminal, and log in error.log file

c. Production — Log in to the terminal, and log in to the error.log file

→ It has many other features to learn, let us explore together.

Demo: Implementing in Project:

STEP 1: Create a logger folder in the root project level

STEP 2: Create index.js or index.ts file in it (based on your environment)

Path: src/logger/index.ts

import devLogger from './devLogger';
import uatLogger from './uatLogger';
import productionLogger from './productionLogger';

let logger = null;

if (process.env.NODE_ENV === 'production') {
logger = productionLogger();
}

if (process.env.NODE_ENV === 'uat') {
logger = uatLogger();
}

if (process.env.NODE_ENV === 'dev') {
logger = devLogger();
}

export default logger;

STEP 3: We will make three files (assuming we have 3 environments) inside the logger folder — devLogger.ts, uatLogger.ts, production Logger.ts

→ If you are in the javascript environment, you can use the .js extensions file

File 1: devLogger.ts

Path: src/logger/devLogger.ts

→ This file is created for dev environment logging configurations

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

const { combine, timestamp, label, printf } = format;

const myFormat = printf(({ level, message, label, timestamp }) => {
return `${timestamp} ${label} [${level}]: ${message}`; // LOG FORMAT
});

const devLogger = () => {
return createLogger({
level: 'debug',
format: combine(
format.colorize(),
label({ label: 'dev' }),
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
myFormat
),
transports: [
new transports.Console() // ONLY PRINTING LOGS IN TERMINAL
]
});
};

export default devLogger;

File 2: uatLogger.ts

Path: src/logger/uatLogger.ts

→ This file is created for UAT environment logging configurations

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

const { combine, timestamp, label, printf } = format;

const myFormat = printf(({ level, message, label, timestamp }) => {
return `${timestamp} ${label} [${level}]: ${message}`;
});

const uatLogger = () => {
return createLogger({
level: 'info',
format: combine(
format.colorize(),
label({ label: 'uat' }),
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
myFormat
),
transports: [
new transports.File({ filename: 'error.log', level: 'error' }),
new transports.File({ filename: 'combined.log' }),
new transports.Console()
]
});
};

export default uatLogger;

File 3: productionLogger.ts

Path: src/logger/productionLogger.ts

→ This file is created for Production environment logging configurations

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

const { combine, timestamp, label, printf } = format;

const myFormat = printf(({ level, message, label, timestamp }) => {
return `${timestamp} ${label} [${level}]: ${message}`;
});

const productionLogger = () => {
return createLogger({
level: 'warn',
format: combine(
label({ label: 'prod' }),
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
myFormat
),
transports: [
new transports.File({ filename: 'error.log', level: 'error' }),
new transports.File({ filename: 'combined.log' }),
new transports.Console()
]
});
};

export default productionLogger;

→ So we are now ready with configurations. All we have to do is utilize this login code instead of the JS default logs console

Implementing Logs in Application:

→ Now if we have to implement it, the simple below steps are followed:

Eg 1: Printing port Number

Path: src/main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { TransformInterceptor } from './transform.interceptor';
import logger from './logger';


async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
app.useGlobalInterceptors(new TransformInterceptor());
const port = 3000;
await app.listen(port);

// ONE IS ENOUGH JUST FOR UNDERSTANDING ALL LOG TYPES
logger.info(`Nest application is running in port ${port}`);
logger.warn(`Nest application is running in port ${port}`);
logger.error(`Nest application is running in port ${port}`);
logger.debug(`Nest application is running in port ${port}`);
logger.silly(`Nest application is running in port ${port}`);
}
bootstrap();

Eg 2: Logging User Information in GetTasks Method in TaskController

import {
Body,
Controller,
Get,
Param,
Post,
Delete,
Patch,
Query,
UseGuards,
} from '@nestjs/common';
import { TasksService } from './tasks.service';
import { CreateTaskDto } from './dto/create-task.dto';
import { GetTasksFilterDto } from './dto/get-tasks.dto';
import { UpdateTaskStatusDto } from './dto/update-task-status.dto';
import { Task } from './task.entity';
import { AuthGuard } from '@nestjs/passport';
import { GetUser } from 'src/auth/get-user.decorator';
import { User } from 'src/auth/user.entity';
import logger from 'src/logger'; // IMPORTING LOGGER

@Controller('tasks')
@UseGuards(AuthGuard())
export class TasksController {
private logger = logger;
private context = `[TasksController] `;
constructor(private taskService: TasksService) {}

@Get()
getTasks(@Query() filterDto: GetTasksFilterDto, @GetUser() user: User): Promise<Task[]> {
this.logger.info(`${this.context} User "${user.username}" retrieving all tasks, Filters: ${JSON.stringify(filterDto)}`);
return this.taskService.getTasks(filterDto, user);// USING LOG
}

EG 3: Logging Error Log in GetTask Task Service

import { Injectable, NotFoundException, InternalServerErrorException } from '@nestjs/common';
import { TaskStatus } from './task-status.enum';
import { GetTasksFilterDto } from './dto/get-tasks.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Task } from './task.entity';
import { CreateTaskDto } from './dto/create-task.dto';
import { Repository } from 'typeorm';
import { User } from 'src/auth/user.entity';
import logger from 'src/logger'; // IMPORT LOG

@Injectable()
export class TasksService {
private logger = logger; // ASSIGNING IN CLASS
private context = `[TasksService] `;
constructor(
@InjectRepository(Task)
private taskRepository: Repository<Task>,
) {}

async getTasks(filterDto: GetTasksFilterDto, user: User): Promise<Task[]> {
const query = this.taskRepository.createQueryBuilder('task');
const { status, search } = filterDto;

query.where({ user: user.id});

if (status) {
query.andWhere('task.status = :status', { status });
}
if (search) {
query.andWhere(
'(LOWER(task.title) LIKE LOWER(:search) OR LOWER(task.description) LIKE LOWER(:search))',
{ search: `%${search}%` },
);
}

try {
const tasks = await query.getMany();
return tasks;
} catch (error) {
// ERROR LOG
this.logger.error(`${this.context} Failed to get tasks for user ${user.username}, Filters: ${JSON.stringify(filterDto)}, Stack Error: ${error.stack}`);
throw new InternalServerErrorException();
}
}

Running in DEV Environment:

→ Let us run Get Task API

→ We see info logs printing in terminal

→ Let us try to generate error — writing wring column in select query

→ So in dev every log is only in terminal

→ We see logs of type debug to error

{
error: 0,- Yes
warn: 1,- Yes
info: 2,- Yes
http: 3,- Yes
verbose: 4,- Yes
debug: 5, - Yes
silly: 6
}

Running in UAT Environment:

→ Here logs are printed in the terminal and also sent in files — error.log, combined.log

error.log — contains only error logs

combined.log — all logs till its defined level (i.e. info for UAT)

error.log file

combined.log file:

→ Here only info, warn and error logs printed

Running in Production Environment:

→ Here only warn and error types of log printed

→ error.log file

→ combined.log file

Pull Request:

This PR is for converting regular logger into Winston logger

https://github.com/AmirMustafa/nestjs-task-management/pull/5

Conclusion:

Winston logger is used in production for JS application— easily print in console or dump into file or JSON files or even in streaming applications.

Another important feature is we can config environment specific. We can tell which type of log to print in DEV, UAT or Production environment helps in saving cost and protecting from logging unnecessary logs in production.

Thank you for reading till the end 🙌 . If you enjoyed this article or learned something new, support me by clicking the share button below to reach more people and/or subscribe Happy Learnings !! to see some other tips, articles, and things I learn about and share there.!

--

--

Amir Mustafa

JavaScript Specialist | Consultant | YouTuber 🎬. | AWS ☁️ | Docker 🐳 | Digital Nomad | Human. Connect with me on https://www.linkedin.com/in/amirmustafa1/