The Art of Recording Request and Response in Nest.js

Just diving into Nest.js,
I'm working on setting up a basic logger to trace HTTP requests such as :

:method :url :status :res[content-length] - :response-time ms

Based on what I know, the most suitable place for this would be interceptors. However, I also utilize Guards and as mentioned, Guards are activated after middlewares but before interceptors.

As a result, my forbidden accesses are not being logged. I could split the logging functionality into two different places, but I'd prefer not to. Any suggestions?

Thank you!

Here is my Interceptor code:

import { Injectable, NestInterceptor, ExecutionContext, HttpException, HttpStatus } from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';

@Injectable()
export class HTTPLoggingInterceptor implements NestInterceptor {

  intercept(context: ExecutionContext, call$: Observable<any>): Observable<any> {
    const now = Date.now();
    const request = context.switchToHttp().getRequest();

    const method = request.method;
    const url = request.originalUrl;

    return call$.pipe(
      tap(() => {
        const response = context.switchToHttp().getResponse();
        const delay = Date.now() - now;
        console.log(`${response.statusCode} | [${method}] ${url} - ${delay}ms`);
      }),
      catchError((error) => {
        const response = context.switchToHttp().getResponse();
        const delay = Date.now() - now;
        console.error(`${response.statusCode} | [${method}] ${url} - ${delay}ms`);
        return throwError(error);
      }),
    );
  }
}

Answer №1

Click here to see the GitHub issue and comment

The solution involves utilizing middleware.

import { Injectable, NestMiddleware, Logger } from '@nestjs/common';

import { Request, Response, NextFunction } from 'express';

@Injectable()
export class AppLoggerMiddleware implements NestMiddleware {
  private logger = new Logger('HTTP');

  use(request: Request, response: Response, next: NextFunction): void {
    const { ip, method, path: url } = request;
    const userAgent = request.get('user-agent') || '';

    response.on('close', () => {
      const { statusCode } = response;
      const contentLength = response.get('content-length');

      this.logger.log(
        `${method} ${url} ${statusCode} ${contentLength} - ${userAgent} ${ip}`
      );
    });

    next();
  }
}

Add the middleware to your AppModule as shown below:

export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer): void {
    consumer.apply(AppLoggerMiddleware).forRoutes('*');
  }
}

Answer №2

After some experimentation, I decided to implement a traditional logger directly into the raw app. While this approach may not be optimal as it's not fully integrated with the Nest flow, it effectively serves basic logging requirements.

import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
import { ApplicationModule } from './app.module';
import * as morgan from 'morgan';

async function bootstrap() {
    const app = await NestFactory.create<NestFastifyApplication>(ApplicationModule, new FastifyAdapter());
    app.use(morgan('tiny'));

    await app.listen(process.env.PORT, '0.0.0.0');
}

if (isNaN(parseInt(process.env.PORT))) {
    console.error('No port provided. 👏');
    process.exit(666);
}

bootstrap().then(() => console.log('Service listening 👍: ', process.env.PORT));

Answer №3

Why not try using the finish event instead of the close event?

import { Request, Response, NextFunction } from "express";
import { Injectable, NestMiddleware, Logger } from "@nestjs/common";

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  private logger = new Logger("HTTP");

  use(request: Request, response: Response, next: NextFunction): void {
    const { ip, method, originalUrl } = request;
    const userAgent = request.get("user-agent") || "";

    response.on("finish", () => {
      const { statusCode } = response;
      const contentLength = response.get("content-length");

      this.logger.log(
        `${method} ${originalUrl} ${statusCode} ${contentLength} - ${userAgent} ${ip}`,
      );
    });

    next();
  }
}

It is because, to my knowledge, the express connection is maintained even after sending a response.
Therefore, the close event cannot be triggered.

Reference

01. Node document about response event.
02. Github issue

Answer №4

Opting for Morgan as middleware to intercept requests appealed to me due to its formatting options. Meanwhile, I stuck with the standard Nest Logger for handling output to maintain consistency across my application.

// middleware/request-logging.ts
import { Logger } from '@nestjs/common';
import morgan, { format } from 'morgan';

export function implementRequestLogging(app) {
    const logger = new Logger('Request');
    app.use(
        morgan('tiny', {
            stream: {
                write: (message) => logger.log(message.replace('\n', '')),
            },
        }),
    );
}
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { implementRequestLogging } from './middleware/request-logging';

async function bootstrap() {
    const app = await NestFactory.create(AppModule);
    implementRequestLogging(app);
    await app.listen(configService.get<number>('SERVER_PORT'));
    logger.log(`Application is running on: ${await app.getUrl()}`);
}

Answer №5

NestJS Request Logging Interceptor

// Required modules imported for request logging interceptor
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';

@Injectable()
export class ReqLoggingInterceptor implements NestInterceptor {
  private readonly logger = new Logger('HTTP_REQUEST');

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    // Extracting request and response objects
    const req = context.switchToHttp().getRequest();
    const res = context.switchToHttp().getResponse();

    // Recording current timestamp
    const now = Date.now();

