Microservices with NestJS

Introduction to microservices architecture and how to build microservices using NestJS with gRPC or message queues (e.g., RabbitMQ, Kafka).


Monitoring and Logging in NestJS Microservices

Explanation: Monitoring and Logging

Monitoring is the systematic process of collecting, analyzing, and displaying performance metrics and operational data from a software system. The goal is to provide real-time insights into the health, performance, and resource utilization of the system. Key aspects of monitoring include:

  • Availability: Ensuring the system is up and running.
  • Performance: Measuring response times, throughput, and latency.
  • Error Rates: Identifying and tracking the frequency of errors.
  • Resource Usage: Monitoring CPU, memory, disk, and network utilization.

Logging is the practice of recording events and actions that occur within an application. Logs provide a historical record of what happened, when it happened, and in what context. Logging is crucial for:

  • Debugging: Identifying the root cause of errors.
  • Auditing: Tracking user actions and security events.
  • Performance Analysis: Understanding performance bottlenecks.
  • Compliance: Meeting regulatory requirements for data retention.

In the context of microservices, effective monitoring and logging are particularly important because of the distributed nature of the system. When issues arise, you need to be able to quickly identify which service is causing the problem. Centralized monitoring and logging are essential for managing this complexity.

Implementing Monitoring and Logging in NestJS Microservices

Tools: Prometheus, Grafana, and Elasticsearch

These tools offer a powerful combination for monitoring and logging NestJS microservices:

  • Prometheus: A time-series database that collects metrics by scraping endpoints.
  • Grafana: A data visualization tool that connects to Prometheus (and other data sources) to create dashboards and alerts.
  • Elasticsearch: A distributed search and analytics engine that can store and analyze logs.

