Skip to content

Monitoring

Track cache performance and debug issues.

Cache Statistics

The getStats() method returns cumulative cache metrics since application startup:

typescript
const stats = await cache.getStats();

/*
{
  l1: {
    hits: 15234,       // L1 cache hits (cumulative)
    misses: 1876,      // L1 cache misses (cumulative)
    size: 523,         // Current entries in L1
  },
  l2: {
    hits: 45123,       // L2 cache hits (cumulative)
    misses: 2341,      // L2 cache misses (cumulative)
  },
  stampedePrevented: 142,  // Total stampede events prevented (cumulative)
}
*/

Counters are cumulative

hits, misses, and stampedePrevented are cumulative counters — they grow from zero at application startup and never reset. To calculate rates, take the delta between two snapshots over time.

Available Metrics

MetricTypeSourceDescription
l1.hitsCounterL1 adapter get()Entry found and not expired
l1.missesCounterL1 adapter get()Entry not found, or expired on access
l1.sizeGaugeL1 adapterCurrent number of entries in memory
l2.hitsCounterL2 adapter get(), getMany(), getSwr()Entry found in Redis
l2.missesCounterL2 adapter get(), getMany(), getSwr()Entry not found, or Redis error (fail-open)
stampedePreventedCounterStampede service protect()Concurrent request waited instead of loading

What's NOT in getStats()

The public getStats() returns CacheStats which is a subset of all available stats:

StatsAvailable in getStats()?How to access
SWR activeRevalidations, enabled, staleTtlNo@Inject(SWR_MANAGER).getStats()
Stampede activeFlights, totalWaiters, oldestFlightNo (only prevented)@Inject(STAMPEDE_PROTECTION).getStats()
typescript
import { Inject, Injectable } from '@nestjs/common';
import {
  STAMPEDE_PROTECTION,
  SWR_MANAGER,
  type IStampedeProtection,
  type ISwrManager,
} from '@nestjs-redisx/cache';

@Injectable()
export class DetailedStatsService {
  constructor(
    @Inject(STAMPEDE_PROTECTION) private readonly stampede: IStampedeProtection,
    @Inject(SWR_MANAGER) private readonly swr: ISwrManager,
  ) {}

  getDetailedStats() {
    const stampedeStats = this.stampede.getStats();
    // { activeFlights, totalWaiters, oldestFlight, prevented }

    const swrStats = this.swr.getStats();
    // { activeRevalidations, enabled, staleTtl }

    return { stampede: stampedeStats, swr: swrStats };
  }
}

Calculate Hit Rates

typescript
import { Injectable } from '@nestjs/common';
import { CacheService } from '@nestjs-redisx/cache';

@Injectable()
export class MonitoringService {
  constructor(private readonly cache: CacheService) {}

  async getCacheMetrics() {
    const stats = await this.cache.getStats();

    const l1Total = stats.l1.hits + stats.l1.misses;
    const l2Total = stats.l2.hits + stats.l2.misses;

    return {
      l1: {
        size: stats.l1.size,
        hitRate: l1Total > 0 ? (stats.l1.hits / l1Total * 100).toFixed(2) + '%' : 'N/A',
        hits: stats.l1.hits,
        misses: stats.l1.misses,
      },
      l2: {
        hitRate: l2Total > 0 ? (stats.l2.hits / l2Total * 100).toFixed(2) + '%' : 'N/A',
        hits: stats.l2.hits,
        misses: stats.l2.misses,
      },
      stampedePrevented: stats.stampedePrevented,
    };
  }
}

Built-in Metrics & Tracing Integration

The internal cache service has optional integration with MetricsPlugin and TracingPlugin. When these plugins are registered, metrics and traces are collected automatically — no custom code needed.

MetricsPlugin (automatic)

If MetricsPlugin is registered, the cache service automatically increments these Prometheus counters:

Metric nameLabelsWhen
redisx_cache_hits_totallayer: 'l1'L1 cache hit
redisx_cache_misses_totallayer: 'l1'L1 cache miss
redisx_cache_hits_totallayer: 'l2'L2 cache hit
redisx_cache_misses_totallayer: 'l2'L2 cache miss
redisx_cache_stampede_prevented_totalStampede prevention event
typescript
import { Module } from '@nestjs/common';
import { RedisModule } from '@nestjs-redisx/core';
import { CachePlugin } from '@nestjs-redisx/cache';
import { MetricsPlugin } from '@nestjs-redisx/metrics';

