Nest.js Tutorial

Logging with the Drizzle ORM

Marcin Wanago
NestJS

Debugging is a great way to find issues when running an application locally. Unfortunately, we don’t have this option in a deployed application. Because of that, implementing logging functionality is necessary to track down and investigate any potential problems. In this article, we learn how to use the logger built into NestJS and integrate it with the Drizzle ORM.

Logger built into NestJS

NestJS conveniently offers built-in logging functionalities that we can use. Each message that we log needs to have a level. Here is a breakdown of all log levels, sorted by severity:

  • fatal
  • error
  • warn
  • log
  • debug
  • verbose
NestJS added the “fatal” log level some time last year.

To create logs, we should use the Logger class. One way to do that is to import it from @nestjs/common.

articles.service.ts
1import { Injectable, NotFoundException, Logger } from '@nestjs/common';
2import { DrizzleService } from '../database/drizzle.service';
3import { databaseSchema } from '../database/database-schema';
4import { eq } from 'drizzle-orm';
5 
6@Injectable()
7export class ArticlesService {
8  constructor(private readonly drizzleService: DrizzleService) {}
9 
10  async getById(id: number) {
11    const articles = await this.drizzleService.db
12      .select()
13      .from(databaseSchema.articles)
14      .where(eq(databaseSchema.articles.id, id));
15    const article = articles.pop();
16    if (!article) {
17      Logger.warn('Tried to get an article that does not exist');
18      throw new NotFoundException();
19    }
20    return article;
21  }
22 
23  // ...
24}

Although the above approach does the job, the official documentation recommends creating an instance of the logger inside each class that uses it.

articles.service.ts
1import { Injectable, NotFoundException, Logger } from '@nestjs/common';
2import { DrizzleService } from '../database/drizzle.service';
3import { databaseSchema } from '../database/database-schema';
4import { eq } from 'drizzle-orm';
5 
6@Injectable()
7export class ArticlesService {
8  private readonly logger = new Logger(ArticlesService.name);
9 
10  constructor(private readonly drizzleService: DrizzleService) {}
11 
12  async getById(id: number) {
13    const articles = await this.drizzleService.db
14      .select()
15      .from(databaseSchema.articles)
16      .where(eq(databaseSchema.articles.id, id));
17    const article = articles.pop();
18    if (!article) {
19      this.logger.warn('Tried to get an article that does not exist');
20      throw new NotFoundException();
21    }
22    return article;
23  }
24 
25  // ...
26}

With this adjustment, NestJS can include the name of our service in the log, which makes it easier to read.

Working with log levels

The higher the level of our log is, the higher the severity. For instance, if a user attempts to access an article that does not exist, it likely shouldn’t cause an alarm in the middle of the night. Because of that, in our example above, we used warn instead of error. The higher the log, the more concerned we should be.

To avoid cluttering our logs, we can filter some of them out.

main.ts
1import { NestFactory } from '@nestjs/core';
2import { AppModule } from './app.module';
3import { LogLevel, ValidationPipe } from '@nestjs/common';
4 
5async function bootstrap() {
6  const isProduction = process.env.NODE_ENV === 'production';
7  const logLevels: LogLevel[] = isProduction
8    ? ['fatal', 'error', 'warn', 'log']
9    : ['fatal', 'error', 'warn', 'log', 'debug', 'verbose'];
10 
11  const app = await NestFactory.create(AppModule, {
12    logger: logLevels,
13  });
14  app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
15  await app.listen(3000);
16}
17bootstrap();
A surprising thing is that providing just ['verbose'] turns on all log levels. When we take a look at the isLogLevelEnabled function under the hood of NestJS, we can see that searches for the looks for the highest severity included in our array and turns on the logs with the lower severity as well. However, we provide the complete array for readability.

Logging SQL queries with the Drizzle ORM

Logging SQL queries allows us to investigate how Drizzle ORM interacts with our database and notice potential issues.

The most straightforward way to start logging our SQL queries is to pass an additional parameter to the drizzle function.

drizzle.service.ts
1import { Inject, Injectable } from '@nestjs/common';
2import { Pool } from 'pg';
3import { CONNECTION_POOL } from './database.module-definition';
4import { drizzle, NodePgDatabase } from 'drizzle-orm/node-postgres';
5import { databaseSchema } from './database-schema';
6 
7@Injectable()
8export class DrizzleService {
9  public db: NodePgDatabase<typeof databaseSchema>;
10  constructor(@Inject(CONNECTION_POOL) private readonly pool: Pool) {
11    this.db = drizzle(this.pool, { schema: databaseSchema, logger: true });
12  }
13}

The downside to this solution is that the output of the default Drizzle ORM logger looks different from that of the NestJS logger. Let’s deal with that by creating a custom Drizzle ORM logger.

custom-drizzle-logger.ts
1import { Logger as NestLogger } from '@nestjs/common';
2import { Logger } from 'drizzle-orm/logger';
3 
4export class CustomDrizzleLogger implements Logger {
5  private readonly logger = new NestLogger('SQL');
6 
7  logQuery(query: string, params: unknown[]): void {
8    this.logger.log(`${query} | Params: ${JSON.stringify(params)}`);
9  }
10}

