As our application grows, more and more people start depending on it. At a time like this, it is crucial to ensure that our API works well. To do that, we could use a way to troubleshoot the application to detect anomalies and to be able to find their origin. This article serves as an introduction to how we can keep logs on what happens in our application. Logger built into NestJS Fortunately, NestJS comes with a logger built-in. Before using it, we should create its instance. posts.service.ts import { Injectable, Logger } from '@nestjs/common'; @Injectable() export default class PostsService { private readonly logger = new Logger(PostsService.name) // ... }Although we could use the Logger we import from @nestjs/common directly, creating a brand new instance for every service is a good practice and allows us to supply the name of the service for the Logger constructor. Log levels A crucial thing about the Logger is that it comes with a few methods: error warn log verbose debug The above methods correspond with log levels that we can configure for our application. main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import getLogLevels from './utils/getLogLevels'; async function bootstrap() { const app = await NestFactory.create(AppModule, { logger: getLogLevels(process.env.NODE_ENV === 'production') }); // ... } bootstrap(); We don’t use the ConfigService above to read the environment variables because it isn’t initialized yet. getLogLevels.ts import { LogLevel } from '@nestjs/common/services/logger.service'; function getLogLevels(isProduction: boolean): LogLevel[] { if (isProduction) { return ['log', 'warn', 'error']; } return ['error', 'warn', 'log', 'verbose', 'debug']; } export default getLogLevels;Because of the above setup, the debug and verbose methods won’t produce logs on production. If we take a look at the isLogLevelEnabled function, we can notice that providing ['debug'] turns on all of the log levels, not only verbose. This is because NestJS assumes that if we want to display the verbose logs, we also want to display logs of all of the lower levels. Because of that, ['debug'] is the same as ['error', 'warn', 'log', 'verbose', 'debug']. We can find the importance of each log level here. After doing all of the above, let’s start using the logger. posts.service.ts import { Injectable, Logger } from '@nestjs/common'; import Post from './post.entity'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import PostNotFoundException from './exceptions/postNotFound.exception'; @Injectable() export default class PostsService { private readonly logger = new Logger(PostsService.name) constructor( @InjectRepository(Post) private postsRepository: Repository, ) {} async getPostById(id: number) { const post = await this.postsRepository.findOne(id, { relations: ['author'] }); if (post) { return post; } this.logger.warn('Tried to access a post that does not exist'); throw new PostNotFoundException(id); } // ... } Now we can see that passing PostsService.name caused PostService to appear as a prefix to our log message. Using the logger in a middleware Even though the above approach might come in handy, it might be cumbersome to write log messages manually. Thankfully, we can produce logs from middleware. logs.middleware.ts import { Injectable, Logger, NestMiddleware } from '@nestjs/common'; import { Request, Response, NextFunction } from 'express'; @Injectable() class LogsMiddleware implements NestMiddleware { private readonly logger = new Logger('HTTP'); use(request: Request, response: Response, next: NextFunction) { response.on('finish', () => { const { method, originalUrl } = request; const { statusCode, statusMessage } = response; const message = `${method} ${originalUrl} ${statusCode} ${statusMessage}`; if (statusCode >= 500) { return this.logger.error(message); } if (statusCode >= 400) { return this.logger.warn(message); } return this.logger.log(message); }); next(); } } export default LogsMiddleware; Check out the MDN documentation to read more about HTTP response status codes. Above, we gather information about the request and response and log it based on the status code. Of course, the request and response objects contain more helpful information, so feel free to make your logs even more verbose. The last step is to apply our middleware for all of our routes. app.module.ts import { MiddlewareConsumer, Module } from '@nestjs/common'; import { PostsModule } from './posts/posts.module'; import { DatabaseModule } from './database/database.module'; import LogsMiddleware from './utils/logs.middleware'; @Module({ imports: [ PostsModule, DatabaseModule, // ... ], // ... }) export class AppModule { configure(consumer: MiddlewareConsumer) { consumer .apply(LogsMiddleware) .forRoutes('*'); } } Using the logger with TypeORM Another helpful thing we can do is to log all SQL queries that happen in our application. To do that with TypeORM, we need to implement the Logger interface: databaseLogger.ts import { Logger as TypeOrmLogger } from 'typeorm'; import { Logger as NestLogger } from '@nestjs/common'; class DatabaseLogger implements TypeOrmLogger { private readonly logger = new NestLogger('SQL'); logQuery(query: string, parameters?: unknown[]) { this.logger.log(`${query} -- Parameters: ${this.stringifyParameters(parameters)}`); } logQueryError(error: string, query: string, parameters?: unknown[]) { this.logger.error(`${query} -- Parameters: ${this.stringifyParameters(parameters)} -- ${error}`); } logQuerySlow(time: number, query: string, parameters?: unknown[]) { this.logger.warn(`Time: ${time} -- Parameters: ${this.stringifyParameters(parameters)} -- ${query}`); } logMigration(message: string) { this.logger.log(message); } logSchemaBuild(message: string) { this.logger.log(message); } log(level: 'log' | 'info' | 'warn', message: string) { if (level === 'log') { return this.logger.log(message); } if (level === 'info') { return this.logger.debug(message); } if (level === 'warn') { return this.logger.warn(message); } } private stringifyParameters(parameters?: unknown[]) { try { return JSON.stringify(parameters); } catch { return ''; } } } export default DatabaseLogger;The last step is to use the above class in our TypeORM configuration: database.module.ts import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule, ConfigService } from '@nestjs/config'; import DatabaseLogger from './databaseLogger'; @Module({ imports: [ TypeOrmModule.forRootAsync({ imports: [ConfigModule], inject: [ConfigService], useFactory: (configService: ConfigService) => ({ type: 'postgres', logger: new DatabaseLogger(), host: configService.get('POSTGRES_HOST'), // ... }) }), ], }) export class DatabaseModule {}When we start looking into the logs from TypeORM, we notice that it often produces quite lengthy queries. For example, the below query happens when we retrieve the data of the user that attempts to log in: Saving logs into a PostgreSQL database So far, we’ve only been logging all of the messages to the console. While that might work fine when developing the application on our machine, it wouldn’t make a lot of sense in a deployed application. There are a lot of services that can help us gather and manage logs, such as DataDog and Loggly. They are not free of charge, though. Therefore, in this article, we save logs into a PostgreSQL database. For starters, let’s create an entity for our log: log.entity.ts import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'; @Entity() class Log { @PrimaryGeneratedColumn() public id: number; @Column() public context: string; @Column() public message: string; @Column() public level: string; @CreateDateColumn() creationDate: Date; } export default Log; Above, we use the @CreateDateColum decorator. If you want to know more about dates in PostgreSQL, check out Managing date and time with PostgreSQL and TypeORM Once we’ve got the above done, let’s create a service that allows us to create logs: logs.service.ts import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import Log from './log.entity'; import CreateLogDto from './dto/createLog.dto'; @Injectable() export default class LogsService { constructor( @InjectRepository(Log) private logsRepository: Repository ) {} async createLog(log: CreateLogDto) { const newLog = await this.logsRepository.create(log); await this.logsRepository.save(newLog, { data: { isCreatingLogs: true } }); return newLog; } }Above, you can notice that we pass isCreatingLogs: true when saving our logs to the database. The above is because we need to overcome the issue of an infinite loop. When we store logs in the database, it causes SQL queries to be logged. When we log SQL queries, they are saved to the database, causing an infinite loop. Because of that, we need to adjust our DatabaseLogger slightly: databaseLogger.ts import { Logger as TypeOrmLogger, QueryRunner } from 'typeorm'; import { Logger as NestLogger } from '@nestjs/common'; class DatabaseLogger implements TypeOrmLogger { private readonly logger = new NestLogger('SQL'); logQuery(query: string, parameters?: unknown[], queryRunner?: QueryRunner) {