    // Checking environment for production
    const isProduction = process.env.DB_HOST?.toLowerCase()?.includes('stage') || process.env.NODE_ENV === 'local';

    // Constructing log message
    const logMessage =
      `METHOD - ${req.method} | URL - ${req.url} | ` +
      (!isProduction
        ? ''
        : `QUERY - ${JSON.stringify(req.query)} | PARAMS - ${JSON.stringify(req.params)} | BODY - ${JSON.stringify(req.body)} `) +
      `${this.getColorizedStatusCode(res.statusCode)} ${Date.now() - now} ms`;

    // Handling the observable
    return next.handle().pipe(
      tap(() => {
        // Logging request details on success
       req.url && this.logger.log(logMessage);
      }),
      catchError((error) => {
        // Logging request details on error and rethrowing the error
      req.url && this.logger.log(logMessage);
        throw error;
      }),
    );
  }

  private getColorizedStatusCode(statusCode: number): string {
    // ANSI escape codes for colorizing status code
    const yellow = '\x1b[33m';
    const reset = '\x1b[0m';

    return `${yellow}${statusCode}${reset}`;
  }
}

Instead of using external libraries like morgan, I have implemented a custom logging interceptor in NestJS...

Additional Note:

  1. I include request body, query, and params in logs for easier debugging of errors.

Answer №6

Give this code a try =>

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
    use(req: Request, res: Response, next: Function) {
        const { ip, method, originalUrl: url  } = req;
        const hostname = require('os').hostname();
        const userAgent = req.get('user-agent') || '';
        const referer = req.get('referer') || '';

        res.on('close', () => {
            const { statusCode, statusMessage } = res;
            const contentLength = res.get('content-length');
            logger.log(`[${hostname}] "${method} ${url}" ${statusCode} ${statusMessage} ${contentLength} "${referer}" "${userAgent}" "${ip}"`);
        });

        next();
    }
}

Answer №7

merge(fromEvent())

import { NestMiddleware, Injectable } from '@nestjs/common';
import { FastifyRequest, FastifyReply } from 'fastify';
import { InjectPinoLogger, PinoLogger } from 'nestjs-pino';
import serializers from 'pino-std-serializers';
import { merge, fromEvent, first } from 'rxjs';


@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  constructor(
    @InjectPinoLogger(LoggerMiddleware.name)
    private readonly logger: PinoLogger,
  ) {}

  use(req: FastifyRequest, res: FastifyReply['raw'], next: (error?: Error) => void) {
    
    // Logging request information
    const method = req.method;
    const url = req.url;
    const now = Date.now();
    const traceId = this.cls.get<IAsyncStorage>(TRACE_ID);

    this.logger.assign({ traceIdContext: traceId.value });

    this.logger.debug({
      info: {
        url,
        now,
        method,
        country,
        request: serializers.req(req),
        requestType: 'external-request',
      },
    });
    
    // Constructing data for logging the response
    const loggerData = {
      info: {
        url,
        method,
        country,
        response: serializers.res(res),
        requestType: 'response-from',
        data: '<omitted>', // JSON.stringify(this.responseJson),
      },
    };

    // Handling events for response completion and error
    const event$ = merge(fromEvent(res, 'finish'), fromEvent(res, 'close'));
    const eventError$ = merge(fromEvent(res, 'error'));

    // Subscribing to handle response completion event
    event$.pipe(first()).subscribe({
      next: () => {
        this.logger.debug(loggerData);
      },
    });

    // Subscribing to handle response error event
    eventError$.pipe(first()).subscribe({
      next: () => {
        this.logger.error(loggerData);
      },
    });

    return next(); // Proceed to the next middleware in the chain
  }
}

Answer №8

Dealing with the issue of logging the correct status code can be tricky, especially when filters run after interceptors. I found that implementing the logging directly in the interceptor was the most reliable solution for me, similar to what you have done in your code. By leveraging the observable in the interceptor, you can ensure that functions are executed after successful or error completions.

One challenge I faced was that the status code on the response may not always be properly set, even within the tap or catchError operators. To work around this, I checked the method of the request - if it's a POST method, then a successful response is a 201; otherwise, it is always a 200. In case of an error, I extracted the status code from the error object instead of relying on the response object. This ensured that the status code would be available on my error object before the observable completes, thanks to my Exception filter running beforehand.

Answer №9

If you're looking to create a custom logger for your Nest.js application, you can easily do so by following the instructions in the documentation for implementing the LoggerMiddleware.

To log all requests and responses, consider applying the logger to the wildcard * route. Within the logger class, you have the flexibility to choose specific fields to log both before and after the request is made:

export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('*');
  }
}

class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: Function) {
    console.log('Request', req.method, req.originalUrl, /*...*/);
    next();
    console.log('Response', res.statusCode, res.statusMessage, /*...*/);
  }
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Utilizing a class structure to organize express.Router?

I've been playing around with using Express router and classes in Typescript to organize my routes. This is the approach I've taken so far. In the index.ts file, I'm trying to reference the Notes class from the notes.ts file, which has an en ...

