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 Case | Type |
|---|---|
| Count events | Counter |
| Current value | Gauge |
| Latency/duration | Histogram |
| Size distribution | Histogram |
Next Steps
- Grafana — Visualize custom metrics
- Alerting — Alert on custom metrics
- Troubleshooting — Debug metrics