Skip to content

Health Monitoring

Monitor Redis connections and handle failures gracefully.

Connection Status

typescript
enum ConnectionStatus {
  DISCONNECTED = 'disconnected',
  CONNECTING = 'connecting',
  CONNECTED = 'connected',
  RECONNECTING = 'reconnecting',
  ERROR = 'error',
}

Health Checks

Single Client Health Check

typescript
import { Injectable, Inject } from '@nestjs/common';
import { CLIENT_MANAGER, RedisClientManager, IHealthStatus } from '@nestjs-redisx/core';

@Injectable()
export class HealthService {
  constructor(
    @Inject(CLIENT_MANAGER)
    private readonly clientManager: RedisClientManager,
  ) {}

  async checkRedisHealth(): Promise<IHealthStatus> {
    return this.clientManager.healthCheck('default') as Promise<IHealthStatus>;
  }
}

All Clients Health Check

typescript
async checkAllClients(): Promise<IHealthStatus[]> {
  return this.clientManager.healthCheck() as Promise<IHealthStatus[]>;
}

Health Status Response

typescript
interface IHealthStatus {
  name: string;
  healthy: boolean;
  status: ConnectionStatus;
  latency: number | null;      // Ping latency in ms
  lastError: string | null;
  lastCheckAt: Date;
  metadata: {
    driverType: string;
    connectionType: string;
    reconnectAttempts: number;
    uptime: number;            // Milliseconds
  };
}

NestJS Terminus Integration

Health Indicator

typescript
import { Injectable, Inject } from '@nestjs/common';
import {
  HealthIndicator,
  HealthIndicatorResult,
  HealthCheckError,
} from '@nestjs/terminus';
import { CLIENT_MANAGER, RedisClientManager } from '@nestjs-redisx/core';

@Injectable()
export class RedisHealthIndicator extends HealthIndicator {
  constructor(
    @Inject(CLIENT_MANAGER)
    private readonly clientManager: RedisClientManager,
  ) {
    super();
  }

  async isHealthy(key: string): Promise<HealthIndicatorResult> {
    const healthStatuses = await this.clientManager.healthCheck();
    const statuses = Array.isArray(healthStatuses)
      ? healthStatuses
      : [healthStatuses];

    const isHealthy = statuses.every((s) => s.healthy);
    const result = this.getStatus(key, isHealthy, {
      clients: statuses.map((s) => ({
        name: s.name,
        status: s.status,
        latency: s.latency,
      })),
    });

    if (isHealthy) {
      return result;
    }

    throw new HealthCheckError('Redis health check failed', result);
  }
}

Health Controller

typescript
import { Controller, Get } from '@nestjs/common';
import { HealthCheck, HealthCheckService } from '@nestjs/terminus';
import { RedisHealthIndicator } from './terminus-indicator.usage';

@Controller('health')
export class HealthController {
  constructor(
    private health: HealthCheckService,
    private redisHealth: RedisHealthIndicator,
  ) {}

  @Get()
  @HealthCheck()
  check() {
    return this.health.check([
      () => this.redisHealth.isHealthy('redis'),
    ]);
  }
}

Connection Statistics

Get Stats

typescript
const stats = this.clientManager.getStats();

Stats Response

typescript
interface IConnectionStats {
  totalClients: number;
  connectedClients: number;
  disconnectedClients: number;
  errorClients: number;
  clients: Record<string, IClientStats>;
  collectedAt: Date;
}

interface IClientStats {
  name: string;
  status: ConnectionStatus;
  commandsExecuted: number;
  errors: number;
  reconnections: number;
  averageLatency: number;
  peakLatency: number;
  lastActivityAt: Date | null;
  connectedAt: Date | null;
  uptime: number;
}

Stats Endpoint

typescript
import { Controller, Get, Inject } from '@nestjs/common';
import { CLIENT_MANAGER, RedisClientManager, IConnectionStats } from '@nestjs-redisx/core';

