Initial Commit
This commit is contained in:
115
packages/api-stack/api/api-rbac/src/instruments/instruments.md
Normal file
115
packages/api-stack/api/api-rbac/src/instruments/instruments.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# `instruments` — `api.rbac.denials_total`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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_total` with description, created
|
||||
on the `@suaveplan/api-rbac` meter.
|
||||
- Closed `RbacDenialReason` union — `"missing-scope" | "rule-deny" |
|
||||
"no-principal"` — so dashboard authors can rely on a finite label
|
||||
set.
|
||||
- Frozen `RBAC_DENIAL_REASONS` and `RBAC_DENIAL_LABEL_KEYS` tuples
|
||||
for tests and metric-discovery tooling.
|
||||
- Base attributes merged on every emission; host injects host-id /
|
||||
host-version at boot.
|
||||
- `MeterProvider` is injectable — tests use a real
|
||||
`MeterProvider` from `@opentelemetry/sdk-metrics`, production
|
||||
resolves through `metrics.getMeterProvider()`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
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:
|
||||
|
||||
```ts
|
||||
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.
|
||||
Reference in New Issue
Block a user