In today's complex application landscapes, observability is essential, providing insights into application performance and system behavior. OpenTelemetry has emerged as a robust standard for distributed tracing, enabling developers to capture detailed telemetry data across diverse systems. When combined with the power of NestJS and RedisX, OpenTelemetry tracing can provide unparalleled visibility into Redis operations and application performance.
Why OpenTelemetry?
OpenTelemetry offers a vendor-agnostic way to collect telemetry data, such as traces and metrics, from applications. It supports various backends like Jaeger, Zipkin, and Prometheus, making it a versatile choice for observability in distributed systems.
RedisX and OpenTelemetry
NestJS RedisX is a modular toolkit designed to integrate seamlessly with Redis, offering a suite of plugins for caching, locking, rate limiting, and more. The Tracing Plugin within RedisX leverages OpenTelemetry to trace every Redis command executed through the toolkit, providing insights into operations like cache hits/misses, lock acquisitions, and stream processing.
Setting Up OpenTelemetry Tracing in NestJS with RedisX
To integrate OpenTelemetry tracing in your NestJS application using RedisX, you need to follow these steps:
- Install Necessary Packages: Install the core and tracing packages:
npm install @nestjs-redisx/core @nestjs-redisx/tracing
-
Configure the Tracing Plugin:
Use
TracingPlugin.registerAsync()for configuration, which allows dynamic setup with theConfigService.
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { RedisModule } from '@nestjs-redisx/core';
import { TracingPlugin } from '@nestjs-redisx/tracing';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
RedisModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
plugins: [
TracingPlugin.registerAsync({
useFactory: (config: ConfigService) => ({
enabled: config.get('TRACING_ENABLED', true),
serviceName: config.get('SERVICE_NAME', 'my-service'),
exporter: {
type: 'otlp',
endpoint: config.get('OTLP_ENDPOINT', 'http://localhost:4318'),
},
sampling: {
strategy: 'ratio',
ratio: config.get('TRACING_SAMPLE_RATIO', 1.0),
},
}),
inject: [ConfigService],
}),
],
useFactory: (config: ConfigService) => ({
clients: {
host: config.get('REDIS_HOST', 'localhost'),
port: config.get('REDIS_PORT', 6379),
},
}),
}),
],
})
export class AppModule {}
- Implement Tracing in Services: With RedisX, tracing is automatically applied to all Redis operations. However, you can also create custom spans for more granular insights.
import { Injectable, Inject } from '@nestjs/common';
import { TRACING_SERVICE, ITracingService } from '@nestjs-redisx/tracing';
@Injectable()
export class UserService {
constructor(@Inject(TRACING_SERVICE) private readonly tracing: ITracingService) {}
async getUser(id: string): Promise<unknown> {
return this.tracing.withSpan('user.get', async () => {
this.tracing.setAttribute('user.id', id);
const user = await this.userRepo.findById(id);
this.tracing.addEvent('user.found', { 'user.email': user.email });
return user;
});
}
}
Benefits of Tracing with RedisX
- Detailed Insights: Every Redis command is traced, providing a clear picture of how data flows through your application.
- Performance Monitoring: Identify bottlenecks and optimize Redis usage by analyzing trace data.
- Error Diagnosis: Quickly pinpoint where errors occur in the Redis interaction chain.
- Seamless Integration: The Tracing Plugin integrates smoothly with other RedisX plugins, sharing the same Redis connection.
Integrating OpenTelemetry tracing with NestJS and RedisX will not only enhance your observability but also empower you to make data-driven decisions to optimize application performance. With the detailed insights that tracing provides, teams can improve system reliability and user experience, making it an indispensable part of modern application development.
nestjs redis tracing
Top comments (2)
The auto-tracing-on-every-Redis-command default is the right call — the failures that actually cost us were the ones where Redis looked fine in isolation but a cache miss cascaded into a slow downstream call nobody was tracing. One thing worth flagging for anyone wiring this up in prod: ratio sampling at 1.0 is great in dev but will bury you in span volume (and cost) under real traffic. We sample head-based low and force-sample anything that errors or crosses a latency threshold, so the spans we keep are the ones we'd actually open. Have you wired the RedisX spans into the wider trace context, so a cache lookup shows up on the same trace as the HTTP request that triggered it?
Good callout on the ratio: 1.0 - that snippet is dev-tuned and I should have flagged it in the article. For production the plugin ships sampling.strategy:
'parent' (a ParentBasedSampler with a ratio fallback for root spans), and that's what I'd actually recommend: Redis spans inherit whatever head-based
decision was already made for the request trace, so a low root ratio upstream keeps Redis span volume proportional automatically. Your
force-keep-on-error/latency policy is tail-sampling territory - we deliberately keep that out of process and lean on the collector's tail_sampling processor
for it. The spans carry ERROR status and exceptions via recordException, so an error policy picks them up with no extra wiring, and a latency policy covers
the slow ones.
And yes on trace context: spans are created against the active OpenTelemetry context, not a private one. If you're running the standard HTTP instrumentation
(@opentelemetry/instrumentation-http or auto-instrumentations), the request span is active when your handler runs, so every RedisX span - cache get, lock
acquire, the raw command spans -parents straight onto that request's trace. withSpan also pushes its own span into the context, so Redis calls inside your
custom spans nest correctly too. One thing worth knowing if you already bootstrap your own OTel SDK: the plugin registers a NodeTracerProvider on init, and
OTel only honors one global provider - init order decides whose exporter/sampler config wins. You get a single trace either way, but it's worth being
deliberate about which side owns the pipeline.