@absolutejs/metrics
v0.3.2
Published
Prometheus / OpenMetrics exposure for the AbsoluteJS substrate. Standardizes every package's metrics() shape into MetricSample[] + a Prometheus text-format renderer + an Elysia metricsPlugin. Per-source collectors as subpaths: runtime, router, egress, que
Maintainers
Readme
@absolutejs/metrics
Prometheus / OpenMetrics exposure for the AbsoluteJS substrate. Every
substrate package already exposes a typed metrics() snapshot — this
package converts those snapshots into the scrape format Prometheus,
VictoriaMetrics, Grafana Agent, OTLP collectors, etc. all understand.
Why
The substrate ships instrumentation but no exposure path. Operators
wire one metricsPlugin() and every metrics() shape across the
substrate becomes a scrape target — no hand-rolled /metrics per
service.
Install
bun add @absolutejs/metricselysia is an optional peer dep (only needed for metricsPlugin).
Usage
import { Elysia } from 'elysia';
import { createMetricsRegistry, metricsPlugin } from '@absolutejs/metrics';
import { runtimeCollector } from '@absolutejs/metrics/runtime';
import { routerCollector } from '@absolutejs/metrics/router';
import { egressCollector } from '@absolutejs/metrics/egress';
import {
queueCollector,
wakeSchedulerCollector
} from '@absolutejs/metrics/queue';
import { syncCollector } from '@absolutejs/metrics/sync';
import { secretsCollector } from '@absolutejs/metrics/secrets';
import { auditCollector } from '@absolutejs/metrics/audit';
import { dispatchCollector } from '@absolutejs/metrics/dispatch';
import { errorsCollector } from '@absolutejs/metrics/errors';
import { logsCollector } from '@absolutejs/metrics/logs';
const registry = createMetricsRegistry();
registry.register(
'runtime',
runtimeCollector(() => runtime.metrics())
);
registry.register(
'router',
routerCollector(() => router.metrics())
);
registry.register(
'egress',
egressCollector(() => egressGuard.metrics())
);
registry.register(
'queue',
queueCollector(() => worker.metrics())
);
registry.register(
'billing-wakes',
wakeSchedulerCollector(() => billingScheduler.metrics(), {
labels: { scheduler: 'billing' }
})
);
registry.register(
'sync',
syncCollector(() => engine.metrics())
);
registry.register(
'secrets',
secretsCollector(() => broker.metrics())
);
registry.register(
'audit',
auditCollector(() => audit.metrics())
);
registry.register(
'dispatch',
dispatchCollector(() => dispatcher.metrics())
);
registry.register(
'errors',
errorsCollector(() => tracker.metrics())
);
registry.register(
'logs',
logsCollector(() => logger.metrics())
);
const app = new Elysia().use(await metricsPlugin({ registry }));
// GET /metrics → Prometheus textOn a listener reachable outside a trusted scrape network, authorize before any collector runs:
const app = new Elysia().use(
await metricsPlugin({
registry,
authorize: (request) =>
request.headers.get('authorization') === `Bearer ${scrapeToken}`
})
);Output looks like:
# HELP abs_runtime_active Tenants currently active in the runtime
# TYPE abs_runtime_active gauge
abs_runtime_active 3
# HELP abs_queue_completed_total Successful job completions
# TYPE abs_queue_completed_total counter
abs_queue_completed_total 1057
# HELP abs_sync_subscriptions Active subscriptions across collections
# TYPE abs_sync_subscriptions gauge
abs_sync_subscriptions 142
…Collectors
Each substrate package gets its own subpath import:
| Subpath | Source |
| -------------------------------- | ---------------------------- |
| @absolutejs/metrics/runtime | @absolutejs/runtime |
| @absolutejs/metrics/router | @absolutejs/router |
| @absolutejs/metrics/egress | runtime egress guard |
| @absolutejs/metrics/queue | @absolutejs/queue |
| @absolutejs/metrics/sync | @absolutejs/sync engine |
| @absolutejs/metrics/secrets | @absolutejs/secrets broker |
| @absolutejs/metrics/rate-limit | @absolutejs/rate-limit |
| @absolutejs/metrics/audit | @absolutejs/audit |
| @absolutejs/metrics/dispatch | @absolutejs/dispatch |
| @absolutejs/metrics/errors | @absolutejs/errors tracker |
| @absolutejs/metrics/logs | @absolutejs/logs logger |
The substrate packages aren't hard deps. Each collector takes a
() => <metrics shape> function — pass () => instance.metrics()
and TypeScript's structural typing handles the rest.
@absolutejs/metrics/handoff exposes handoffCollector(). It exports only
the closed handoff source/outcome vocabularies as labels; correlation ids,
services, operations, messages, and references are intentionally excluded to
prevent unbounded metric cardinality.
Custom metrics
import { counter, gauge } from '@absolutejs/metrics';
registry.register('app', () => [
counter('myapp_requests_total', requestCount, {
help: 'Total HTTP requests',
labels: { route: '/api/users' }
}),
gauge('myapp_workers', activeWorkers, {
help: 'Currently running workers'
})
]);Naming
Convention: abs_<source>_<metric> for substrate metrics
(abs_runtime_active, abs_queue_completed_total). Your app's
metrics should use <app>_<metric> so they don't collide.
Counters end in _total. Gauges don't. (Per Prometheus naming
conventions.)
License
BSL-1.1 with named carveout against hosted observability platforms (Datadog, Grafana Cloud, New Relic, etc.). Change date: 2030-05-31 (Apache 2.0).
