@dstny/scp-metrics
v0.1.0
Published
Standalone OpenTelemetry Metrics SDK
Readme
@dstny/scp-metrics
Standalone wrapper around the OpenTelemetry Metrics SDK for OTLP/HTTP export to Alloy → Prometheus.
Adds one thing the raw OTel JS API doesn't give you: a compile-time label schema per metric, so every call site for a given metric name is forced to pass the exact same label shape.
Usage
import { ScpMetrics } from '@dstny/scp-metrics'
const scpMetrics = new ScpMetrics()
await scpMetrics.init({
url: 'https://alloy.example.com/v1/metrics',
token: accessToken,
resourceAttributes: { 'service.name': 'ScpSdk', 'service.version': '1.2.3' },
// Delays the very first export by rand(0, 5s) so a mass reconnect (deploy, outage
// recovery) doesn't make every client hit the collector at once. Defaults to true.
initialJitter: true,
})
// Labels type is declared once and enforced at every call site.
const requestCount = scpMetrics.defineCounter<{ method: string }>('http_requests_total')
requestCount.add(1, { method: 'GET' })
// requestCount.add(1) // compile error: missing labels
// requestCount.add(1, { method: 'GET', x: 1 }) // compile error: excess label key
const requestDuration = scpMetrics.defineHistogram<{ status: 'success' | 'failure' }>(
'http_request_duration_ms'
)
requestDuration.record(842, { status: 'success' })On token refresh or logout, call scpMetrics.setToken(newToken) / scpMetrics.shutdown() -
both rebind that instance's MeterProvider live, no page reload needed.
Design notes
- No shared/global state: each
new ScpMetrics()instance owns its ownMeterProviderand instrument handles, so independent instances never contend for the same pipeline. This is deliberate rather than an oversight -@opentelemetry/api's own global meter-provider registry only supports one registered provider per process, so this package bypasses it entirely and holds a localMeterProviderreference per instance instead. In practice this means different consumers (or the same consumer reporting to different destinations) can each construct their own instance andinit()it with its ownurl/token, and they will never observe or interfere with each other's metrics. initialJitter(default on) delays the very first export byrand(0, 5s)so a mass reconnect (deploy, outage recovery) doesn't make every client hit the collector at once.- Export is purely interval-driven: a
PeriodicExportingMetricReadersamples current instrument state on a fixed ~5s clock, independent of whether anything changed. Required for Prometheusrate()/staleness semantics, and keeps request volume bounded by the clock tick rather than by how many/how often instruments are updated. - Keep label attributes low-cardinality - see the linked Prometheus naming docs. A runtime
check in
registry.tswarns (never throws) if the same metric name is later recorded with a different label key set, since that's a real Prometheus problem TS generics alone can't catch across separately-compiled call sites. This check is process-wide (not per-instance): it's a cheap dev-time safety net for divergentdefineX<...>()declarations, not part of the actual metrics export path, so it doesn't reintroduce cross-instance coupling.