@Module({
  imports: [
    RedisModule.forRoot({
      clients: { host: 'localhost', port: 6379 },
      plugins: [
        new CachePlugin({ l1: { maxSize: 1000 } }),
        new MetricsPlugin(),  // Enables automatic metric collection
      ],
    }),
  ],
})
export class AppModule {}

TracingPlugin (automatic)

If TracingPlugin is registered, the cache service automatically creates OpenTelemetry spans:

Span nameAttributesWhen
cache.getcache.keyEvery get() call
cache.setcache.key, cache.ttlEvery set() call
typescript
plugins: [
  new CachePlugin({ l1: { maxSize: 1000 } }),
  new TracingPlugin(),  // Enables automatic span creation
],

Optional injection

Both integrations use @Optional() @Inject() — if the plugin is not registered, the cache works normally without metrics or traces.

Custom Prometheus (without MetricsPlugin)

If you prefer manual Prometheus integration with prom-client instead of MetricsPlugin:

typescript
import { Injectable, OnModuleInit } from '@nestjs/common';
import { Gauge, register } from 'prom-client';
import { CacheService } from '@nestjs-redisx/cache';

@Injectable()
export class CacheMetricsService implements OnModuleInit {
  private readonly l1HitRate = new Gauge({
    name: 'cache_l1_hit_rate',
    help: 'L1 cache hit rate (0-1)',
  });

  private readonly l1Size = new Gauge({
    name: 'cache_l1_size',
    help: 'Current L1 cache size',
  });

  private readonly l2HitRate = new Gauge({
    name: 'cache_l2_hit_rate',
    help: 'L2 cache hit rate (0-1)',
  });

  private interval: NodeJS.Timeout;

  constructor(private readonly cache: CacheService) {}

  onModuleInit() {
    this.interval = setInterval(() => this.collect(), 10_000);
  }

  private async collect() {
    const stats = await this.cache.getStats();

    const l1Total = stats.l1.hits + stats.l1.misses;
    const l2Total = stats.l2.hits + stats.l2.misses;

    this.l1HitRate.set(l1Total > 0 ? stats.l1.hits / l1Total : 0);
    this.l1Size.set(stats.l1.size);
    this.l2HitRate.set(l2Total > 0 ? stats.l2.hits / l2Total : 0);
  }

  getMetrics() {
    return register.metrics();
  }
}

Gauges, not Counters

Use Gauge for hit rates and sizes derived from cumulative stats. If you need Counter for hits/misses, calculate the delta between snapshots — don't pass cumulative values to Counter.inc().

Grafana Queries

Example PromQL queries (when using MetricsPlugin):

yaml
# L1 hit rate (over 5 minutes)
rate(redisx_cache_hits_total{layer="l1"}[5m])
  / (rate(redisx_cache_hits_total{layer="l1"}[5m]) + rate(redisx_cache_misses_total{layer="l1"}[5m]))

# L2 hit rate (over 5 minutes)
rate(redisx_cache_hits_total{layer="l2"}[5m])
  / (rate(redisx_cache_hits_total{layer="l2"}[5m]) + rate(redisx_cache_misses_total{layer="l2"}[5m]))

# Stampede prevention rate
rate(redisx_cache_stampede_prevented_total[5m])

Logging

The cache service uses NestJS Logger internally. Key log messages:

ServiceLevelMessage
WarmupServiceLOGStarting cache warmup for N keys...
WarmupServiceLOGCache warmup completed: X succeeded, Y failed (Zms)
SwrManagerServiceDEBUGStarting revalidation for key: {key}
SwrManagerServiceERRORRevalidation failed for key: {key}
StampedeProtectionServiceWARNFailed to acquire distributed lock: ...
CacheServiceWARNInvalid cache key "{key}": ...

To see DEBUG logs, configure NestJS log level:

typescript
const app = await NestFactory.create(AppModule, {
  logger: ['log', 'error', 'warn', 'debug'],
});

Next Steps

Released under the MIT License.