Deployment: Deploying a NestJS Application

Deploying a NestJS application to a cloud platform (e.g., AWS, Google Cloud, Azure) or a containerized environment (e.g., Docker, Kubernetes).


Monitoring and Logging in NestJS

Introduction

Effective monitoring and logging are crucial for maintaining the health, performance, and stability of NestJS applications in production. They provide insights into application behavior, allowing you to identify and resolve issues quickly. This document outlines the importance of monitoring and logging and provides guidance on implementing solutions using Prometheus, Grafana, and the ELK stack.

Monitoring and Logging: What and Why?

What is Monitoring?

Monitoring involves collecting and analyzing metrics about your application's performance and resource utilization. These metrics can include things like:

  • CPU usage
  • Memory consumption
  • Request latency
  • Error rates
  • Database query times
  • Custom business metrics (e.g., number of new users, completed transactions)

Monitoring tools provide a real-time view of your application's health, enabling you to detect anomalies and potential problems before they impact users.

What is Logging?

Logging involves recording events that occur within your application. These events can include:

  • Errors and exceptions
  • Warnings
  • Debug information
  • Audit trails
  • User activity

Logs provide valuable context for troubleshooting issues. They allow you to trace the sequence of events that led to an error, understand user behavior, and identify performance bottlenecks.

Why are Monitoring and Logging Important?

Implementing comprehensive monitoring and logging offers several benefits:

  • Early Problem Detection: Identify issues before they impact users, minimizing downtime and service disruptions.
  • Faster Troubleshooting: Logs provide the context needed to quickly diagnose and resolve problems.
  • Performance Optimization: Metrics help you identify performance bottlenecks and optimize your code.
  • Security Auditing: Logs can be used to track user activity and detect suspicious behavior.
  • Capacity Planning: Monitoring resource utilization helps you plan for future growth and ensure sufficient capacity.
  • Business Insights: Custom metrics can provide valuable insights into user behavior and business performance.

Implementing Monitoring and Logging Solutions in NestJS

This section describes how to implement monitoring and logging solutions in your NestJS application using Prometheus, Grafana, and the ELK stack.

1. Prometheus: Metrics Collection

Prometheus is a popular open-source monitoring and alerting toolkit. It collects metrics from your application and stores them in a time-series database.

Installation and Configuration:

First, install the necessary packages:

npm install --save prometheus-client @nestjs/terminus

Then, create a module (e.g., prometheus.module.ts) to expose Prometheus metrics:

 import { Module } from '@nestjs/common';
import { PrometheusService } from './prometheus.service';
import { TerminusModule } from '@nestjs/terminus';

@Module({
  imports: [TerminusModule],
  providers: [PrometheusService],
  exports: [PrometheusService],
})
export class PrometheusModule {} 

Create a service (e.g., prometheus.service.ts) to manage Prometheus metrics:

 import { Injectable } from '@nestjs/common';
import { Registry, collectDefaultMetrics, Histogram } from 'prom-client';

@Injectable()
export class PrometheusService {
  public register: Registry;
  public httpRequestDurationMicroseconds: Histogram;

  constructor() {
    this.register = new Registry();
    this.register.setDefaultLabels({
      app: 'your-nestjs-app', // Replace with your application name
    });
    collectDefaultMetrics({ register: this.register });

    this.httpRequestDurationMicroseconds = new Histogram({
      name: 'http_request_duration_seconds',
      help: 'Duration of HTTP requests in seconds',
      labelNames: ['method', 'route', 'code'],
      buckets: [0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10], // Customize buckets as needed
      registers: [this.register],
    });
  }
} 

Now, create a controller (e.g., prometheus.controller.ts) to expose the metrics endpoint:

 import { Controller, Get, Header, Res } from '@nestjs/common';
import { Response } from 'express';
import { PrometheusService } from './prometheus.service';

@Controller('metrics')
export class PrometheusController {
  constructor(private readonly prometheusService: PrometheusService) {}

  @Get()
  @Header('Content-Type', 'text/plain')
  async metrics(@Res() response: Response): Promise {
    response.send(await this.prometheusService.register.metrics());
  }
} 

Finally, register the PrometheusModule and PrometheusController in your AppModule:

 import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrometheusModule } from './prometheus/prometheus.module';
import { PrometheusController } from './prometheus/prometheus.controller';

@Module({
  imports: [PrometheusModule],
  controllers: [AppController, PrometheusController],
  providers: [AppService],
})
export class AppModule {} 

Now, you can access the Prometheus metrics at the /metrics endpoint (e.g., http://localhost:3000/metrics). You will need to configure Prometheus to scrape this endpoint.

Using the Metrics:

The example above registers a histogram metric, http_request_duration_seconds. You need to observe the duration of each HTTP request. A NestJS interceptor is ideal for this:

 import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { PrometheusService } from './prometheus.service';

@Injectable()
export class PrometheusInterceptor implements NestInterceptor {
  constructor(private readonly prometheusService: PrometheusService) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const now = Date.now();
    const httpContext = context.switchToHttp();
    const req = httpContext.getRequest();
    const res = httpContext.getResponse();
    const method = req.method;
    const route = req.route.path;

    return next.handle().pipe(
      tap(() => {
        const duration = Date.now() - now;
        const statusCode = res.statusCode;

        this.prometheusService.httpRequestDurationMicroseconds
          .labels(method, route, statusCode.toString())
          .observe(duration / 1000); // Convert milliseconds to seconds
      }),
    );
  }
} 

Apply the interceptor globally or to specific controllers/routes in your AppModule or controller definition:

 import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrometheusModule } from './prometheus/prometheus.module';
import { PrometheusController } from './prometheus/prometheus.controller';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { PrometheusInterceptor } from './prometheus/prometheus.interceptor';

