Skip to content

Custom Metrics

Add custom metrics to track application-specific behavior.

Inject Metrics Service

typescript
import { Injectable, Inject, OnModuleInit } from '@nestjs/common';
import { METRICS_SERVICE, IMetricsService } from '@nestjs-redisx/metrics';
import { CreateOrderDto, Order, OrderRepo } from './types';

@Injectable()
export class OrderService implements OnModuleInit {
  constructor(
    @Inject(METRICS_SERVICE) private readonly metrics: IMetricsService,
    private readonly orderRepo: OrderRepo,
  ) {}

  onModuleInit(): void {
    this.metrics.registerCounter(
      'orders_created_total',
      'Total orders created',
      ['status', 'payment_method'],
    );
  }

  async createOrder(dto: CreateOrderDto): Promise<Order> {
    const order = await this.orderRepo.create(dto);

    // Increment counter
    this.metrics.incrementCounter('orders_created_total', {
      status: order.status,
      payment_method: order.paymentMethod,
    });

    return order;
  }
}

Counter

Track cumulative values that only increase.

Register and Use Counter

typescript
import { Injectable, Inject, OnModuleInit } from '@nestjs/common';
import { METRICS_SERVICE, IMetricsService } from '@nestjs-redisx/metrics';
import { CreateOrderDto, Order, OrderRepo } from './types';

@Injectable()
export class OrderService implements OnModuleInit {
  constructor(
    @Inject(METRICS_SERVICE) private readonly metrics: IMetricsService,
    private readonly orderRepo: OrderRepo,
  ) {}

  onModuleInit(): void {
    this.metrics.registerCounter(
      'orders_created_total',
      'Total orders created',
      ['status', 'payment_method'],
    );
  }

  async createOrder(dto: CreateOrderDto): Promise<Order> {
    const order = await this.orderRepo.create(dto);

    // Increment counter
    this.metrics.incrementCounter('orders_created_total', {
      status: order.status,
      payment_method: order.paymentMethod,
    });

    return order;
  }
}

Counter Methods

typescript
// Increment by 1
this.metrics.incrementCounter('orders_created_total');

// Increment with labels
this.metrics.incrementCounter('orders_created_total', {
  status: 'completed',
});

// Increment by specific amount
this.metrics.incrementCounter('orders_created_total', {
  status: 'completed',
}, 5);

Counter Examples

Track API Requests:

typescript
onModuleInit(): void {
  this.metrics.registerCounter(
    'api_requests_total',
    'Total API requests',
    ['method', 'endpoint', 'status'],
  );
}

// Usage
this.metrics.incrementCounter('api_requests_total', {
  method: 'GET',
  endpoint: '/api/users',
  status: '200',
});

Track Errors:

typescript
onModuleInit(): void {
  this.metrics.registerCounter(
    'errors_total',
    'Total errors',
    ['type', 'severity'],
  );
}

// Usage
try {
  await this.process();
} catch (error) {
  this.metrics.incrementCounter('errors_total', {
    type: error.name,
    severity: 'high',
  });
  throw error;
}

Gauge

Track values that can go up or down.

Register and Use Gauge

typescript
import { Injectable, Inject, OnModuleInit } from '@nestjs/common';
import { METRICS_SERVICE, IMetricsService } from '@nestjs-redisx/metrics';
import { RedisClient } from './types';

@Injectable()
export class QueueService implements OnModuleInit {
  constructor(
    @Inject(METRICS_SERVICE) private readonly metrics: IMetricsService,
    private readonly redis: RedisClient,
  ) {}

  onModuleInit(): void {
    this.metrics.registerGauge(
      'queue_size',
      'Current queue size',
      ['queue'],
    );

    // Update periodically
    setInterval(() => this.updateQueueSize(), 15000);
  }

  private async updateQueueSize(): Promise<void> {
    const size = await this.redis.llen('queue:orders');
    this.metrics.setGauge('queue_size', size, { queue: 'orders' });
  }
}

Gauge Methods

typescript
// Set to specific value
this.metrics.setGauge('queue_size', 42, { queue: 'orders' });

// Increment by 1
this.metrics.incrementGauge('queue_size', { queue: 'orders' });

// Increment by specific amount
this.metrics.incrementGauge('queue_size', { queue: 'orders' }, 5);

// Decrement by 1
this.metrics.decrementGauge('queue_size', { queue: 'orders' });

// Decrement by specific amount
this.metrics.decrementGauge('queue_size', { queue: 'orders' }, 3);

Gauge Examples

Track Active Connections:

typescript
onModuleInit(): void {
  this.metrics.registerGauge(
    'active_connections',
    'Currently active connections',
  );
}

// On connect
this.metrics.incrementGauge('active_connections');