Steps for Implementation

  1. Expose Prometheus Metrics in NestJS

    Install the necessary packages:

    npm install --save prom-client @willsoto/nestjs-prometheus

    Create a module to expose the Prometheus metrics endpoint (e.g., prometheus.module.ts):

     import { Module } from '@nestjs/common';
    import { PrometheusModule } from '@willsoto/nestjs-prometheus';
    import { APP_INTERCEPTOR } from '@nestjs/core';
    import { MetricsInterceptor } from './metrics.interceptor';
    
    @Module({
      imports: [
        PrometheusModule.register({
          path: '/metrics',
          collectDefaultMetrics: true, // Optional: Collects Node.js metrics
        }),
      ],
      providers: [
        {
          provide: APP_INTERCEPTOR,
          useClass: MetricsInterceptor,
        },
      ],
    })
    export class PrometheusModule {} 

    Create an interceptor (metrics.interceptor.ts) to track request durations. This is optional, but highly recommended:

     import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
    import { Observable, tap } from 'rxjs';
    import { Counter, Histogram, register } from 'prom-client';
    
    const requestCounter = new Counter({
      name: 'http_requests_total',
      help: 'Total number of HTTP requests',
      labelNames: ['method', 'route', 'status'],
    });
    
    const requestHistogram = new Histogram({
      name: 'http_request_duration_seconds',
      help: 'Duration of HTTP requests in seconds',
      labelNames: ['method', 'route', 'status'],
      buckets: [0.1, 0.3, 0.5, 0.7, 1, 3, 5, 10], // Define bucket boundaries
    });
    
    @Injectable()
    export class MetricsInterceptor implements NestInterceptor {
      intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
        const start = Date.now();
        const httpContext = context.switchToHttp();
        const request = httpContext.getRequest<Request>();
        const route = request.route.path;
        const method = request.method;
    
        return next.handle().pipe(
          tap({
            next: (val) => {
              const duration = (Date.now() - start) / 1000; // Convert to seconds
              const statusCode = httpContext.getResponse().statusCode;
    
              requestCounter.inc({ method, route, status: statusCode });
              requestHistogram.observe({ method, route, status: statusCode }, duration);
            },
            error: (err) => {
              const duration = (Date.now() - start) / 1000;
              const statusCode = err.getStatus();
    
              requestCounter.inc({ method, route, status: statusCode });
              requestHistogram.observe({ method, route, status: statusCode }, duration);
            }
          })
        );
      }
    } 

    Import PrometheusModule into your main application module (e.g., app.module.ts):

     import { Module } from '@nestjs/common';
    import { PrometheusModule } from './prometheus.module'; // Import your PrometheusModule
    import { AppController } from './app.controller';
    import { AppService } from './app.service';
    
    @Module({
      imports: [PrometheusModule],
      controllers: [AppController],
      providers: [AppService],
    })
    export class AppModule {} 

    Now, your NestJS application will expose metrics at the /metrics endpoint. Prometheus can scrape these metrics.

  2. Configure Prometheus to Scrape NestJS Microservices

    Configure Prometheus to scrape the /metrics endpoint of each NestJS microservice. This is typically done in the prometheus.yml file. For example:

     global:
      scrape_interval:     15s
    
    scrape_configs:
      - job_name: 'nestjs-microservice-1'
        static_configs:
          - targets: ['nestjs-microservice-1:3000'] # Replace with the actual host and port
    
      - job_name: 'nestjs-microservice-2'
        static_configs:
          - targets: ['nestjs-microservice-2:3001']  # Replace with the actual host and port 
  3. Create Grafana Dashboards

    Connect Grafana to your Prometheus instance. Then, create dashboards to visualize the metrics. You can use PromQL (Prometheus Query Language) to query the data and display it in charts, graphs, and tables. Example queries:

    • Total requests: sum(http_requests_total)
    • Request duration (95th percentile): histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))
  4. Implement Logging with Elasticsearch

    Install necessary packages:

    npm install --save nest-winston winston winston-transport elasticsearch

    Create a logging module (e.g., logging.module.ts) and configure Winston with Elasticsearch transport:

     import { Module } from '@nestjs/common';
    import { WinstonModule } from 'nest-winston';
    import * as winston from 'winston';
    import { ElasticsearchTransport } from 'winston-transport-elasticsearch';
    
    const esTransportOpts = {
      level: 'info',
      clientOpts: { node: 'http://localhost:9200' }, // Replace with your Elasticsearch URL
      indexPrefix: 'nestjs-logs',
      indexSuffixPattern: '-%Y-%m-%d',
    };
    
    @Module({
      imports: [
        WinstonModule.forRoot({
          transports: [
            new winston.transports.Console(),
            new ElasticsearchTransport(esTransportOpts),
          ],
        }),
      ],
      exports: [WinstonModule], // Export WinstonModule to be used in other modules
    })
    export class LoggingModule {} 

    Import LoggingModule into your main application module (e.g., app.module.ts):

     import { Module } from '@nestjs/common';
    import { AppController } from './app.controller';
    import { AppService } from './app.service';
    import { LoggingModule } from './logging.module';
    
    @Module({
      imports: [LoggingModule],
      controllers: [AppController],
      providers: [AppService],
    })
    export class AppModule {} 

    Use the logger in your services and controllers:

     import { Injectable, Logger, Inject } from '@nestjs/common';
    import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
    
    @Injectable()
    export class AppService {
      constructor(@Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: Logger) {}
    
      getHello(): string {
        this.logger.log('Hello World!');
        this.logger.error('This is an error message');
        return 'Hello World!';
      }
    } 
  5. Create Kibana Dashboards

    Connect Kibana to your Elasticsearch instance. Create dashboards to visualize and analyze the logs. You can use Kibana's search and filtering capabilities to find specific events and identify patterns.

Centralized Logging Strategies

Centralized logging is crucial for managing logs from multiple microservices. Here are some strategies:

  • Centralized Logging Server: All microservices send their logs to a single logging server (e.g., using Fluentd, Logstash, or Rsyslog). This server then forwards the logs to Elasticsearch.
  • Message Queue: Microservices publish logs to a message queue (e.g., Kafka or RabbitMQ). A separate consumer service reads the logs from the queue and stores them in Elasticsearch.
  • Sidecar Container: Deploy a logging agent (e.g., Fluentd or Logstash) as a sidecar container alongside each microservice. The sidecar container collects logs from the microservice and forwards them to Elasticsearch.

For NestJS microservices, consider using a library like Winston with Elasticsearch transport as shown above, combined with one of the centralized logging strategies. Ensure each log entry includes relevant metadata such as:

  • Service Name: The name of the microservice that generated the log.
  • Timestamp: The time when the log event occurred.
  • Correlation ID: A unique identifier that links log entries across multiple microservices for a single transaction. This is crucial for tracing requests through the system.
  • Log Level: Indicates the severity of the event (e.g., DEBUG, INFO, WARN, ERROR).