Every time Drizzle ORM wants to log a query, it calls the logQuery function. We can take advantage of that by using the NestJS logger.

We can now use our custom logger instead of the default one built into the Drizzle ORM.

drizzle-service.ts
1import { Inject, Injectable } from '@nestjs/common';
2import { Pool } from 'pg';
3import { CONNECTION_POOL } from './database.module-definition';
4import { drizzle, NodePgDatabase } from 'drizzle-orm/node-postgres';
5import { databaseSchema } from './database-schema';
6import { CustomDrizzleLogger } from './custom-drizzle-logger';
7 
8@Injectable()
9export class DrizzleService {
10  public db: NodePgDatabase<typeof databaseSchema>;
11  constructor(@Inject(CONNECTION_POOL) private readonly pool: Pool) {
12    this.db = drizzle(this.pool, {
13      schema: databaseSchema,
14      logger: new CustomDrizzleLogger(),
15    });
16  }
17}

Thanks to this solution, our logs are a lot more consistent.

Using a logging interceptor

Manually defining logs for every case in our application might produce the best results but can be time-consuming. To automate our logs somewhat, we can write a NestJS interceptor that handles logging automatically.

Interceptors in NestJS allow us run additional logic before or after NestJS handles a particular API request.
logger.interceptor.ts
1import {
2  Injectable,
3  NestInterceptor,
4  ExecutionContext,
5  CallHandler,
6  Logger,
7} from '@nestjs/common';
8import { Request, Response } from 'express';
9 
10@Injectable()
11export class LoggerInterceptor implements NestInterceptor {
12  private readonly logger = new Logger('HTTP');
13 
14  intercept(context: ExecutionContext, next: CallHandler) {
15    const httpContext = context.switchToHttp();
16    const request = httpContext.getRequest<Request>();
17    const response = httpContext.getResponse<Response>();
18 
19    response.on('finish', () => {
20      const { method, originalUrl } = request;
21      const { statusCode, statusMessage } = response;
22 
23      const message = `${method} ${originalUrl} ${statusCode} ${statusMessage}`;
24 
25      if (statusCode >= 500) {
26        return this.logger.error(message);
27      }
28 
29      if (statusCode >= 400) {
30        return this.logger.warn(message);
31      }
32 
33      return this.logger.log(message);
34    });
35 
36    return next.handle();
37  }
38}
You can add more data into the logs such as the POST request body.

Above, we wait for the HTTP request to complete and then determine the log level based on the status code of our response.

There is more than one way to use our interceptor. For example, we can decorate a single method with it.

articles.controller.ts
1import {
2  Controller,
3  Get,
4  Param,
5  ParseIntPipe,
6  UseInterceptors,
7} from '@nestjs/common';
8import { ArticlesService } from './articles.service';
9import { LoggerInterceptor } from '../utilities/logger.interceptor';
10 
11@Controller('articles')
12export class ArticlesController {
13  constructor(private readonly articlesService: ArticlesService) {}
14 
15  @Get(':id')
16  @UseInterceptors(LoggerInterceptor)
17  getById(@Param('id', ParseIntPipe) id: number) {
18    return this.articlesService.getById(id);
19  }
20 
21  // ...
22}

Another approach is to decorate the entire controller.

articles.controller.ts
1import { Controller, UseInterceptors } from '@nestjs/common';
2import { LoggerInterceptor } from '../utilities/logger.interceptor';
3 
4@Controller('articles')
5@UseInterceptors(LoggerInterceptor)
6export class ArticlesController {
7  // ...
8}

We can also use the useGlobalInterceptors function to apply our interceptor to all our controllers.

main.ts
1import { NestFactory } from '@nestjs/core';
2import { AppModule } from './app.module';
3import { LogLevel, ValidationPipe } from '@nestjs/common';
4import { LoggerInterceptor } from './utilities/logger.interceptor';
5 
6async function bootstrap() {
7  const isProduction = process.env.NODE_ENV === 'production';
8  const logLevels: LogLevel[] = isProduction
9    ? ['fatal', 'error', 'warn', 'log']
10    : ['fatal', 'error', 'warn', 'log', 'debug', 'verbose'];
11 
12  const app = await NestFactory.create(AppModule, {
13    logger: logLevels,
14  });
15 
16  app.useGlobalInterceptors(new LoggerInterceptor());
17 
18  app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
19  await app.listen(3000);
20}
21bootstrap();

Summary

In this article, we explored how to use the built-in NestJS logger in a few different ways. Thanks to our approach, we can automatically log all SQL queries that Drizzle ORM makes and the HTTP responses our NestJS application sends. Besides that, we can also implement custom logging if necessary. To implement that, we had to create a custom interceptor that logs various messages automatically when our NestJS application sends a response. We also made a custom Drizzle ORM logger and combined it with the logger built into NestJS.

By adopting a thorough approach to logging, troubleshooting issues becomes significantly easier. It makes it a valuable thing to learn and implement in your NestJS application.