Docs/telemetryv1.0.0-rc.1

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 sum and count on 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

  • gauge does not accumulate: every call replaces sum with the latest value. If you need a running total, use increment.
  • observe (histogram) accumulates min, max, sum, and count across 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.
From the canonical UDIOT public documentation.Try it in the playground ↗