// On disconnect
this.metrics.decrementGauge('active_connections');

Track Memory Usage:

typescript
onModuleInit(): void {
  this.metrics.registerGauge(
    'memory_usage_bytes',
    'Memory usage in bytes',
    ['type'],
  );
}

setInterval(() => {
  const mem = process.memoryUsage();
  this.metrics.setGauge('memory_usage_bytes', mem.heapUsed, { type: 'heap_used' });
  this.metrics.setGauge('memory_usage_bytes', mem.heapTotal, { type: 'heap_total' });
  this.metrics.setGauge('memory_usage_bytes', mem.rss, { type: 'rss' });
}, 15000);

Histogram

Track distribution of values (latency, sizes).

Register and Use Histogram

typescript
import { Injectable, Inject, OnModuleInit } from '@nestjs/common';
import { METRICS_SERVICE, IMetricsService } from '@nestjs-redisx/metrics';
import { PaymentDto, Payment, PaymentGateway } from './types';

@Injectable()
export class PaymentService implements OnModuleInit {
  constructor(
    @Inject(METRICS_SERVICE) private readonly metrics: IMetricsService,
    private readonly gateway: PaymentGateway,
  ) {}

  onModuleInit(): void {
    this.metrics.registerHistogram(
      'payment_duration_seconds',
      'Payment processing duration',
      ['provider'],
      [0.1, 0.5, 1, 2, 5, 10],
    );
  }

  async processPayment(dto: PaymentDto): Promise<Payment> {
    const stopTimer = this.metrics.startTimer('payment_duration_seconds', {
      provider: dto.provider,
    });

    try {
      const payment = await this.gateway.charge(dto);
      stopTimer();  // Records duration and returns seconds
      return payment;
    } catch (error) {
      stopTimer();
      throw error;
    }
  }
}

Histogram Methods

typescript
// Start timer — returns a function that stops and records duration
const stopTimer = this.metrics.startTimer('payment_duration_seconds', {
  provider: 'stripe',
});

// ... do work ...

// Stop timer (records duration in seconds, returns duration)
const durationSeconds = stopTimer();

// Or observe value directly
this.metrics.observeHistogram('payment_duration_seconds', 1.234, {
  provider: 'stripe',
});

Histogram Examples

Track Request Size:

typescript
onModuleInit(): void {
  this.metrics.registerHistogram(
    'request_size_bytes',
    'HTTP request size in bytes',
    ['endpoint'],
    [100, 1000, 10000, 100000, 1000000],
  );
}

// Usage
this.metrics.observeHistogram(
  'request_size_bytes',
  req.body.length,
  { endpoint: '/api/upload' },
);

Track Database Query Duration:

typescript
onModuleInit(): void {
  this.metrics.registerHistogram(
    'db_query_duration_seconds',
    'Database query duration',
    ['operation', 'table'],
    [0.001, 0.01, 0.1, 0.5, 1],
  );
}

// Usage
const stopTimer = this.metrics.startTimer('db_query_duration_seconds', {
  operation: 'SELECT',
  table: 'users',
});

const users = await this.userRepo.find();
stopTimer();

Real-World Examples

E-commerce Metrics

typescript
import { Injectable, Inject, OnModuleInit } from '@nestjs/common';
import { METRICS_SERVICE, IMetricsService } from '@nestjs-redisx/metrics';
import { Order } from '../types';

@Injectable()
export class OrderMetrics implements OnModuleInit {
  constructor(
    @Inject(METRICS_SERVICE) private readonly metrics: IMetricsService,
  ) {}

  onModuleInit(): void {
    this.metrics.registerCounter(
      'orders_created_total',
      'Total orders created',
      ['status', 'payment_method', 'country'],
    );

    this.metrics.registerHistogram(
      'order_value_dollars',
      'Order value in dollars',
      [],
      [10, 50, 100, 500, 1000, 5000],
    );

    this.metrics.registerHistogram(
      'order_processing_duration_seconds',
      'Time to process order',
      ['step'],
      [0.1, 0.5, 1, 2, 5],
    );

    this.metrics.registerGauge(
      'inventory_level',
      'Current inventory level',
      ['product_id'],
    );
  }

  trackOrderCreated(order: Order): void {
    this.metrics.incrementCounter('orders_created_total', {
      status: order.status,
      payment_method: order.paymentMethod,
      country: order.shippingAddress.country,
    });

    this.metrics.observeHistogram('order_value_dollars', order.total);
  }

  trackInventoryChange(productId: string, newLevel: number): void {
    this.metrics.setGauge('inventory_level', newLevel, {
      product_id: productId,
    });
  }
}

API Metrics

typescript
import { Injectable, Inject, OnModuleInit } from '@nestjs/common';
import { METRICS_SERVICE, IMetricsService } from '@nestjs-redisx/metrics';