@Controller('admin')
export class AdminController {
  constructor(
    @Inject(CLIENT_MANAGER)
    private readonly clientManager: RedisClientManager,
  ) {}

  @Get('redis/stats')
  getRedisStats(): IConnectionStats {
    return this.clientManager.getStats();
  }
}

Auto-Reconnection

Default Behavior

Automatic reconnection with exponential backoff:

  • Initial delay: 1000ms
  • Maximum delay: 30000ms
  • Backoff multiplier: 2x
  • Jitter: enabled (prevents thundering herd)
  • Max attempts: unlimited

Custom Reconnection Options

typescript
await this.clientManager.createClient('custom', config, {
  reconnection: {
    maxAttempts: 10,
    initialDelay: 500,
    maxDelay: 10000,
    backoffMultiplier: 1.5,
    enableJitter: true,
  },
  driverType: 'ioredis',
  metadata: {
    name: 'custom',
    config,
    status: ConnectionStatus.DISCONNECTED,
    reconnectAttempts: 0,
  },
});
typescript
await this.clientManager.createClient('custom', config, {
  reconnection: {
    maxAttempts: 10,
    initialDelay: 500,
    maxDelay: 10000,
    backoffMultiplier: 1.5,
    enableJitter: true,
  },
  driverType: 'node-redis',
  metadata: {
    name: 'custom',
    config,
    status: ConnectionStatus.DISCONNECTED,
    reconnectAttempts: 0,
  },
});

Reconnection Timeline

Attempt 1: 500ms (initial delay)
Attempt 2: 750ms (500 * 1.5)
Attempt 3: 1125ms (750 * 1.5)
Attempt 4: 1687ms (1125 * 1.5)
...
Attempt N: capped at 10000ms (max delay)

Event Monitoring

Manager Events

typescript
import { ManagerEvent } from '@nestjs-redisx/core';

enum ManagerEvent {
  CONNECTED = 'manager:connected',
  DISCONNECTED = 'manager:disconnected',
  RECONNECTING = 'manager:reconnecting',
  ERROR = 'manager:error',
  CREATED = 'manager:created',
  REMOVED = 'manager:removed',
}

Event Listener

typescript
import { Injectable, OnModuleInit, Inject, Logger } from '@nestjs/common';
import {
  CLIENT_MANAGER,
  RedisClientManager,
  ManagerEvent,
} from '@nestjs-redisx/core';

@Injectable()
export class RedisMonitor implements OnModuleInit {
  private readonly logger = new Logger(RedisMonitor.name);

  constructor(
    @Inject(CLIENT_MANAGER)
    private readonly clientManager: RedisClientManager,
  ) {}

  onModuleInit(): void {
    this.clientManager.on(ManagerEvent.CONNECTED, (data) => {
      this.logger.log(`Redis client '${data.name}' connected`);
    });

    this.clientManager.on(ManagerEvent.DISCONNECTED, (data) => {
      this.logger.warn(`Redis client '${data.name}' disconnected`);
    });

    this.clientManager.on(ManagerEvent.RECONNECTING, (data) => {
      this.logger.log(
        `Redis client '${data.name}' reconnecting (attempt ${data.metadata?.attempt})`,
      );
    });

    this.clientManager.on(ManagerEvent.ERROR, (data) => {
      this.logger.error(
        `Redis client '${data.name}' error: ${data.error?.message}`,
      );
    });
  }
}

Prometheus Metrics

typescript
import { Injectable, OnModuleInit, Inject } from '@nestjs/common';
import { Counter, Gauge, Histogram } from 'prom-client';
import { CLIENT_MANAGER, RedisClientManager, ManagerEvent } from '@nestjs-redisx/core';

@Injectable()
export class RedisMetricsCollector implements OnModuleInit {
  private readonly connectionGauge = new Gauge({
    name: 'redis_connection_status',
    help: 'Redis connection status (1=connected, 0=disconnected)',
    labelNames: ['client'],
  });

