import { Meter } from '@opentelemetry/api'; import { j as UnifiedAdmitter, C as ConcurrencyGuard } from './unified-D0OOwfM-.cjs'; import { D as Decision, L as Limiter } from './types-DKirIBQt.cjs'; import './store-BZNM-FbH.cjs'; /** * Optional OpenTelemetry observability layer. * * `@opentelemetry/api` is an *optional* peer dependency. Everything here imports it * **type-only** ({@link Meter}), so the import is erased at compile time and this module carries * no runtime dependency on the OTel SDK — callers who never touch it pay nothing, and callers who * do pass in their own already-configured {@link Meter}. * * Two wrappers are provided: * * - {@link instrumentLimiter} returns a new {@link Limiter} that delegates to the inner one while * recording a checks counter, a remaining histogram, and a store-latency histogram on every * `check`/`checkSync`. * - {@link instrumentGuard} attaches OTel *observable* gauges to an existing * {@link ConcurrencyGuard} that sample `guard.stats()` on each metric collection. */ /** * The **stable** metric names ThrottleKit emits. Treated as a public contract: a name changes only * with a deliberate major bump (a renamed metric breaks dashboards), and the metrics-contract test * pins these exact strings. Reference them instead of hard-coding (e.g. in Grafana/alert config). * An OTel Prometheus exporter maps the dots to underscores (`throttlekit.checks` → * `throttlekit_checks`); see docs/METRICS.md. */ declare const METRIC_NAMES: { /** Counter `+1` per check. Attributes: `{ strategy, allowed }`. */ readonly checks: "throttlekit.checks"; /** Histogram of `decision.remaining`. Attribute: `{ strategy }`. */ readonly remaining: "throttlekit.remaining"; /** Histogram (ms) of wall time inside the store per check. Attribute: `{ strategy }`. */ readonly storeLatency: "throttlekit.store.latency"; /** Observable gauge: current inferred concurrency ceiling. */ readonly concurrencyLimit: "throttlekit.concurrency.limit"; /** Observable gauge: concurrency leases outstanding. */ readonly concurrencyInflight: "throttlekit.concurrency.inflight"; /** Observable gauge (ms): windowed no-load RTT baseline. */ readonly concurrencyRttNoload: "throttlekit.concurrency.rtt_noload"; /** * Counter `+1` per **unified-admission denial**, attributed to the binding lane via a `{ lane }` * attribute ∈ `rate` | `concurrency` | `cost` | `policy` (the joint-LP bid-price filter). The metric * a span attribute can't be — it lets a Grafana board break denials down by *which axis* bound them. * Recorded by {@link instrumentAdmitter}. **Additive in 1.2.0** (a new name, not a changed one). */ readonly deniesByAxis: "throttlekit.denies_by_axis"; }; /** Common options for the instrumentation wrappers. */ interface InstrumentOptions { /** * Extra static attributes attached to every recorded measurement (e.g. `{ region: "us-east" }`). * Merged after the built-in attributes, so it can override `strategy`/`allowed` if desired. */ attributes?: Record; } /** * Wrap a {@link Limiter} so every check is observed through `meter`. The returned limiter is a * thin delegate: `reset` and `strategy` pass straight through, and both `check` (async) and * `checkSync` (sync) record: * * - `throttlekit.checks` — counter, `+1` per check, attributes * `{ strategy, allowed, ...opts.attributes }`. * - `throttlekit.remaining` — histogram of `decision.remaining`, attribute `{ strategy }`. * - `throttlekit.store.latency` — histogram (unit `ms`) of the wall time spent inside the inner * check, attribute `{ strategy }`. * * Instruments are created once, outside the hot path. If the inner `checkSync` throws (an * async-only store), the error propagates unchanged and no measurement is recorded. */ declare function instrumentLimiter(limiter: Limiter, meter: Meter, opts?: InstrumentOptions): Limiter; /** * Attach OpenTelemetry *observable* gauges to an existing {@link ConcurrencyGuard}. The same guard * is returned (its `acquire`/`limit`/`inflight`/`stats` are untouched) — this only registers * callbacks that sample {@link ConcurrencyGuard.stats} whenever the SDK collects metrics: * * - `throttlekit.concurrency.limit` — the current inferred ceiling. * - `throttlekit.concurrency.inflight` — outstanding leases. * - `throttlekit.concurrency.rtt_noload` — the windowed no-load RTT baseline (ms). * * A single batched callback feeds all three gauges, so `stats()` is read exactly once per * collection. Passive observation never perturbs the guard's adaptive estimate. */ declare function instrumentGuard(guard: ConcurrencyGuard, meter: Meter, opts?: InstrumentOptions): ConcurrencyGuard; /** * Wrap a {@link UnifiedAdmitter} so every **denied** admission increments the * {@link METRIC_NAMES.deniesByAxis} counter with a `{ lane }` attribute identifying the binding lane — * `rate` / `concurrency` / `cost`, or `"policy"` for a joint-LP bid-price denial. This is the one signal * the `throttlekit.binding_axis` *span* attribute could never be: a metric label a Grafana board can group * by, so denials finally decompose by axis — * `sum by (lane) (rate(throttlekit_denies_by_axis_total[5m]))`. * * The returned admitter delegates `admit` / `admitSync` / `lastDecisions` to the inner one and records * **only denials** (an allow records nothing). The lane is read from the admission's own `bindingAxis` * (exact, never racy); a denied admission with no binding axis is — by the `unifiedAdmission` contract — * a joint-LP `policy` denial. `options.attributes` (e.g. `{ region }`) merge onto every measurement. * * @example * ```ts * import { instrumentAdmitter } from "throttlekit/otel"; * import { metrics } from "@opentelemetry/api"; * * const admit = instrumentAdmitter( * unifiedAdmission({ rate, concurrency, cost }), * metrics.getMeter("checkout"), * ); * const { decision, release } = await admit.admit({ key: tenant, cost: tokens }); * ``` */ declare function instrumentAdmitter(admitter: UnifiedAdmitter, meter: Meter, options?: InstrumentOptions): UnifiedAdmitter; /** * The **stable** span-attribute keys set by {@link recordDecisionOnSpan}. A public contract like * {@link METRIC_NAMES}; pinned by the metrics-contract test. See docs/METRICS.md. */ declare const SPAN_ATTRIBUTES: { /** The active strategy name (e.g. `"gcra"`, `"quota"`). */ readonly strategy: "throttlekit.strategy"; /** Whether the request was admitted (boolean). */ readonly allowed: "throttlekit.allowed"; /** The effective ceiling. */ readonly limit: "throttlekit.limit"; /** Units remaining after the decision. */ readonly remaining: "throttlekit.remaining"; /** Milliseconds to wait before retrying (`0` when allowed). */ readonly retryAfterMs: "throttlekit.retry_after_ms"; /** * For `unifiedAdmission`: the axis that bound the combined Decision * (`"concurrency"` | `"rate"` | `"cost"`). Only set when the combined * decision was denied — when admitted, no axis was binding. Set by * {@link recordUnifiedAdmissionOnSpan} from the * `UnifiedAdmitter.lastDecisions()` snapshot. (TK-1008) */ readonly bindingAxis: "throttlekit.binding_axis"; }; /** The slice of an OpenTelemetry `Span` used to record decision attributes (structural; no import). */ interface SpanLike { setAttribute(key: string, value: string | number | boolean): unknown; } /** * Record a rate-limit {@link Decision} onto an OpenTelemetry span using the stable * {@link SPAN_ATTRIBUTES} keys, so a trace can be searched by `throttlekit.allowed=false` and * faceted by `throttlekit.strategy`. Pass the span you already have (e.g. `trace.getActiveSpan()`); * typing it structurally keeps this module dependency-free. `extra` adds your own attributes. * * @example * ```ts * import { trace } from "@opentelemetry/api"; * import { recordDecisionOnSpan } from "throttlekit/otel"; * const d = await limiter.check(key); * const span = trace.getActiveSpan(); * if (span) recordDecisionOnSpan(span, d, limiter.strategy.name); * ``` */ declare function recordDecisionOnSpan(span: SpanLike, decision: Decision, strategyName: string, extra?: Record): void; /** The lastDecisions snapshot shape from {@link UnifiedAdmitter.lastDecisions}. */ type UnifiedLastDecisions = Readonly>>; /** * Identify the **binding axis** for a unified admission — the first * denying axis in concurrency → rate → cost order (the same order * `unifiedAdmission`'s sequential mode evaluates in, so the result * matches the user's mental model of "which check blocked me first"). * * Returns `undefined` when no axis denied (the combined decision was * either an allow, or no axis was configured). * * The fused backend evaluates rate and cost atomically — both can deny * in the same admission. We still return the first one in the * concurrency → rate → cost order so the attribute value is * deterministic and matches the sequential convention. * * @example * ```ts * import { trace } from "@opentelemetry/api"; * import { bindingAxisOf } from "throttlekit/otel"; * * const { decision } = await admit.admit({ key, cost: tokens }); * if (!decision.allowed) { * const axis = bindingAxisOf(admit.lastDecisions()); * span?.setAttribute("throttlekit.binding_axis", axis ?? "unknown"); * } * ``` */ declare function bindingAxisOf(lastDecisions: UnifiedLastDecisions): "rate" | "concurrency" | "cost" | undefined; /** * Record a `unifiedAdmission` decision onto an OpenTelemetry span. Sets * the standard {@link SPAN_ATTRIBUTES} (`allowed`/`limit`/`remaining`/ * `retryAfterMs`) plus the **`throttlekit.binding_axis`** attribute * identifying which axis (rate / concurrency / cost) bound the * decision when it was denied. The `strategy` attribute is *not* set * — unified admissions span multiple strategies, so the binding axis * is the analogous classifier. Pass `extra` for any additional * attributes you want on the span. * * @example * ```ts * import { trace } from "@opentelemetry/api"; * import { recordUnifiedAdmissionOnSpan } from "throttlekit/otel"; * * const { decision } = await admit.admit({ key, cost: tokens }); * const span = trace.getActiveSpan(); * if (span) recordUnifiedAdmissionOnSpan(span, decision, admit.lastDecisions()); * ``` */ declare function recordUnifiedAdmissionOnSpan(span: SpanLike, decision: Decision, lastDecisions: UnifiedLastDecisions, extra?: Record): void; export { type InstrumentOptions, METRIC_NAMES, SPAN_ATTRIBUTES, type SpanLike, bindingAxisOf, instrumentAdmitter, instrumentGuard, instrumentLimiter, recordDecisionOnSpan, recordUnifiedAdmissionOnSpan };