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:
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:
TelemetryModule.forRoot({
serviceName: 'my-api',
serviceVersion: '1.2.0',
resourceAttributes: { 'deployment.environment': 'production' },
});| Field | Type | Default |
|---|---|---|
serviceName | string | 'modularityjs' |
serviceVersion | string | undefined | — |
resourceAttributes | Record<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.
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:
import { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-node';
TelemetryOtelModule.forRoot({ traceExporter: new ConsoleSpanExporter() });Config
| Field | Type | Description |
|---|---|---|
traceExporter | SpanExporter | Where to send spans (OTLP, console, Jaeger, etc.) |
spanProcessors | SpanProcessor[] | Custom span processors passed straight to NodeSDK (e.g. batch + simple processors). Default: unset |
metricExporter | MetricReader | Wired into NodeSDK.metricReader. Set this to enable OTel metrics export. Default: unset |
instrumentations | Instrumentation[] | 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:
node --import @opentelemetry/auto-instrumentations-node/register dist/server.jsTelemetry 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.
import { HttpTelemetryModule } from '@modularityjs/http-telemetry';
const modules = [
// ... TelemetryModule, TelemetryOtelModule, HttpModule, HttpFastifyModule ...
HttpTelemetryModule,
];Added span attributes:
| Attribute | Value |
|---|---|
http.route | Request URL (request.url) |
http.method | HTTP method (GET, POST, …) |
http.user_agent | user-agent request header (when present) |
Cache (@modularityjs/cache-telemetry)
Wraps CacheService via the plugin system. Each cache operation becomes a CLIENT span.
import { CacheTelemetryModule } from '@modularityjs/cache-telemetry';
const modules = [
// ... TelemetryModule, TelemetryOtelModule, CacheModule, PluginsModule ...
CacheTelemetryModule,
];| Span name | Attributes |
|---|---|
cache get | cache.key, cache.hit |
cache set | cache.key, cache.ttl_ms |
cache delete | cache.key |
cache has | cache.key |
cache invalidateTag | cache.tag |
cache invalidateTags | cache.tag_count |
Lock (@modularityjs/lock-telemetry)
Wraps LockService via the plugin system. Each acquire/release becomes a CLIENT span.
import { LockTelemetryModule } from '@modularityjs/lock-telemetry';
const modules = [
// ... TelemetryModule, TelemetryOtelModule, LockModule, PluginsModule ...
LockTelemetryModule,
];| Span name | Attributes |
|---|---|
lock acquire | lock.key, lock.ttl_ms, lock.acquired |
lock release | lock.key |
Events (@modularityjs/events-telemetry)
Wraps EventBus.dispatch via the plugin system. Each dispatch becomes an INTERNAL span named after the event class.
import { EventsTelemetryModule } from '@modularityjs/events-telemetry';
const modules = [
// ... TelemetryModule, TelemetryOtelModule, EventsModule, PluginsModule ...
EventsTelemetryModule,
];| Span name | Attributes |
|---|---|
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.
import { QueueTelemetryModule } from '@modularityjs/queue-telemetry';
const modules = [
// ... TelemetryModule, TelemetryOtelModule, QueueModule, PluginsModule ...
QueueTelemetryModule,
];| Span name | Kind | Attributes |
|---|---|---|
queue publish {topic} | PRODUCER | messaging.system, messaging.destination |
queue publishBatch | PRODUCER | messaging.system, messaging.batch_size |
On the consumer side, extract the context from message headers to continue the trace:
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.
import { SchedulerTelemetryModule } from '@modularityjs/scheduler-telemetry';
const modules = [
// ... TelemetryModule, TelemetryOtelModule, SchedulerModule, PluginsModule ...
SchedulerTelemetryModule,
];| Span name | Kind | Attributes |
|---|---|---|
scheduler run {name} | INTERNAL | scheduler.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.
import { StorageTelemetryModule } from '@modularityjs/storage-telemetry';
const modules = [
// ... TelemetryModule, TelemetryOtelModule, StorageModule, PluginsModule ...
StorageTelemetryModule,
];| Span name | Attributes |
|---|---|
storage read | storage.key, storage.size |
storage read_stream | storage.key |
storage write | storage.key, storage.content_type* |
storage write_stream | storage.key, storage.content_type* |
storage delete | storage.key |
storage exists | storage.key, storage.found |
storage list | storage.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.
import { RateLimitTelemetryModule } from '@modularityjs/rate-limit-telemetry';
const modules = [
// ... TelemetryModule, TelemetryOtelModule, RateLimitModule, PluginsModule ...
RateLimitTelemetryModule,
];| Span name | Attributes |
|---|---|
rate_limit consume | rate_limit.key, rate_limit.points, rate_limit.allowed, rate_limit.remaining |
rate_limit get | rate_limit.key, rate_limit.allowed, rate_limit.remaining |
rate_limit reset | rate_limit.key |
Session (@modularityjs/session-telemetry)
Wraps SessionService via the plugin system. Each session operation becomes a CLIENT span.
import { SessionTelemetryModule } from '@modularityjs/session-telemetry';
const modules = [
// ... TelemetryModule, TelemetryOtelModule, SessionModule, PluginsModule ...
SessionTelemetryModule,
];| Span name | Attributes |
|---|---|
session get | session.id, session.found |
session set | session.id |
session destroy | session.id |
session regenerate | session.id, session.new_id |
Mail (@modularityjs/mail-telemetry)
Wraps MailService.send via the plugin system. Each send becomes a CLIENT span.
import { MailTelemetryModule } from '@modularityjs/mail-telemetry';
const modules = [
// ... TelemetryModule, TelemetryOtelModule, MailModule, PluginsModule ...
MailTelemetryModule,
];| Span name | Attributes |
|---|---|
mail send | mail.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.
import { SmsTelemetryModule } from '@modularityjs/sms-telemetry';
const modules = [
// ... TelemetryModule, TelemetryOtelModule, SmsModule, PluginsModule ...
SmsTelemetryModule,
];| Span name | Attributes |
|---|---|
sms send | sms.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.
import { NotificationTelemetryModule } from '@modularityjs/notification-telemetry';
const modules = [
// ... TelemetryModule, TelemetryOtelModule, NotificationModule, PluginsModule ...
NotificationTelemetryModule,
];| Span name | Attributes |
|---|---|
notification send | notification.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.
import { WebhookTelemetryModule } from '@modularityjs/webhook-telemetry';
const modules = [
// ... TelemetryModule, TelemetryOtelModule, WebhookModule, PluginsModule ...
WebhookTelemetryModule,
];| Span name | Attributes |
|---|---|
webhook dispatch | webhook.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.
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
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:
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);
},
);
}
}