Telemetry — Exporters
Overview
Exporters are the delivery layer: they receive telemetry data from the registry or tracing engine and forward it to a backend. UDIOT ships three exporters out of the box:
| Exporter | Data type | Best for |
|---|---|---|
OTLPMetricsExporter |
Metrics | Experimental HTTP JSON delivery; validate the selected receiver |
OTLPTraceExporter |
Traces | Experimental HTTP JSON delivery; validate the selected receiver |
ConsoleMetricsExporter |
Metrics | Development and debugging |
The built-in HTTP exporters catch network failures and log non-abort exceptions.
Timeout aborts are silent. They do not check response.ok, retry delivery or
report receiver rejection through their return value. Monitor ingestion and
implement stronger delivery behavior in your own exporter where required.
Despite their names, these implementations are not proven standards-compliant OTLP encoders: metrics serialize framework snapshot fields directly, attributes are plain objects and trace status uses the framework's string value. The examples below describe the current wire payload, not backend certification.
Key Concepts
- HTTP JSON delivery — defaults use port
4318and resource/scope envelopes; receiver compatibility must be checked with the exact emitted payload. - AbortController timeout — both OTLP exporters use a
5000 ms(configurable) timeout withAbortControllerto prevent hung requests from blocking the flush loop. - Metric flush vs span export — metrics are batched and flushed periodically. Traces are exported span-by-span immediately after each span finalizes.
OTLPMetricsExporter
Sends the full MetricSnapshot to the configured /v1/metrics HTTP endpoint after each flush cycle.
Constructor
new OTLPMetricsExporter(options?: OTLPMetricsExporterOptions)
OTLPMetricsExporterOptions
| Field | Type | Default | Description |
|---|---|---|---|
url |
string |
'http://localhost:4318/v1/metrics' |
OTLP metrics endpoint |
headers |
Record<string, string> |
{} |
Additional HTTP headers (e.g., auth tokens) |
timeoutMs |
number |
5000 |
Request timeout in milliseconds |
fetcher |
typeof fetch |
global fetch |
Custom fetch implementation for testing |
Wire format
The exporter wraps the snapshot in the OTLP resource metrics envelope:
{
"resourceMetrics": [{
"scopeMetrics": [{
"scope": { "name": "@tagna/udiot" },
"metrics": [
{
"name": "http.requests.total",
"type": "counter",
"sum": 1042,
"count": 1042,
"min": 1,
"max": 1,
"lastValue": 1,
"updatedAt": 1711094400000,
"attributes": { "method": "GET" }
}
]
}]
}]
}
Example: configuring a metrics receiver
import {
initializeTelemetry,
OTLPMetricsExporter,
} from '@tagna/udiot/telemetry';
initializeTelemetry({
metrics: {
exporters: [
new OTLPMetricsExporter({
url: 'https://otel-collector.internal/v1/metrics',
headers: {
'Authorization': `Bearer ${process.env.OTEL_TOKEN}`,
},
timeoutMs: 8000,
}),
],
flushIntervalMs: 30_000,
},
});
OTLPTraceExporter
Sends each completed TracingSpan to the configured /v1/traces HTTP endpoint immediately after the span finalizes.
Constructor
new OTLPTraceExporter(options?: OTLPTraceExporterOptions)
OTLPTraceExporterOptions
| Field | Type | Default | Description |
|---|---|---|---|
url |
string |
'http://localhost:4318/v1/traces' |
OTLP traces endpoint |
headers |
Record<string, string> |
{} |
Additional HTTP headers |
timeoutMs |
number |
5000 |
Request timeout in milliseconds |
fetcher |
typeof fetch |
global fetch |
Custom fetch implementation for testing |
Timestamp conversion
Span startTime and endTime are stored in milliseconds. The exporter converts them to nanoseconds (ms * 1_000_000) for the OTLP startTimeUnixNano / endTimeUnixNano fields, which is the unit expected by the OTLP specification.
Wire format
{
"resourceSpans": [{
"scopeSpans": [{
"scope": { "name": "@tagna/udiot" },
"spans": [{
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"spanId": "00f067aa0ba902b7",
"parentSpanId": "b9c7c989f97918e1",
"name": "processOrder",
"kind": "server",
"startTimeUnixNano": 1711094400000000000,
"endTimeUnixNano": 1711094400142000000,
"attributes": { "orderId": "ord_xyz" },
"status": { "code": "ok" }
}]
}]
}]
}
Example: configuring a trace receiver
import {
OTLPTraceExporter,
registerTraceExporter,
} from '@tagna/udiot/telemetry';
registerTraceExporter(
new OTLPTraceExporter({
url: 'http://jaeger:4318/v1/traces',
})
);
Configure and test the receiving service independently; a successful HTTP request alone does not prove the payload was accepted or retained.
ConsoleMetricsExporter
Writes metric snapshots as structured JSON log lines. Designed for local development and CI debugging where you want to see metric data without running a collector.
Constructor
new ConsoleMetricsExporter(options?: ConsoleMetricsExporterOptions)
ConsoleMetricsExporterOptions
| Field | Type | Default | Description |
|---|---|---|---|
sink |
(payload: ConsoleMetricsPayload) => void |
telemetry logger .info() |
Custom output function |
minMetrics |
number |
1 |
Skip export if snapshot has fewer than this many metrics |
ConsoleMetricsPayload
interface ConsoleMetricsPayload {
timestamp: string; // ISO 8601
metrics: MetricSnapshot;
}
Default output
When the default sink is used, the exporter calls getTelemetryLogger().info('metrics.snapshot', { metricsCount, metrics }), which produces:
{
"level": "info",
"message": "metrics.snapshot",
"timestamp": "2026-03-22T10:00:00.000Z",
"attributes": {
"metricsCount": 3,
"metrics": [...]
}
}
Example: custom sink for pretty-printing
import { ConsoleMetricsExporter } from '@tagna/udiot/telemetry';
const exporter = new ConsoleMetricsExporter({
sink: (payload) => {
for (const m of payload.metrics) {
console.log(`[${payload.timestamp}] ${m.name} (${m.type}): ${m.lastValue}`);
}
},
minMetrics: 2,
});
Example: initializing the SDK with all three exporters
import {
initializeTelemetry,
shutdownTelemetry,
OTLPMetricsExporter,
OTLPTraceExporter,
ConsoleMetricsExporter,
registerTraceExporter,
} from '@tagna/udiot/telemetry';
const isProduction = process.env.NODE_ENV === 'production';
const collectorBase = process.env.OTEL_COLLECTOR_URL ?? 'http://localhost:4318';
// Metrics exporters
const metricsExporters = [
new OTLPMetricsExporter({ url: `${collectorBase}/v1/metrics` }),
];
if (!isProduction) {
metricsExporters.push(new ConsoleMetricsExporter());
}
initializeTelemetry({
metrics: {
exporters: metricsExporters,
flushIntervalMs: 60_000,
},
});
// Trace exporter
registerTraceExporter(
new OTLPTraceExporter({ url: `${collectorBase}/v1/traces` })
);
// Graceful shutdown
process.on('SIGTERM', async () => {
await shutdownTelemetry();
process.exit(0);
});
Common Patterns
Authentication — pass the headers required by your verified receiver through
the headers option. Keep credentials in server-side secret configuration and
verify the receiver's current authentication requirements independently.
Custom fetch — pass a fetcher option to inject a custom fetch implementation. This is the standard way to test exporters without making real HTTP calls, or to add retry logic.
Disabling in tests — do not register any exporters in your test setup. Without exporters, flush() is a no-op and finalizeSpan skips export silently.
Pitfalls
- Both OTLP exporters skip export silently if the request is aborted by the timeout. Check your collector's ingestion rate if you notice missing data under high load — the
5000 msdefault may need to be raised. ConsoleMetricsExporterwithminMetrics: 0orminMetrics: 1(the default) will emit an entry even for a single metric. Set a higherminMetricsvalue if you want to suppress noisy early-startup exports.- The
OTLPTraceExporterexports one span per HTTP request. Under very high trace volume this can generate many small requests. Consider deploying an OpenTelemetry Collector in front of your backend to batch and compress spans.