@Injectable()
export class ApiMetrics implements OnModuleInit {
  constructor(
    @Inject(METRICS_SERVICE) private readonly metrics: IMetricsService,
  ) {}

  onModuleInit(): void {
    this.metrics.registerCounter(
      'http_requests_total',
      'Total HTTP requests',
      ['method', 'endpoint', 'status'],
    );

    this.metrics.registerHistogram(
      'http_request_duration_seconds',
      'HTTP request duration',
      ['method', 'endpoint'],
      [0.01, 0.05, 0.1, 0.5, 1, 2, 5],
    );

    this.metrics.registerCounter(
      'http_errors_total',
      'Total HTTP errors',
      ['method', 'endpoint', 'code'],
    );
  }

  trackRequest(
    method: string,
    endpoint: string,
    status: number,
    duration: number,
  ): void {
    this.metrics.incrementCounter('http_requests_total', {
      method,
      endpoint,
      status: status.toString(),
    });

    this.metrics.observeHistogram('http_request_duration_seconds', duration, {
      method,
      endpoint,
    });

    if (status >= 400) {
      this.metrics.incrementCounter('http_errors_total', {
        method,
        endpoint,
        code: status.toString(),
      });
    }
  }
}

Background Job Metrics

typescript
import { Injectable, Inject, OnModuleInit } from '@nestjs/common';
import { METRICS_SERVICE, IMetricsService } from '@nestjs-redisx/metrics';
import { Job } from '../types';

@Injectable()
export class JobMetrics implements OnModuleInit {
  constructor(
    @Inject(METRICS_SERVICE) private readonly metrics: IMetricsService,
  ) {}

  onModuleInit(): void {
    this.metrics.registerCounter(
      'jobs_processed_total',
      'Total jobs processed',
      ['type', 'status'],
    );

    this.metrics.registerHistogram(
      'job_duration_seconds',
      'Job processing duration',
      ['type'],
      [1, 5, 10, 30, 60, 300, 600],
    );

    this.metrics.registerGauge(
      'jobs_active',
      'Currently active jobs',
      ['type'],
    );

    this.metrics.registerGauge(
      'job_queue_size',
      'Jobs waiting in queue',
      ['type'],
    );
  }

  async processJob(type: string, job: Job): Promise<void> {
    this.metrics.incrementGauge('jobs_active', { type });
    const stopTimer = this.metrics.startTimer('job_duration_seconds', { type });

    try {
      await this.executeJob(job);

      this.metrics.incrementCounter('jobs_processed_total', {
        type,
        status: 'success',
      });
      stopTimer();
    } catch (error) {
      this.metrics.incrementCounter('jobs_processed_total', {
        type,
        status: 'error',
      });
      stopTimer();
      throw error;
    } finally {
      this.metrics.decrementGauge('jobs_active', { type });
    }
  }

  private async executeJob(_job: Job): Promise<void> {
    // Process the job
  }
}

Middleware Integration

typescript
import { Injectable, NestMiddleware, Inject, OnModuleInit } from '@nestjs/common';
import { METRICS_SERVICE, IMetricsService } from '@nestjs-redisx/metrics';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class MetricsMiddleware implements NestMiddleware, OnModuleInit {
  constructor(
    @Inject(METRICS_SERVICE) private readonly metrics: IMetricsService,
  ) {}

  onModuleInit(): void {
    this.metrics.registerHistogram(
      'http_request_duration_seconds',
      'HTTP request duration',
      ['method', 'route', 'status'],
    );
  }

  use(req: Request, res: Response, next: NextFunction): void {
    const stopTimer = this.metrics.startTimer('http_request_duration_seconds', {
      method: req.method,
      route: req.route?.path || req.path,
    });

    res.on('finish', () => {
      stopTimer();
    });

    next();
  }
}

Best Practices

1. Use descriptive names:

typescript
// ✅ Good
'orders_created_total'
'payment_duration_seconds'
'inventory_level'

// ❌ Bad
'count'
'time'
'value'

2. Include units in name:

typescript
// ✅ Good
'memory_bytes'
'duration_seconds'
'temperature_celsius'

// ❌ Bad
'memory'  // What unit?
'time'    // Seconds? Milliseconds?

3. Use low-cardinality labels:

typescript
// ✅ Good - Limited values
{ status: 'success' }  // success, error
{ method: 'GET' }      // GET, POST, etc.

// ❌ Bad - High cardinality
{ user_id: '12345' }   // Millions of users!
{ timestamp: '...' }   // Infinite values!

4. Choose correct metric type:

Use CaseType
Count eventsCounter
Current valueGauge
Latency/durationHistogram
Size distributionHistogram

Next Steps

Released under the MIT License.