4.0 KiB
instruments — api.rbac.denials_total
(no submodules)
Purpose
Owns the single OpenTelemetry counter api.rbac.denials_total and the
factory that constructs the RbacInstruments registry against a
caller-supplied MeterProvider. The middleware itself never touches
the global OTel API directly — every emission flows through
recordRbacDenial(instruments, { plugin, route, reason }), which
guarantees the three label keys are populated on every call and the
base attributes from the host (host-id, host-version, etc.) are
merged in.
Centralising emission here lets the spec-conformance test pin the
exact counter name, label set, and reason vocabulary against a real
MeterProvider from @opentelemetry/sdk-metrics — empirically the
class of regression that broke production dashboards in two of three
prior incidents was a label-key rename slipping past lint+typecheck.
Features
- Single counter
api.rbac.denials_totalwith description, created on the@suaveplan/api-rbacmeter. - Closed
RbacDenialReasonunion —"missing-scope" | "rule-deny" | "no-principal"— so dashboard authors can rely on a finite label set. - Frozen
RBAC_DENIAL_REASONSandRBAC_DENIAL_LABEL_KEYStuples for tests and metric-discovery tooling. - Base attributes merged on every emission; host injects host-id / host-version at boot.
MeterProvideris injectable — tests use a realMeterProviderfrom@opentelemetry/sdk-metrics, production resolves throughmetrics.getMeterProvider().
Usage: Basic
import {
createRbacInstruments,
recordRbacDenial,
} from "@suaveplan/api-rbac/instruments";
const instruments = createRbacInstruments();
recordRbacDenial(instruments, {
plugin: "billing",
route: "/invoices",
reason: "missing-scope",
});
Usage: Advanced
Inject a real MeterProvider and a PeriodicExportingMetricReader
with an InMemoryMetricExporter to assert the exact counter
emissions in a conformance test:
import { metrics } from "@opentelemetry/api";
import {
InMemoryMetricExporter,
MeterProvider,
PeriodicExportingMetricReader,
} from "@opentelemetry/sdk-metrics";
import {
createRbacInstruments,
RBAC_DENIALS_TOTAL_NAME,
recordRbacDenial,
} from "@suaveplan/api-rbac/instruments";
const exporter = new InMemoryMetricExporter(0);
const reader = new PeriodicExportingMetricReader({ exporter });
const provider = new MeterProvider({ readers: [reader] });
const instruments = createRbacInstruments({
meterProvider: provider,
baseAttributes: { host: "h-1" },
});
recordRbacDenial(instruments, {
plugin: "billing",
route: "/x",
reason: "rule-deny",
});
await provider.forceFlush();
const dump = exporter.getMetrics();
// dump[0].scopeMetrics[0].metrics carries RBAC_DENIALS_TOTAL_NAME …
API Reference
RBAC_DENIALS_TOTAL_NAME— string literal"api.rbac.denials_total".RBAC_METER_NAME— string literal"@suaveplan/api-rbac".RBAC_DENIAL_REASONS— frozen tuple of legal reason labels.RBAC_DENIAL_LABEL_KEYS— frozen tuple["plugin", "route", "reason"].RbacDenialReason—"missing-scope" | "rule-deny" | "no-principal".RbacInstruments—{ denials, baseAttributes }.RbacInstrumentsOptions—{ meterProvider?, baseAttributes? }.createRbacInstruments(options?)— factory.recordRbacDenial(instruments, attrs)— emission helper.
Implementation Notes
The factory falls back to metrics.getMeterProvider() only when no
provider is supplied; production hosts always pass their own
provider so SDK installation is opt-in. The counter description is
load-bearing for the OTel collector's view / attribute_filter
configuration — operators rely on it to scope the metric to
particular pipelines without re-discovering it. Object.freeze on
the registry plus Object.freeze on the base attribute bag makes
accidental in-place mutation throw in strict mode, which is the
class of bug tslib + verbatimModuleSyntax cannot catch.