Skip to content

Tracing

Connectum provides distributed tracing through OpenTelemetry interceptors for both server-side and client-side RPC calls, plus deep tracing utilities for your business logic.

Server Interceptor

createOtelInterceptor() automatically creates spans for all incoming RPC calls with OpenTelemetry semantic conventions:

typescript
import { createOtelInterceptor } from '@connectum/otel';

const interceptor = createOtelInterceptor({
  filter: ({ service }) => !service.includes('grpc.health'),
  serverPort: 5000,
  recordMessages: false,
  trustRemote: false,
});

Options

OptionTypeDefaultDescription
filterOtelFilter--Skip specific requests from tracing
serverAddressstringos.hostname()Override server address attribute
serverPortnumber--Add server port attribute
recordMessagesbooleanfalseInclude request/response bodies in spans
trustRemotebooleanfalseUse remote context as parent span
withoutTracingbooleanfalseDisable tracing (metrics only)
withoutMetricsbooleanfalseDisable metrics (tracing only)
attributeFilterOtelAttributeFilter--Exclude specific span attributes

Client Interceptor

For outgoing RPC calls, use createOtelClientInterceptor() to propagate trace context:

typescript
import { createConnectTransport } from '@connectrpc/connect-node';
import { createClient } from '@connectrpc/connect';
import { createOtelClientInterceptor } from '@connectum/otel';
import { UserService } from '#gen/user_pb.js';

const transport = createConnectTransport({
  baseUrl: 'http://user-service:5001',
  interceptors: [
    createOtelClientInterceptor({
      serverAddress: 'user-service',   // Required
      serverPort: 5001,
    }),
  ],
});

const client = createClient(UserService, transport);

The client interceptor:

  • Injects trace context into outgoing requests via propagation.inject()
  • Creates SpanKind.CLIENT spans
  • Records rpc.client.* metrics

Deep Tracing with traced()

Wrap individual functions to create spans for your business logic:

typescript
import { traced } from '@connectum/otel';

const findUser = traced(async (id: string) => {
  return await db.users.findById(id);
}, {
  name: 'UserRepository.findUser',
  recordArgs: true,         // Record function arguments as span attributes
});

// Each call creates a span: "UserRepository.findUser"
const user = await findUser('123');

Deep Tracing with traceAll()

Wrap all methods of an object via Proxy:

typescript
import { traceAll } from '@connectum/otel';

class OrderRepository {
  async findById(id: string) {
    return await db.orders.findById(id);
  }

  async create(data: OrderData) {
    return await db.orders.create(data);
  }

  async updateStatus(id: string, status: string) {
    return await db.orders.update(id, { status });
  }
}

// Auto-instrument all methods (does NOT mutate the original)
const repository = traceAll(new OrderRepository(), {
  prefix: 'OrderRepository',
  exclude: ['internalHelper'],
  recordArgs: true,
});

// Calls automatically create spans:
// "OrderRepository.findById", "OrderRepository.create", etc.
await repository.findById('order-123');

Performance

traceAll() uses ES6 Proxy and creates method wrappers lazily on first access. It prevents double-wrapping automatically.

Distributed Tracing Across Services

A complete example showing trace context propagation between two services:

typescript
// ---- Service A (Gateway) ----
import { createServer } from '@connectum/core';
import { createConnectTransport } from '@connectrpc/connect-node';
import { createClient } from '@connectrpc/connect';
import { createOtelInterceptor, createOtelClientInterceptor } from '@connectum/otel';
import { UserService } from '#gen/user_pb.js';
import gatewayRoutes from '#gen/routes.js';

// Server: trace incoming requests
const server = createServer({
  services: [gatewayRoutes],
  port: 5000,
  interceptors: [
    createOtelInterceptor({
      serverPort: 5000,
      filter: ({ service }) => !service.includes('grpc.health'),
    }),
  ],
});

// Client: propagate trace context to Service B
const transport = createConnectTransport({
  baseUrl: 'http://user-service:5001',
  interceptors: [
    createOtelClientInterceptor({
      serverAddress: 'user-service',
      serverPort: 5001,
    }),
  ],
});

const userClient = createClient(UserService, transport);

// Trace context flows automatically:
// Service A (server span) -> Service A (client span) -> Service B (server span)