401 Unauthorized response returned upon making a POST request in Angular due to token invalidation

Looking for assistance on understanding and implementing the process of adding a product to the cart upon button click. I have a list of products retrieved from an API, each with options to increment quantity using + and - buttons. When the + button is cli ...

The Angular Material date picker unpredictably updates when a date is manually changed and the tab key is pressed

My component involves the use of the Angular material date picker. However, I have encountered a strange issue with it. When I select a date using the calendar control, everything works fine. But if I manually change the date and then press the tab button, ...

Compiling Vue with TypeScript: Troubleshooting common errors

Using Vue Components with Templates Multiple Times in TypeScript I am working on utilizing a component with a template multiple times within another component. The code is split between a .html file and a .ts file. The .html file structure is as follows: ...

Typescript classes implementing data hydration and dehydration techniques

Exploring ways to share TypeScript classes or interfaces between a React + TS frontend and node + TS backend. Converting class instances into JSON poses a challenge as TS types are removed during compile time. Considering options for defining objects in a ...

You cannot invoke this expression while destructuring an array of React hooks in TypeScript

Within my React TypeScript component, I have several fields that check a specific condition. If the condition is not met, the corresponding field error is set to true in order to be reflected in the component's DOM and prevent submission. However, whe ...

The ListBuckets command in AWS S3 provides a comprehensive list of all available

I'm currently working on a TypeScript application that interacts with an AWS S3 bucket. The issue I'm facing is that my current credentials only allow me to read and write data to specific buckets, not all of them. For example: ListBuckets retu ...

Achieve compliance with the ESLint "no-var-requires" rule by utilizing an import statement for a module that lacks a declaration file

Within my NodeJS/TypeScript project, I am successfully using fluent-ffmpeg. To utilize it, I have to import the path properties from both ffmpeg-installer/ffmpeg and ffprobe-installer/ffprobe. The import for ffmpeg appears as follows: import * as ffmpegIn ...

Guide for retrieving a user object from an HTTP request

I am looking to retrieve only the user object from the request. public async getUserByHash(hash: IHash) { this.logger.log('Hash for check email accessed'); const user = await this.hashRepository.findOne({ select: ['id', ...

How can a parent's method be activated only after receiving an event emitter from the child and ensuring that the parent's ngIf condition is met?

Every time the element in the child template is clicked, it triggers the method activateService(name) and emits an event with the name of the selected service using the event emitter serviceSelected. The parent component's method scrollDown(name) is t ...

Firebase data not appearing on screen despite using the async pipe for observables

My current challenge involves accessing data based on an id from Firebase, which comes back as an observable. Upon logging it to the console, I can confirm that the Observable is present. However, the issue arises when attempting to display this data on th ...

Having trouble establishing a connection with mongoose and typescript

When attempting to establish a connection using mongoose, I consistently encounter the errors outlined below. However, if I use MongoClient instead, everything functions as expected. import connectMongo from '../../lib/connectMongo' console.log( ...

Ionic3 footer using ion-tabs

Is there a way to create a common footer for all pages with 5 buttons, where the first button is selected by default? The page opened by this first button should have three tabs. I have already created the tabs but am unsure how to add the footer without r ...

Tips for accessing the index of an image while hovering with the mouse in an Angular application

I am working on integrating the following features into my project. I want to implement an image box with functionality similar to the one in this Demo Implementation link. In my code, I have already implemented the side box feature, but I would like to k ...

The anticipated data originates from the 'style' attribute, which is formally noted within the 'IntrinsicAttributes & TextInputProps & RefAttributes<TextInput>' type

I have been working on developing a text form using typescript within the React Native framework. To accomplish this, I created a TextInput component specifically designed for email and password inputs. Below is the code I have been working with: TextInpu ...

Exploring the ckeditor5-typing plugin within CKEditor

Currently, I am in the process of developing a soft keyboard using CKEditor. One part of this involves transforming text upon input (which I have completed) and occasionally needing to delete a key (where I am currently facing challenges). Below is the sni ...

Sending data to the makeStyle function in TypeScript

How can I set the style of backgroundImage in my React component based on the value of post.mainImage? Here is the code snippet: import React from 'react'; import { Post } from '../interfaces'; import { makeStyles, createStyles, Theme ...

Using brackets around or after an expression in Typescript

Exploring Typescript: Is there a distinction between the two square bracket notations? After running some tests, it appears they function equivalently. Any insights would be appreciated! interface test { a: string; b: string; } const x: test[] = [{a ...

Launching a new tab with a specific URL using React

I'm attempting to create a function that opens a new tab with the URL stored in item.url. The issue is, the item.url property is provided by the client, not by me. Therefore, I can't guarantee whether it begins with https:// or http://. For insta ...

How can I access the most up-to-date state value in an event listener within a React element?

Here is the code I am working with: var C = () => { var [s, setS] = useState(0) var dRef = useRef<HTMLDivElement>() useMount(() => { setShortcut("ArrowDown", () => { setS(s+1) }) }) return ...