Spans
Create custom spans to trace application-specific operations.
TracingService
Inject ITracingService to create custom spans.
Basic Usage
import { Injectable, Inject } from '@nestjs/common';
import { TRACING_SERVICE, ITracingService } from '@nestjs-redisx/tracing';
import { UserRepository } from './types';
@Injectable()
export class UserService {
constructor(
@Inject(TRACING_SERVICE) private readonly tracing: ITracingService,
private readonly userRepo: UserRepository,
) {}
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;
});
}
}Result:
GET /api/users/123
├── HTTP GET /api/users/123 (50ms)
│ └── user.get (45ms) ← Custom span
│ ├── Attribute: user.id = "123"
│ └── Event: user.foundAPI Reference
withSpan()
Create a span that wraps an async operation. Automatically ends the span and sets status.
withSpan<T>(
name: string,
fn: () => T | Promise<T>,
options?: ISpanOptions,
): Promise<T>Parameters:
| Parameter | Type | Description |
|---|---|---|
name | string | Span name |
fn | () => T | Promise<T> | Function to trace |
options | ISpanOptions | Optional span options |
Returns: Promise resolving to function result
Example:
const result = await this.tracing.withSpan(
'process.order',
async () => {
this.tracing.setAttribute('order.id', orderId);
return await this.processOrder(orderId);
},
{
kind: 'INTERNAL',
attributes: {
'service.name': 'order-service',
},
},
);startSpan()
Create a span manually (you must call end()).
startSpan(name: string, options?: ISpanOptions): ISpanExample:
const span = this.tracing.startSpan('long.operation');
try {
span.setAttribute('operation.type', 'batch');
await this.step1();
span.addEvent('step1.completed');
await this.step2();
span.addEvent('step2.completed');
span.setStatus('OK');
} catch (error) {
span.setStatus('ERROR');
span.recordException(error);
throw error;
} finally {
span.end(); // ← MUST call end()
}getCurrentSpan()
Get the currently active span.
getCurrentSpan(): ISpan | undefinedExample:
@Get('/users/:id')
async getUser(@Param('id') id: string) {
const span = this.tracing.getCurrentSpan();
if (span) {
span.setAttribute('user.id', id);
span.addEvent('user.requested');
}
return this.userService.getUser(id);
}Span Attributes
Add metadata to spans.
setAttribute()
span.setAttribute('user.id', '123');
span.setAttribute('user.role', 'admin');
span.setAttribute('cache.hit', false);
span.setAttribute('request.size', 1024);setAttributes()
span.setAttributes({
'user.id': '123',
'user.role': 'admin',
'user.email': 'user@example.com',
});Semantic Conventions
Use standard attribute names when possible.
// ✅ Good - Standard names
span.setAttributes({
'db.system': 'redis',
'db.operation': 'GET',
'db.redis.key': 'user:123',
});
// ❌ Bad - Custom names
span.setAttributes({
'database': 'redis',
'command': 'GET',
'key': 'user:123',
});Common conventions:
| Category | Attributes | Example |
|---|---|---|
| Database | db.system, db.operation, db.statement | db.system: "redis" |
| HTTP | http.method, http.url, http.status_code | http.method: "GET" |
| RPC | rpc.system, rpc.service, rpc.method | rpc.service: "UserService" |
| Messaging | messaging.system, messaging.operation | messaging.system: "redis-streams" |
Span Events
Record point-in-time occurrences.
addEvent()
span.addEvent('cache.miss');
span.addEvent('query.slow', { duration_ms: 150 });
span.addEvent('validation.failed', {
'validation.field': 'email',
'validation.error': 'Invalid format',
});Timeline:
Span: process.order (100ms)
├── t=0ms: Start
├── t=10ms: Event: validation.started
├── t=15ms: Event: validation.completed
├── t=20ms: Event: payment.processing
├── t=80ms: Event: payment.completed
└── t=100ms: EndSpan Status
Indicate success or failure.
Success
span.setStatus('OK');Error
span.setStatus('ERROR');Exception Recording
try {
await riskyOperation();
} catch (error) {
span.recordException(error);
span.setStatus('ERROR');
throw error;
}Captured information:
- Exception type
- Exception message
- Stack trace
- Timestamp
Span Kinds
Categorize span types.
INTERNAL
Internal operations (not the default — see CLIENT below).
await this.tracing.withSpan('process.data', async () => {
// ...
}, {
kind: 'INTERNAL',
});CLIENT
Outgoing requests (HTTP, database, Redis). This is the default kind.
await this.tracing.withSpan('api.call', async () => {
return axios.get('https://api.example.com/data');
}, {
kind: 'CLIENT',
});SERVER
Incoming requests.
// Automatically set by HTTP instrumentation
@Get('/users')
async getUsers() {
// Span kind: SERVER
}PRODUCER
Publishing messages.
await this.tracing.withSpan('message.publish', async () => {
await this.streamProducer.publish('orders', data);
}, {
kind: 'PRODUCER',
});CONSUMER
Consuming messages.
@StreamConsumer({ stream: 'orders' })
async handleOrder(message: IStreamMessage) {
// Span kind: CONSUMER
}Nested Spans
Create parent-child relationships.
Automatic Nesting
async processOrder(orderId: string): Promise<void> {
await this.tracing.withSpan('order.process', async () => {
this.tracing.setAttribute('order.id', orderId);
// Child span automatically linked
await this.tracing.withSpan('order.validate', async () => {
this.tracing.setAttribute('validation.type', 'schema');
await this.validateOrder(orderId);
});
// Another child span
await this.tracing.withSpan('payment.charge', async () => {
this.tracing.setAttribute('payment.method', 'card');
await this.chargePayment(orderId);
});
});
}Result:
order.process (100ms)
├── order.validate (10ms)
└── payment.charge (80ms)Manual Nesting
For manual span nesting, use withSpan which automatically propagates context:
await this.tracing.withSpan('parent', async () => {
// Child span automatically linked via context
await this.tracing.withSpan('child', async () => {
// ...
});
});Real-World Examples
E-commerce Order Processing
import { Injectable, Inject } from '@nestjs/common';
import { TRACING_SERVICE, ITracingService } from '@nestjs-redisx/tracing';
import { CreateOrderDto, Order, OrderRepository, PaymentService } from '../types';
@Injectable()
export class OrderService {
constructor(
@Inject(TRACING_SERVICE) private readonly tracing: ITracingService,
private readonly orderRepo: OrderRepository,
private readonly paymentService: PaymentService,
) {}
async createOrder(dto: CreateOrderDto): Promise<Order> {
return this.tracing.withSpan('order.create', async () => {
this.tracing.setAttribute('order.total', dto.total);
this.tracing.setAttribute('order.items_count', dto.items.length);
this.tracing.setAttribute('customer.id', dto.customerId);
// Validate
await this.tracing.withSpan('order.validate', async () => {
this.tracing.addEvent('validation.started');
await this.validateOrder(dto);
this.tracing.addEvent('validation.completed');
});
// Create order
const order = await this.tracing.withSpan('order.save', async () => {
this.tracing.setAttribute('db.operation', 'INSERT');
return this.orderRepo.create(dto);
});
// Process payment
await this.tracing.withSpan('payment.process', async () => {
this.tracing.setAttribute('payment.method', dto.paymentMethod);
this.tracing.setAttribute('payment.amount', dto.total);
try {
await this.paymentService.charge(order.id, dto.total);
this.tracing.addEvent('payment.succeeded');
} catch (error) {
this.tracing.addEvent('payment.failed', {
'error.type': (error as Error).name,
'error.message': (error as Error).message,
});
this.tracing.recordException(error as Error);
throw error;
}
});
return order;
});
}
private async validateOrder(_dto: CreateOrderDto): Promise<void> {
// validation logic
}
}User Registration Flow
import { Injectable, Inject, ConflictException } from '@nestjs/common';
import { TRACING_SERVICE, ITracingService } from '@nestjs-redisx/tracing';
import { RegisterDto, User, UserRepository, CacheStore, EmailService } from '../types';
@Injectable()
export class AuthService {
constructor(
@Inject(TRACING_SERVICE) private readonly tracing: ITracingService,
private readonly userRepo: UserRepository,
private readonly cache: CacheStore,
private readonly emailService: EmailService,
) {}
async register(dto: RegisterDto): Promise<User> {
return this.tracing.withSpan('user.register', async () => {
this.tracing.setAttribute('user.email', dto.email);
// Check if user exists
const existingUser = await this.tracing.withSpan(
'user.check_exists',
async () => {
this.tracing.addEvent('cache.lookup');
const cached = await this.cache.get(`user:${dto.email}`);
if (cached) {
this.tracing.setAttribute('cache.hit', true);
return cached;
}
this.tracing.setAttribute('cache.hit', false);
this.tracing.addEvent('db.query');
return this.userRepo.findByEmail(dto.email);
},
);
if (existingUser) {
this.tracing.addEvent('user.already_exists');
throw new ConflictException('User already exists');
}
// Hash password
const hashedPassword = await this.tracing.withSpan(
'password.hash',
async () => {
this.tracing.setAttribute('hash.algorithm', 'bcrypt');
this.tracing.setAttribute('hash.rounds', 10);
return `hashed_${dto.password}`;
},
);
// Create user
const user = await this.tracing.withSpan('user.create', async () => {
this.tracing.addEvent('db.insert');
return this.userRepo.create({
email: dto.email,
password: hashedPassword,
});
});
// Send welcome email
await this.tracing.withSpan('email.send_welcome', async () => {
this.tracing.setAttribute('email.to', user.email);
this.tracing.setAttribute('email.template', 'welcome');
await this.emailService.sendWelcome(user.email);
this.tracing.addEvent('email.sent');
});
return user;
});
}
}Batch Processing
import { Injectable, Inject } from '@nestjs/common';
import { TRACING_SERVICE, ITracingService } from '@nestjs-redisx/tracing';
import { User, UserRepository } from '../types';
@Injectable()
export class ReportService {
constructor(
@Inject(TRACING_SERVICE) private readonly tracing: ITracingService,
private readonly userRepo: UserRepository,
) {}
async generateMonthlyReports(): Promise<void> {
await this.tracing.withSpan('reports.generate_monthly', async () => {
const users = await this.tracing.withSpan('users.fetch_all', async () => {
return this.userRepo.findAll();
});
this.tracing.setAttribute('users.count', users.length);
let successCount = 0;
let errorCount = 0;
for (const user of users) {
await this.tracing.withSpan('report.generate_for_user', async () => {
this.tracing.setAttribute('user.id', user.id);
try {
await this.generateReport(user);
successCount++;
} catch (error) {
errorCount++;
this.tracing.recordException(error as Error);
throw error;
}
}).catch(() => {}); // Continue on individual failures
}
this.tracing.setAttribute('reports.success_count', successCount);
this.tracing.setAttribute('reports.error_count', errorCount);
this.tracing.addEvent('reports.completed', {
'reports.total': users.length,
});
});
}
private async generateReport(_user: User): Promise<void> {
// report generation logic
}
}Best Practices
1. Use withSpan() for automatic cleanup
// ✅ Good - Automatic span.end() and status
await this.tracing.withSpan('operation', async () => {
await doWork();
});
// ❌ Risky - Manual span.end()
const span = this.tracing.startSpan('operation');
await doWork();
span.end(); // Skipped if error thrown!2. Add meaningful attributes
// ✅ Good
span.setAttributes({
'user.id': user.id,
'user.role': user.role,
'operation.type': 'batch',
});
// ❌ Bad - Too generic
span.setAttribute('data', JSON.stringify(user));3. Record events for milestones
span.addEvent('validation.started');
await validate();
span.addEvent('validation.completed');4. Set status appropriately
try {
await operation();
span.setStatus('OK');
} catch (error) {
span.setStatus('ERROR');
span.recordException(error);
throw error;
}5. Keep span names concise
// ✅ Good
'user.get', 'order.create', 'payment.process'
// ❌ Bad
'getUserFromDatabaseWithCaching', 'processOrderAndSendEmail'Next Steps
- Plugin Tracing — Automatic plugin traces
- Visualization — Analyze traces
- Troubleshooting — Debug tracing issues