@Module({
  imports: [PrometheusModule],
  controllers: [AppController, PrometheusController],
  providers: [
    AppService,
    {
      provide: APP_INTERCEPTOR,
      useClass: PrometheusInterceptor,
    },
  ],
})
export class AppModule {} 

This will record the duration of each HTTP request and expose it as a Prometheus metric.

2. Grafana: Visualization

Grafana is a data visualization and monitoring tool that allows you to create dashboards based on the metrics collected by Prometheus.

Configuration:

After installing Grafana, you need to configure it to connect to your Prometheus instance. Follow these steps:

  1. Add Prometheus as a data source in Grafana.
  2. Create a new dashboard in Grafana.
  3. Add panels to the dashboard to visualize the metrics collected by Prometheus (e.g., HTTP request duration, CPU usage).

Grafana allows you to create custom queries using PromQL (Prometheus Query Language) to visualize specific metrics and trends.

3. ELK Stack (Elasticsearch, Logstash, Kibana): Centralized Logging

The ELK stack is a powerful solution for centralized logging. It consists of:

  • Elasticsearch: A distributed search and analytics engine that stores your logs.
  • Logstash: A data processing pipeline that collects, parses, and transforms your logs.
  • Kibana: A data visualization tool that allows you to explore and analyze your logs.

Implementation:

Here's a basic outline of how to implement the ELK stack for logging in your NestJS application:

  1. Install Winston and Winston-Loki: These packages allow you to send logs to Loki, a log aggregation system (often used with Grafana).
    npm install winston winston-loki
  2. Configure Winston: Create a custom logger using Winston and configure it to send logs to Loki. You can also configure different log levels (e.g., debug, info, warn, error).
  3. Configure Logstash (Optional): If you need to perform complex log processing or transformation before sending logs to Elasticsearch, you can use Logstash. However, for basic logging, you might be able to skip Logstash and send logs directly to Elasticsearch. Logstash configuration involves defining input, filter, and output plugins.
  4. Configure Elasticsearch: Set up an Elasticsearch index to store your logs.
  5. Configure Kibana: Configure Kibana to connect to your Elasticsearch instance and create dashboards to visualize and analyze your logs.

NestJS Logging Example with Winston and Winston-Loki

First, install the required packages:

npm install winston winston-loki

Create a logging service (e.g., logger.service.ts):

 import { Injectable, LoggerService } from '@nestjs/common';
import { createLogger, format, transports, Logger } from 'winston';
import LokiTransport from 'winston-loki';

@Injectable()
export class Logger implements LoggerService {
  private logger: Logger;

  constructor() {
    this.logger = createLogger({
      format: format.combine(
        format.timestamp(),
        format.json(),
      ),
      transports: [
        new transports.Console(),
        new LokiTransport({
          host: 'http://localhost:3100', // Replace with your Loki instance URL
          labels: { app: 'your-nestjs-app' }, // Replace with your app name
          json: true,
          format: format.json(),
        }),
      ],
    });
  }

  log(message: string, context?: string) {
    this.logger.info(message, { context });
  }

  error(message: string, trace: string, context?: string) {
    this.logger.error(message, { trace, context });
  }

  warn(message: string, context?: string) {
    this.logger.warn(message, { context });
  }

  debug(message: string, context?: string) {
    this.logger.debug(message, { context });
  }

  verbose(message: string, context?: string) {
    this.logger.verbose(message, { context });
  }
} 

Replace http://localhost:3100 with the URL of your Loki instance. You'll need to have Loki running separately (e.g., using Docker). You can then use Grafana to query and visualize logs stored in Loki.

Use the custom logger in your NestJS application:

 import { Injectable } from '@nestjs/common';
import { Logger } from './logger.service';

@Injectable()
export class AppService {
  constructor(private readonly logger: Logger) {}

  getHello(): string {
    this.logger.log('Hello from AppService', 'AppService');
    return 'Hello World!';
  }

  async throwError(): Promise<void> {
    try {
      throw new Error('This is a test error');
    } catch (error) {
      this.logger.error('Error occurred', error.stack, 'AppService');
    }
  }
} 

Bind the custom logger to the NestJS Logger service in your AppModule:

 import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { Logger } from './logger.service';
import { PrometheusModule } from './prometheus/prometheus.module';
import { PrometheusController } from './prometheus/prometheus.controller';

@Module({
  imports: [PrometheusModule],
  controllers: [AppController, PrometheusController],
  providers: [
    AppService,
    {
      provide: Logger,
      useClass: Logger,
    },
  ],
})
export class AppModule {} 

Now, any time you use the Logger service in your NestJS application, the logs will be sent to both the console and Loki (and visualized in Grafana, assuming you have it configured). This example avoids the full ELK stack to focus on the Winston/Loki combination, which is frequently used.

Best Practices for Monitoring and Logging

  • Use Structured Logging: Log messages in a structured format (e.g., JSON) to make them easier to parse and analyze.
  • Include Context: Add relevant context to your log messages, such as user IDs, request IDs, and transaction IDs.
  • Set Appropriate Log Levels: Use different log levels (e.g., debug, info, warn, error) to categorize log messages and filter them based on their severity.
  • Aggregate Logs: Centralize your logs in a single location to make them easier to search and analyze.
  • Monitor Key Metrics: Focus on monitoring key metrics that are critical to your application's performance and availability.
  • Set Up Alerts: Configure alerts to notify you when critical metrics exceed predefined thresholds.
  • Regularly Review Logs and Metrics: Make it a habit to regularly review your logs and metrics to identify potential issues and optimize your application's performance.
  • Secure Your Logs: Protect your logs from unauthorized access by implementing appropriate security measures.