  private readonly reconnectCounter = new Counter({
    name: 'redis_reconnect_total',
    help: 'Total Redis reconnection attempts',
    labelNames: ['client'],
  });

  private readonly errorCounter = new Counter({
    name: 'redis_error_total',
    help: 'Total Redis errors',
    labelNames: ['client'],
  });

  private readonly latencyHistogram = new Histogram({
    name: 'redis_latency_seconds',
    help: 'Redis command latency',
    labelNames: ['client'],
    buckets: [0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1],
  });

  constructor(
    @Inject(CLIENT_MANAGER)
    private readonly clientManager: RedisClientManager,
  ) {}

  onModuleInit(): void {
    this.clientManager.on(ManagerEvent.CONNECTED, (data) => {
      this.connectionGauge.labels(data.name).set(1);
    });

    this.clientManager.on(ManagerEvent.DISCONNECTED, (data) => {
      this.connectionGauge.labels(data.name).set(0);
    });

    this.clientManager.on(ManagerEvent.RECONNECTING, (data) => {
      this.reconnectCounter.labels(data.name).inc();
    });

    this.clientManager.on(ManagerEvent.ERROR, (data) => {
      this.errorCounter.labels(data.name).inc();
    });
  }

  recordLatency(client: string, latency: number): void {
    this.latencyHistogram.labels(client).observe(latency / 1000);
  }
}

Client Metadata

Get Metadata

typescript
const metadata = this.clientManager.getMetadata('default');

Metadata Structure

typescript
interface IClientMetadata {
  name: string;
  config: ConnectionConfig;
  status: ConnectionStatus;
  connectedAt?: Date;
  lastError?: Error;
  reconnectAttempts: number;
}

Update Metadata

typescript
this.clientManager.updateMetadata('default', {
  // Custom metadata fields
});

Graceful Shutdown

Automatic Cleanup

RedisModule handles cleanup automatically via NestJS lifecycle hooks:

typescript
// In RedisService and RedisClientManager
async onModuleDestroy(): Promise<void> {
  await this.clientManager.closeAll();
}

Manual Cleanup

typescript
// Close specific client
await this.clientManager.closeClient('cache');

// Close all clients
await this.clientManager.closeAll();

Shutdown Timeout

Configure graceful shutdown timeout:

typescript
RedisModule.forRoot({
  clients: { ... },
  global: {
    gracefulShutdown: true,
    gracefulShutdownTimeout: 10000,  // 10 seconds
  },
})

Best Practices

Health Check Interval

typescript
import { Injectable, OnModuleInit, OnModuleDestroy, Inject } from '@nestjs/common';
import { CLIENT_MANAGER, RedisClientManager } from '@nestjs-redisx/core';

// Periodic health check
@Injectable()
export class HealthMonitor implements OnModuleInit, OnModuleDestroy {
  private interval: NodeJS.Timeout;

  constructor(
    @Inject(CLIENT_MANAGER)
    private readonly clientManager: RedisClientManager,
  ) {}

  onModuleInit(): void {
    this.interval = setInterval(async () => {
      const health = await this.clientManager.healthCheck();
      // Process health results
    }, 30000);  // Every 30 seconds
  }

  onModuleDestroy(): void {
    clearInterval(this.interval);
  }
}

Alert on Errors

typescript
this.clientManager.on(ManagerEvent.ERROR, async (data) => {
  // Send alert
  await this.alertService.send({
    level: 'error',
    message: `Redis client '${data.name}' error`,
    error: data.error?.message,
  });
});

Log Connection Changes

typescript
this.clientManager.on(ManagerEvent.CONNECTED, (data) => {
  this.logger.log(`Connected: ${data.name}`);
});

this.clientManager.on(ManagerEvent.DISCONNECTED, (data) => {
  this.logger.warn(`Disconnected: ${data.name}`);
});

Next Steps

Released under the MIT License.