Skip to content

Telemetry

Distributed tracing support built on OpenTelemetry. The contract package defines a TracerService abstraction; the OTEL driver wires it to the OpenTelemetry SDK; per-contract extension packages transparently instrument HTTP, cache, lock, events, queue, scheduler, storage, and logger without touching application code.

Contract (@modularityjs/telemetry)

Defines the abstract TracerService and TelemetryConfig:

typescript
abstract class TracerService {
  abstract startSpan(name: string, kind?: SpanKind): Span;
  abstract getActiveSpan(): Span | undefined;
  abstract withSpan<T>(
    name: string,
    kind: SpanKind,
    fn: (span: Span) => Promise<T> | T,
  ): Promise<T>;
  abstract injectContext(carrier: Record<string, string>): void;
  abstract extractContext(carrier: Record<string, string>): Context;
  abstract runInContext<T>(context: Context, fn: () => T): T;
}

TelemetryModule.forRoot() configures the service name and optional resource attributes:

typescript
TelemetryModule.forRoot({
  serviceName: 'my-api',
  serviceVersion: '1.2.0',
  resourceAttributes: { 'deployment.environment': 'production' },
});
FieldTypeDefault
serviceNamestring'modularityjs'
serviceVersionstring | undefined
resourceAttributesRecord<string, string>{}

Driver (@modularityjs/telemetry-otel)

Implements TracerService using the OpenTelemetry Node.js SDK. Manages the SDK lifecycle — starts on onInit, flushes and shuts down on onShutdown.

typescript
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { TelemetryModule } from '@modularityjs/telemetry';
import { TelemetryOtelModule } from '@modularityjs/telemetry-otel';

const modules = [
  TelemetryModule.forRoot({ serviceName: 'my-api' }),
  TelemetryOtelModule.forRoot({
    traceExporter: new OTLPTraceExporter({
      url: 'http://collector:4318/v1/traces',
    }),
  }),
];

For local development, use the console exporter to print spans to stdout:

typescript
import { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-node';

TelemetryOtelModule.forRoot({ traceExporter: new ConsoleSpanExporter() });

Config

FieldTypeDescription
traceExporterSpanExporterWhere to send spans (OTLP, console, Jaeger, etc.)
spanProcessorsSpanProcessor[]Custom span processors passed straight to NodeSDK (e.g. batch + simple processors). Default: unset
metricExporterMetricReaderWired into NodeSDK.metricReader. Set this to enable OTel metrics export. Default: unset
instrumentationsInstrumentation[]Additional OTel auto-instrumentations (default: [])

ESM and auto-instrumentation

Auto-instrumentation of Node.js built-ins (http, net, dns) requires the OTel loader to be registered before any imports. For ESM apps, start the process with:

bash
node --import @opentelemetry/auto-instrumentations-node/register dist/server.js

Telemetry Extensions

Telemetry extensions transparently instrument existing services via @Plugin AOP. Add only the ones you need — each is independent.

HTTP (@modularityjs/http-telemetry)

Enriches the active span with HTTP route and controller metadata on every request. Does not create spans itself — relies on OTel's HTTP auto-instrumentation for that.

typescript
import { HttpTelemetryModule } from '@modularityjs/http-telemetry';

const modules = [
  // ... TelemetryModule, TelemetryOtelModule, HttpModule, HttpFastifyModule ...
  HttpTelemetryModule,
];

Added span attributes:

AttributeValue
http.routeRequest URL (request.url)
http.methodHTTP method (GET, POST, …)
http.user_agentuser-agent request header (when present)

Cache (@modularityjs/cache-telemetry)

Wraps CacheService via the plugin system. Each cache operation becomes a CLIENT span.

typescript
import { CacheTelemetryModule } from '@modularityjs/cache-telemetry';

const modules = [
  // ... TelemetryModule, TelemetryOtelModule, CacheModule, PluginsModule ...
  CacheTelemetryModule,
];
Span nameAttributes
cache getcache.key, cache.hit
cache setcache.key, cache.ttl_ms
cache deletecache.key
cache hascache.key
cache invalidateTagcache.tag
cache invalidateTagscache.tag_count

Lock (@modularityjs/lock-telemetry)

Wraps LockService via the plugin system. Each acquire/release becomes a CLIENT span.

typescript
import { LockTelemetryModule } from '@modularityjs/lock-telemetry';

const modules = [
  // ... TelemetryModule, TelemetryOtelModule, LockModule, PluginsModule ...
  LockTelemetryModule,
];
Span nameAttributes
lock acquirelock.key, lock.ttl_ms, lock.acquired
lock releaselock.key

Events (@modularityjs/events-telemetry)

Wraps EventBus.dispatch via the plugin system. Each dispatch becomes an INTERNAL span named after the event class.

typescript
import { EventsTelemetryModule } from '@modularityjs/events-telemetry';

const modules = [
  // ... TelemetryModule, TelemetryOtelModule, EventsModule, PluginsModule ...
  EventsTelemetryModule,
];
Span nameAttributes
event dispatch {ClassName}event.class, event.error_count

The span status is set to ERROR if any handler returned errors.

Queue (@modularityjs/queue-telemetry)

Wraps QueueService.publish and publishBatch via the plugin system. Injects W3C trace context (traceparent, tracestate) into message headers so the trace continues across the publish/consume boundary.

typescript
import { QueueTelemetryModule } from '@modularityjs/queue-telemetry';

const modules = [
  // ... TelemetryModule, TelemetryOtelModule, QueueModule, PluginsModule ...
  QueueTelemetryModule,
];
Span nameKindAttributes
queue publish {topic}PRODUCERmessaging.system, messaging.destination
queue publishBatchPRODUCERmessaging.system, messaging.batch_size

On the consumer side, extract the context from message headers to continue the trace:

typescript
import { Inject, Injectable } from '@modularityjs/di';
import { Consume } from '@modularityjs/queue';
import { TracerService } from '@modularityjs/telemetry';

@Injectable()
class OrderConsumer {
  constructor(@Inject(TracerService) private readonly tracer: TracerService) {}

  @Consume('orders')
  async handle(message: QueueMessage): Promise<void> {
    const context = this.tracer.extractContext(message.headers ?? {});
    this.tracer.runInContext(context, async () => {
      // spans created here are children of the publisher's span
      await this.processOrder(message.payload);
    });
  }
}

Scheduler (@modularityjs/scheduler-telemetry)

Wraps SchedulerService.runJob() via the plugin system. Each manual job execution becomes an INTERNAL span named after the job.

typescript
import { SchedulerTelemetryModule } from '@modularityjs/scheduler-telemetry';

const modules = [
  // ... TelemetryModule, TelemetryOtelModule, SchedulerModule, PluginsModule ...
  SchedulerTelemetryModule,
];
Span nameKindAttributes
scheduler run {name}INTERNALscheduler.job

The span status is set to ERROR and the exception recorded if the job throws.

Storage (@modularityjs/storage-telemetry)

Wraps all StorageService methods via the plugin system. Each operation becomes a CLIENT span.

typescript
import { StorageTelemetryModule } from '@modularityjs/storage-telemetry';

const modules = [
  // ... TelemetryModule, TelemetryOtelModule, StorageModule, PluginsModule ...
  StorageTelemetryModule,
];
Span nameAttributes
storage readstorage.key, storage.size
storage read_streamstorage.key
storage writestorage.key, storage.content_type*
storage write_streamstorage.key, storage.content_type*
storage deletestorage.key
storage existsstorage.key, storage.found
storage liststorage.prefix*, storage.entry_count

* Only set when provided.

Rate Limit (@modularityjs/rate-limit-telemetry)

Wraps RateLimiterService via the plugin system. Each consume, get, and reset becomes a CLIENT span.

typescript
import { RateLimitTelemetryModule } from '@modularityjs/rate-limit-telemetry';

const modules = [
  // ... TelemetryModule, TelemetryOtelModule, RateLimitModule, PluginsModule ...
  RateLimitTelemetryModule,
];
Span nameAttributes
rate_limit consumerate_limit.key, rate_limit.points, rate_limit.allowed, rate_limit.remaining
rate_limit getrate_limit.key, rate_limit.allowed, rate_limit.remaining
rate_limit resetrate_limit.key

Session (@modularityjs/session-telemetry)

Wraps SessionService via the plugin system. Each session operation becomes a CLIENT span.

typescript
import { SessionTelemetryModule } from '@modularityjs/session-telemetry';

const modules = [
  // ... TelemetryModule, TelemetryOtelModule, SessionModule, PluginsModule ...
  SessionTelemetryModule,
];
Span nameAttributes
session getsession.id, session.found
session setsession.id
session destroysession.id
session regeneratesession.id, session.new_id

Mail (@modularityjs/mail-telemetry)

Wraps MailService.send via the plugin system. Each send becomes a CLIENT span.

typescript
import { MailTelemetryModule } from '@modularityjs/mail-telemetry';

const modules = [
  // ... TelemetryModule, TelemetryOtelModule, MailModule, PluginsModule ...
  MailTelemetryModule,
];
Span nameAttributes
mail sendmail.recipients, mail.has_attachments, mail.accepted, mail.rejected

SMS (@modularityjs/sms-telemetry)

Wraps SmsService.send via the plugin system. Each send becomes a CLIENT span.

typescript
import { SmsTelemetryModule } from '@modularityjs/sms-telemetry';

const modules = [
  // ... TelemetryModule, TelemetryOtelModule, SmsModule, PluginsModule ...
  SmsTelemetryModule,
];
Span nameAttributes
sms sendsms.recipients, sms.body_length, sms.accepted, sms.rejected

Notification (@modularityjs/notification-telemetry)

Wraps NotificationService.send via the plugin system. Each dispatch becomes a PRODUCER span.

typescript
import { NotificationTelemetryModule } from '@modularityjs/notification-telemetry';

const modules = [
  // ... TelemetryModule, TelemetryOtelModule, NotificationModule, PluginsModule ...
  NotificationTelemetryModule,
];
Span nameAttributes
notification sendnotification.channels, notification.channel_names, notification.recipients, notification.succeeded, notification.failed

Webhook (@modularityjs/webhook-telemetry)

Wraps WebhookService.dispatch via the plugin system. Each dispatch becomes a PRODUCER span.

typescript
import { WebhookTelemetryModule } from '@modularityjs/webhook-telemetry';

const modules = [
  // ... TelemetryModule, TelemetryOtelModule, WebhookModule, PluginsModule ...
  WebhookTelemetryModule,
];
Span nameAttributes
webhook dispatchwebhook.event, webhook.subscriptions, webhook.succeeded, webhook.failed

Logger (@modularityjs/logger-telemetry)

Adds an OtelLogTransport to the LogTransportPool. Each log entry is attached to the active span as an OTel event (span.addEvent(message, { 'log.level', 'log.channel', ... })); entries logged outside any span are dropped. No-op when there is no active span — logs that need standalone export should keep file/console transports alongside this extension.

typescript
import { LoggerTelemetryModule } from '@modularityjs/logger-telemetry';

const modules = [
  // ... TelemetryModule, TelemetryOtelModule, LoggerModule ...
  LoggerTelemetryModule,
];

No configuration required. The transport captures all channels at debug level and above.

Full Setup Example

typescript
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { CacheTelemetryModule } from '@modularityjs/cache-telemetry';
import { EventsTelemetryModule } from '@modularityjs/events-telemetry';
import { HttpTelemetryModule } from '@modularityjs/http-telemetry';
import { LockTelemetryModule } from '@modularityjs/lock-telemetry';
import { LoggerTelemetryModule } from '@modularityjs/logger-telemetry';
import { MailTelemetryModule } from '@modularityjs/mail-telemetry';
import { NotificationTelemetryModule } from '@modularityjs/notification-telemetry';
import { PluginsModule } from '@modularityjs/plugins';
import { QueueTelemetryModule } from '@modularityjs/queue-telemetry';
import { RateLimitTelemetryModule } from '@modularityjs/rate-limit-telemetry';
import { SchedulerTelemetryModule } from '@modularityjs/scheduler-telemetry';
import { SessionTelemetryModule } from '@modularityjs/session-telemetry';
import { SmsTelemetryModule } from '@modularityjs/sms-telemetry';
import { StorageTelemetryModule } from '@modularityjs/storage-telemetry';
import { TelemetryModule } from '@modularityjs/telemetry';
import { TelemetryOtelModule } from '@modularityjs/telemetry-otel';
import { WebhookTelemetryModule } from '@modularityjs/webhook-telemetry';

const modules = [
  // contract — configure service identity
  TelemetryModule.forRoot({ serviceName: 'my-api', serviceVersion: '1.0.0' }),

  // driver — connects to your OTel collector
  TelemetryOtelModule.forRoot({
    traceExporter: new OTLPTraceExporter({
      url: 'http://collector:4318/v1/traces',
    }),
  }),

  // plugin system — required by most telemetry extensions
  PluginsModule,

  // extensions — add only what you need
  HttpTelemetryModule,
  CacheTelemetryModule,
  LockTelemetryModule,
  EventsTelemetryModule,
  MailTelemetryModule,
  NotificationTelemetryModule,
  QueueTelemetryModule,
  RateLimitTelemetryModule,
  SchedulerTelemetryModule,
  SessionTelemetryModule,
  SmsTelemetryModule,
  StorageTelemetryModule,
  WebhookTelemetryModule,
  LoggerTelemetryModule,
];

Using TracerService Directly

For custom spans in application code:

typescript
import { Inject, Injectable } from '@modularityjs/di';
import { SpanKind, TracerService } from '@modularityjs/telemetry';

@Injectable()
class PaymentService {
  constructor(@Inject(TracerService) private readonly tracer: TracerService) {}

  async charge(amount: number): Promise<void> {
    return this.tracer.withSpan(
      'payment.charge',
      SpanKind.CLIENT,
      async (span) => {
        span.setAttribute('payment.amount', amount);
        await this.gateway.charge(amount);
      },
    );
  }
}