Telemetry — Metrics
Overview
The metrics subsystem provides a lightweight, in-process aggregation registry for three metric types: counters, gauges, and histograms. All data is held in memory as MetricAggregate objects, keyed by (name, type, attributes). Exporters receive a snapshot of the aggregates whenever the flush loop fires or flush() is called manually.
Key Concepts
- MetricsRegistry — the central singleton. All recording and exporting goes through it.
- Metric key — each aggregate is uniquely identified by
name + type + serialized attributes. The same metric name recorded with different attribute sets creates separate aggregates. - Gauge semantics — a gauge replaces
sumandcounton every write (always reflects the latest value). Counters and histograms accumulate. - Flush — all registered exporters receive a full snapshot simultaneously via
Promise.all.
API Reference
getMetricsRegistry(): MetricsRegistry
Returns the global MetricsRegistry singleton.
import { getMetricsRegistry } from '@tagna/udiot/telemetry';
const registry = getMetricsRegistry();
setMetricsRegistry(registry: MetricsRegistry): MetricsRegistry
Replaces the global registry and returns the previous one. Useful in tests.
resetMetricsRegistry(): void
Creates a fresh global registry, discarding all accumulated data.
Recording methods
registry.increment(name, value?, attributes?): void
Records a counter increment. value defaults to 1.
| Parameter | Type | Default |
|---|---|---|
name |
string |
required |
value |
number |
1 |
attributes |
MetricAttributes |
undefined |
registry.gauge(name, value, attributes?): void
Records a gauge (point-in-time value). Each call replaces the stored value.
registry.observe(name, value, attributes?): void
Records a histogram observation. Accumulates sum, count, min, and max.
MetricAttributes
type MetricAttributes = Record<string, string | number | boolean>;
MetricType
type MetricType = 'counter' | 'gauge' | 'histogram';
MetricAggregate
The shape of every entry in the registry:
| Field | Type | Description |
|---|---|---|
name |
string |
Metric name |
type |
MetricType |
'counter', 'gauge', or 'histogram' |
attributes |
MetricAttributes | undefined |
Dimension labels |
sum |
number |
Running sum (gauge: current value) |
count |
number |
Number of recordings (gauge: always 1) |
min |
number |
Minimum observed value |
max |
number |
Maximum observed value |
lastValue |
number |
Most recently recorded value |
updatedAt |
number |
Unix timestamp (ms) of last write |
MetricSnapshot
type MetricSnapshot = MetricAggregate[];
snapshot() returns a shallow-copy array — safe to iterate or serialize without affecting the registry.
Exporter management
registry.registerExporter(exporter: MetricsExporter): void
Adds an exporter. All registered exporters receive the same snapshot on every flush.
registry.unregisterExporter(exporter: MetricsExporter): void
Removes a previously registered exporter.
registry.snapshot(): MetricSnapshot
Returns a point-in-time copy of all aggregates. Does not trigger export.
registry.flush(): Promise<void>
Calls export(snapshot) on all registered exporters concurrently. The SDK calls this automatically when flushIntervalMs is set, and once more during shutdownTelemetry().
MetricsExporter interface
interface MetricsExporter {
export(snapshot: MetricSnapshot): Promise<void> | void;
}
Implement this interface to forward metrics to any backend.
Basic Usage
Tracking HTTP request metrics
import { getMetricsRegistry } from '@tagna/udiot/telemetry';
const registry = getMetricsRegistry();
async function handleRequest(method: string, route: string) {
const start = Date.now();
// Count active connections
registry.gauge('http.connections.active', getCurrentConnectionCount());
// Count total requests
registry.increment('http.requests.total', 1, { method, route });
try {
const response = await processRequest();
registry.increment('http.requests.success', 1, { method, route });
return response;
} catch (error) {
registry.increment('http.requests.error', 1, { method, route });
throw error;
} finally {
// Histogram of request durations
registry.observe('http.request.duration_ms', Date.now() - start, { method, route });
}
}
Custom Prometheus exporter
import { MetricsExporter, MetricSnapshot } from '@tagna/udiot/telemetry';
class PrometheusExporter implements MetricsExporter {
export(snapshot: MetricSnapshot): void {
const lines: string[] = [];
for (const metric of snapshot) {
const labelStr = metric.attributes
? '{' +
Object.entries(metric.attributes)
.map(([k, v]) => `${k}="${v}"`)
.join(',') +
'}'
: '';
if (metric.type === 'counter') {
lines.push(`# TYPE ${metric.name} counter`);
lines.push(`${metric.name}${labelStr} ${metric.sum}`);
} else if (metric.type === 'gauge') {
lines.push(`# TYPE ${metric.name} gauge`);
lines.push(`${metric.name}${labelStr} ${metric.lastValue}`);
} else {
// histogram — emit sum and count for Prometheus compatibility
lines.push(`# TYPE ${metric.name} summary`);
lines.push(`${metric.name}_sum${labelStr} ${metric.sum}`);
lines.push(`${metric.name}_count${labelStr} ${metric.count}`);
}
}
// Push to Prometheus pushgateway, write to file, etc.
console.log(lines.join('\n'));
}
}
// Register
import { getMetricsRegistry } from '@tagna/udiot/telemetry';
getMetricsRegistry().registerExporter(new PrometheusExporter());
Common Patterns
Attribute cardinality — keep the number of unique attribute value combinations low. Each unique combination creates a separate aggregate in memory. Avoid putting high-cardinality values (user IDs, UUIDs) in attributes.
Gauge for resource levels — use gauge for values that represent a current state: queue depth, connection pool size, memory usage. Use increment for things that accumulate: request counts, error counts.
Testing — use registry.reset() in afterEach to clear aggregates, or call resetMetricsRegistry() to replace the global instance entirely.
Pitfalls
gaugedoes not accumulate: every call replacessumwith the latest value. If you need a running total, useincrement.observe(histogram) accumulatesmin,max,sum, andcountacross all observations for the lifetime of the registry. There is no bucketing — for percentile calculations you need a custom exporter or a backend like Prometheus.- Calling
flush()with no exporters registered is a no-op and does not throw.