@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
| Option | Type | Default | Description |
|---|---|---|---|
endpoint | string | '/metrics' | Scrape endpoint path. |
defaultMetrics | boolean | true | Register the built-in metric set. |
httpMetrics | boolean | true | Register the HTTP request-tracking middleware. |
customMetrics | NamedMetricConfig[] | [] | Metrics pre-registered declaratively. |
defaultBuckets | number[] | [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10] | Histogram bucket boundaries. |
defaultQuantiles | number[] | [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
| Export | Kind |
|---|---|
MetricsPlugin | function |
Counter | class |
Gauge | class |
Histogram | class |
MetricsService | class |
Summary | class |
ICounter | interface |
IGauge | interface |
IHistogram | interface |
IMetric | interface |
IMetricsService | interface |
ISummary | interface |
MetricConfig | interface |
MetricOptions | interface |
MetricsPluginOptions | interface |
NamedMetricConfig | interface |
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
...