@setu-ts/metrics-plugin

Prometheus metrics. Registers an IMetricsService under CAPABILITIES.METRICS ('metrics') and serves the scrape endpoint at /metrics.

Zero dependencies — the Prometheus text-format 0.0.4 renderer is implemented in this package.

Label values are escaped per text format 0.0.4 (backslash, double quote, line feed). Any other control character (carriage return, NUL, the rest of C0 and DEL) has no escape in that format and is encoded in label values and HELP text as U+FFFD followed by its two hex digits (a lone UTF-16 surrogate as U+FFFD, u and four hex digits; a literal U+FFFD as two of them), so an application-supplied value cannot split an exposition line and two distinct values never render as the same sample line.

Installation

import { MetricsPlugin } from '@setu-ts/metrics-plugin';

Usage

import { createApplication } from '@setu-ts/kernel';
import { RuntimePlugin } from '@setu-ts/runtime';
import { MetricsPlugin } from '@setu-ts/metrics-plugin';
import { CAPABILITIES, type IMetricsService } from '@setu-ts/common';

const app = createApplication({
  plugins: [RuntimePlugin(), MetricsPlugin({ endpoint: '/metrics', httpMetrics: true })],
});
await app.start({ port: 3000 });

const metrics = app.services.get<IMetricsService>(CAPABILITIES.METRICS);
const signups = metrics.counter('app_signups_total', { help: 'Completed signups' });
signups.inc(1);

Options

OptionTypeDefaultDescription
endpointstring'/metrics'Scrape endpoint path.
defaultMetricsbooleantrueRegister the built-in metric set.
httpMetricsbooleantrueRegister the HTTP request-tracking middleware.
customMetricsNamedMetricConfig[][]Metrics pre-registered declaratively.
defaultBucketsnumber[][0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]Histogram bucket boundaries.
defaultQuantilesnumber[][0.5, 0.9, 0.99]Summary quantiles.

Instruments

counter, gauge, histogram, and summary, all over a shared base.

HTTP collectors

The metrics middleware runs at priority 20 — outermost, so it observes all ingress and the final response status. The record path is wrapped in try/finally, so a request that throws still decrements the active-requests gauge rather than leaking it.

HTTP series use only method and status labels. The case-sensitive known methods are GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, and PATCH; every other method uses _OTHER, including on thrown-error paths. This bounds method-label cardinality.

Exports

ExportKind
MetricsPluginfunction
Counterclass
Gaugeclass
Histogramclass
MetricsServiceclass
Summaryclass
ICounterinterface
IGaugeinterface
IHistograminterface
IMetricinterface
IMetricsServiceinterface
ISummaryinterface
MetricConfiginterface
MetricOptionsinterface
MetricsPluginOptionsinterface
NamedMetricConfiginterface

Generated from the package barrel by pnpm run docs:exports; pnpm run check:docs fails when it drifts.

Full API

Every export and option is documented in PUBLIC_API.md.

API reference

Metrics (@setu-ts/metrics-plugin)

Built-in HTTP series carry method and status labels. The method label preserves GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, and PATCH, compared case-sensitively; all other methods use _OTHER on both success and error paths.

Provides Prometheus metrics.

Label values are escaped per text format 0.0.4 (backslash, double quote, line feed). Any other control character (carriage return, NUL, the rest of C0 and DEL) has no escape in that format and is replaced with U+FFFD in label values and HELP text, so an application-supplied value cannot split an exposition line.

Service Interface

Resolve the service via ctx.services.get<IMetricsService>('metrics'). Each factory method is get-or-create: the first call for a name constructs and registers the instrument; later calls return the same handle. Record methods are value-first (inc(value?, labels?), set(value, labels?), observe(value, labels?)), matching the committed IMetric.observe.

interface MetricOptions {
  readonly help?: string; // defaults to the metric name
  readonly labels?: readonly string[];
  readonly buckets?: readonly number[]; // histogram; falls back to defaultBuckets
  readonly quantiles?: readonly number[]; // summary; falls back to defaultQuantiles
  readonly maxSamples?: number; // summary sliding-window size (default 512)
}

interface IMetricsService {
  counter(name: string, options?: MetricOptions): ICounter;
  gauge(name: string, options?: MetricOptions): IGauge;
  histogram(name: string, options?: MetricOptions): IHistogram;
  summary(name: string, options?: MetricOptions): ISummary;
  get(name: string): IMetric | undefined;
}

interface ICounter extends IMetric {
  inc(value?: number, labels?: Readonly<Record<string, string>>): void;
}
interface IGauge extends IMetric {
  set(value: number, labels?: Readonly<Record<string, string>>): void;
  inc(value?: number, labels?: Readonly<Record<string, string>>): void;
  dec(value?: number, labels?: Readonly<Record<string, string>>): void;
}
interface IHistogram extends IMetric {
  observe(value: number, labels?: Readonly<Record<string, string>>): void;
  readonly buckets: readonly number[];
}
interface ISummary extends IMetric {
  observe(value: number, labels?: Readonly<Record<string, string>>): void;
  readonly quantiles: readonly number[];
}

The declarative MetricConfig (type and help required) remains the shape for ctx.metrics.register(name, config) and the plugin’s customMetrics option. NamedMetricConfig is that shape plus name, and is exported from @setu-ts/metrics-plugin — customMetrics is typed as an array of it, so without the export a caller could pass an inline literal but could not declare its own array in a variable. A pre-registered metric appears in the scrape output as its # HELP and # TYPE lines from startup, before anything records on it. The GET /metrics scrape endpoint responds with Content-Type: text/plain; version=0.0.4; charset=utf-8.

Registration

import { MetricsPlugin } from '@setu-ts/metrics-plugin';

app.register(
  MetricsPlugin({
    endpoint: '/metrics',
    defaultMetrics: true,
    httpMetrics: true,
    // Replaces the default ['/health', '/live', '/ready'] exclusion set; the
    // plugin's own endpoint is always excluded either way.
    excludePaths: ['/health', '/live', '/ready'],
    customMetrics: [
      { name: 'users_total', help: 'Total users', type: 'counter' },
      { name: 'active_connections', help: 'Active connections', type: 'gauge' },
    ],
  }),
);

Custom Metrics

app.router.post('/users', async (ctx) => {
  const metrics = ctx.services.get<IMetricsService>('metrics');
  const user = await createUser(ctx.request.body);

  metrics.counter('users_total').inc();
  metrics.gauge('active_connections').inc();

  return ctx.response.status(201).json(user);
});

// Histogram
app.router.get('/search', async (ctx) => {
  const metrics = ctx.services.get<IMetricsService>('metrics');
  const histogram = metrics.histogram('search_duration_seconds', {
    labels: ['query_type'],
    buckets: [0.1, 0.5, 1, 5],
  });

  const start = Date.now();
  const results = await search(ctx.request.query);
  histogram.observe((Date.now() - start) / 1000, { query_type: 'full-text' });

  return ctx.response.json(results);
});

Prometheus Endpoint

GET /metrics

# HELP users_total Total users
# TYPE users_total counter
users_total 1234

# HELP active_connections Active connections
# TYPE active_connections gauge
active_connections 42

# HELP http_request_duration_seconds HTTP request duration
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.1"} 100
http_request_duration_seconds_bucket{le="0.5"} 150
...

Agent card · TypeDoc API reference