Skip to content

Spans

Create custom spans to trace application-specific operations.

TracingService

Inject ITracingService to create custom spans.

Basic Usage

typescript
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.found

API Reference

withSpan()

Create a span that wraps an async operation. Automatically ends the span and sets status.

typescript
withSpan<T>(
  name: string,
  fn: () => T | Promise<T>,
  options?: ISpanOptions,
): Promise<T>

Parameters:

ParameterTypeDescription
namestringSpan name
fn() => T | Promise<T>Function to trace
optionsISpanOptionsOptional span options

Returns: Promise resolving to function result

Example:

typescript
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()).

typescript
startSpan(name: string, options?: ISpanOptions): ISpan

Example:

typescript
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.

typescript
getCurrentSpan(): ISpan | undefined

Example:

typescript
@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()

typescript
span.setAttribute('user.id', '123');
span.setAttribute('user.role', 'admin');
span.setAttribute('cache.hit', false);
span.setAttribute('request.size', 1024);

setAttributes()

typescript
span.setAttributes({
  'user.id': '123',
  'user.role': 'admin',
  'user.email': 'user@example.com',
});

Semantic Conventions

Use standard attribute names when possible.

typescript
// ✅ 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:

CategoryAttributesExample
Databasedb.system, db.operation, db.statementdb.system: "redis"
HTTPhttp.method, http.url, http.status_codehttp.method: "GET"
RPCrpc.system, rpc.service, rpc.methodrpc.service: "UserService"
Messagingmessaging.system, messaging.operationmessaging.system: "redis-streams"

Span Events

Record point-in-time occurrences.

addEvent()

typescript
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: End

Span Status

Indicate success or failure.

Success

typescript
span.setStatus('OK');

Error

typescript
span.setStatus('ERROR');

Exception Recording

typescript
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).

typescript
await this.tracing.withSpan('process.data', async () => {
  // ...
}, {
  kind: 'INTERNAL',
});

CLIENT

Outgoing requests (HTTP, database, Redis). This is the default kind.

typescript
await this.tracing.withSpan('api.call', async () => {
  return axios.get('https://api.example.com/data');
}, {
  kind: 'CLIENT',
});

SERVER

Incoming requests.

typescript
// Automatically set by HTTP instrumentation
@Get('/users')
async getUsers() {
  // Span kind: SERVER
}

PRODUCER

Publishing messages.

typescript
await this.tracing.withSpan('message.publish', async () => {
  await this.streamProducer.publish('orders', data);
}, {
  kind: 'PRODUCER',
});

CONSUMER

Consuming messages.

typescript
@StreamConsumer({ stream: 'orders' })
async handleOrder(message: IStreamMessage) {
  // Span kind: CONSUMER
}

Nested Spans

Create parent-child relationships.

Automatic Nesting

typescript
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:

typescript
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

typescript
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

typescript
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

typescript
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

typescript
// ✅ 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

typescript
// ✅ 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

typescript
span.addEvent('validation.started');
await validate();
span.addEvent('validation.completed');

4. Set status appropriately

typescript
try {
  await operation();
  span.setStatus('OK');
} catch (error) {
  span.setStatus('ERROR');
  span.recordException(error);
  throw error;
}

5. Keep span names concise

typescript
// ✅ Good
'user.get', 'order.create', 'payment.process'

// ❌ Bad
'getUserFromDatabaseWithCaching', 'processOrderAndSendEmail'

Next Steps

Released under the MIT License.