first commit
This commit is contained in:
52
docs/networking/network/network-plugin-monitor/README.md
Normal file
52
docs/networking/network/network-plugin-monitor/README.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# @suaveplan/network-plugin-monitor - Production monitoring + distributed tracing for the Suaveplan mesh: metric collector, alert engine, network tracer, historical store, adaptive sampler
|
||||
|
||||
## Summary
|
||||
|
||||
Production monitoring + distributed tracing for the Suaveplan mesh. Collects metrics across four categories (node / connection / message / hierarchy), supports the four standard metric types (Counter / Gauge / Histogram / Summary) with both push and pull collection, raises threshold-based `monitor:alert` bus events with cooldown suppression, propagates OpenTelemetry-compatible trace ids across mesh hops with four sampling strategies (probabilistic / rate-limited / error-based / priority-based) plus clock-skew detection and correction, retains rolled-up history per metric, and adapts sampling rate downward under high metric volume so monitoring overhead never degrades the network it observes. Wrapped in a single `MonitorPlugin` satisfying the `NetworkPlugin` lifecycle.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [alert](./src/alert/alert-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [alert-engine](./src/alert/alert-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [collector](./src/collector/metric-collector.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [metric-collector](./src/collector/metric-collector.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [grafana](./src/grafana/dashboard-generator.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [dashboard-generator](./src/grafana/dashboard-generator.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [history](./src/history/historical-data-store.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [historical-data-store](./src/history/historical-data-store.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [monitor](./src/monitor/network-monitor.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [network-monitor](./src/monitor/network-monitor.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [plugin](./src/plugin/monitor-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [monitor-plugin](./src/plugin/monitor-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [prometheus](./src/prometheus/prometheus-exposition.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [prometheus-exposition](./src/prometheus/prometheus-exposition.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [bandwidth-monitor](./src/quality/bandwidth-monitor.md) — `BandwidthMonitor` records per-link bandwidth / RTT / packet-loss measurements into a bounded ring buffer and exposes s…
|
||||
- [degradation-manager](./src/quality/degradation-manager.md) — `DegradationManager` is a 4-rung quality classifier that decides the current `QualityLevel` (`HIGH`/`MEDIUM`/`LOW`/`MIN…
|
||||
- [quality-middleware](./src/quality/quality-middleware.md) — `createQualityMiddleware` builds an outgoing-message gate that drops or forwards each message based on the current `Qua…
|
||||
- [quality-types](./src/quality/quality-types.md) — Shared type definitions for the quality-class classification + adaptive degradation feature group. These types underpin…
|
||||
- [sampler](./src/sampler/adaptive-sampler.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [adaptive-sampler](./src/sampler/adaptive-sampler.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [tracer](./src/tracer/network-tracer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [network-tracer](./src/tracer/network-tracer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [error-codes](./src/error-codes.md) — Frozen registry of every error code raised by the monitor package. Codes are namespaced under `NET_MON` and registered…
|
||||
<!-- end:auto-docs --> - [alert-engine](./src/alert/alert-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [collector](./src/collector/metric-collector.md) — <!-- begin:auto-docs --> _(no submodules)_- [metric-collector](./src/collector/metric-collector.md) — <!-- begin:auto-docs --> _(no submodules)_- [grafana](./src/grafana/dashboard-generator.md) — <!-- begin:auto-docs --> _(no submodules)_- [dashboard-generator](./src/grafana/dashboard-generator.md) — <!-- begin:auto-docs --> _(no submodules)_- [history](./src/history/historical-data-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [historical-data-store](./src/history/historical-data-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [monitor](./src/monitor/network-monitor.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-monitor](./src/monitor/network-monitor.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/monitor-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [monitor-plugin](./src/plugin/monitor-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [prometheus](./src/prometheus/prometheus-exposition.md) — <!-- begin:auto-docs --> _(no submodules)_- [prometheus-exposition](./src/prometheus/prometheus-exposition.md) — <!-- begin:auto-docs --> _(no submodules)_- [bandwidth-monitor](./src/quality/bandwidth-monitor.md) — `BandwidthMonitor` records per-link bandwidth / RTT / packet-loss measurements into a bounded ring buffer and exposes s…
|
||||
- [degradation-manager](./src/quality/degradation-manager.md) — `DegradationManager` is a 4-rung quality classifier that decides the current `QualityLevel` (`HIGH`/`MEDIUM`/`LOW`/`MIN…
|
||||
- [quality-middleware](./src/quality/quality-middleware.md) — `createQualityMiddleware` builds an outgoing-message gate that drops or forwards each message based on the current `Qua…
|
||||
- [quality-types](./src/quality/quality-types.md) — Shared type definitions for the quality-class classification + adaptive degradation feature group. These types underpin…
|
||||
- [sampler](./src/sampler/adaptive-sampler.md) — <!-- begin:auto-docs --> _(no submodules)_- [adaptive-sampler](./src/sampler/adaptive-sampler.md) — <!-- begin:auto-docs --> _(no submodules)_- [tracer](./src/tracer/network-tracer.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-tracer](./src/tracer/network-tracer.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of every error code raised by the monitor package. Codes are namespaced under `NET_MON` and registered…
|
||||
- [alert-engine](./src/alert/alert-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [collector](./src/collector/metric-collector.md) — <!-- begin:auto-docs --> _(no submodules)_- [metric-collector](./src/collector/metric-collector.md) — <!-- begin:auto-docs --> _(no submodules)_- [grafana](./src/grafana/dashboard-generator.md) — <!-- begin:auto-docs --> _(no submodules)_- [dashboard-generator](./src/grafana/dashboard-generator.md) — <!-- begin:auto-docs --> _(no submodules)_- [history](./src/history/historical-data-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [historical-data-store](./src/history/historical-data-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [monitor](./src/monitor/network-monitor.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-monitor](./src/monitor/network-monitor.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/monitor-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [monitor-plugin](./src/plugin/monitor-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [prometheus](./src/prometheus/prometheus-exposition.md) — <!-- begin:auto-docs --> _(no submodules)_- [prometheus-exposition](./src/prometheus/prometheus-exposition.md) — <!-- begin:auto-docs --> _(no submodules)_- [bandwidth-monitor](./src/quality/bandwidth-monitor.md) — `BandwidthMonitor` records per-link bandwidth / RTT / packet-loss measurements into a bounded ring buffer and exposes s…
|
||||
- [degradation-manager](./src/quality/degradation-manager.md) — `DegradationManager` is a 4-rung quality classifier that decides the current `QualityLevel` (`HIGH`/`MEDIUM`/`LOW`/`MIN…
|
||||
- [quality-middleware](./src/quality/quality-middleware.md) — `createQualityMiddleware` builds an outgoing-message gate that drops or forwards each message based on the current `Qua…
|
||||
- [quality-types](./src/quality/quality-types.md) — Shared type definitions for the quality-class classification + adaptive degradation feature group. These types underpin…
|
||||
- [sampler](./src/sampler/adaptive-sampler.md) — <!-- begin:auto-docs --> _(no submodules)_- [adaptive-sampler](./src/sampler/adaptive-sampler.md) — <!-- begin:auto-docs --> _(no submodules)_- [tracer](./src/tracer/network-tracer.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-tracer](./src/tracer/network-tracer.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of every error code raised by the monitor package. Codes are namespaced under `NET_MON` and registered…
|
||||
- [alert-engine](./src/alert/alert-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [collector](./src/collector/metric-collector.md) — <!-- begin:auto-docs --> _(no submodules)_- [metric-collector](./src/collector/metric-collector.md) — <!-- begin:auto-docs --> _(no submodules)_- [grafana](./src/grafana/dashboard-generator.md) — <!-- begin:auto-docs --> _(no submodules)_- [dashboard-generator](./src/grafana/dashboard-generator.md) — <!-- begin:auto-docs --> _(no submodules)_- [history](./src/history/historical-data-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [historical-data-store](./src/history/historical-data-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [monitor](./src/monitor/network-monitor.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-monitor](./src/monitor/network-monitor.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/monitor-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [monitor-plugin](./src/plugin/monitor-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [prometheus](./src/prometheus/prometheus-exposition.md) — <!-- begin:auto-docs --> _(no submodules)_- [prometheus-exposition](./src/prometheus/prometheus-exposition.md) — <!-- begin:auto-docs --> _(no submodules)_- [bandwidth-monitor](./src/quality/bandwidth-monitor.md) — `BandwidthMonitor` records per-link bandwidth / RTT / packet-loss measurements into a bounded ring buffer and exposes s…
|
||||
- [degradation-manager](./src/quality/degradation-manager.md) — `DegradationManager` is a 4-rung quality classifier that decides the current `QualityLevel` (`HIGH`/`MEDIUM`/`LOW`/`MIN…
|
||||
- [quality-middleware](./src/quality/quality-middleware.md) — `createQualityMiddleware` builds an outgoing-message gate that drops or forwards each message based on the current `Qua…
|
||||
- [quality-types](./src/quality/quality-types.md) — Shared type definitions for the quality-class classification + adaptive degradation feature group. These types underpin…
|
||||
- [sampler](./src/sampler/adaptive-sampler.md) — <!-- begin:auto-docs --> _(no submodules)_- [adaptive-sampler](./src/sampler/adaptive-sampler.md) — <!-- begin:auto-docs --> _(no submodules)_- [tracer](./src/tracer/network-tracer.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-tracer](./src/tracer/network-tracer.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of every error code raised by the monitor package. Codes are namespaced under `NET_MON` and registered…
|
||||
- [alert-engine](./src/alert/alert-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [collector](./src/collector/metric-collector.md) — <!-- begin:auto-docs --> _(no submodules)_ - [metric-collector](./src/collector/metric-collector.md) — <!-- begin:auto-docs --> _(no submodules)_- [grafana](./src/grafana/dashboard-generator.md) — <!-- begin:auto-docs --> _(no submodules)_ - [dashboard-generator](./src/grafana/dashboard-generator.md) — <!-- begin:auto-docs --> _(no submodules)_- [history](./src/history/historical-data-store.md) — <!-- begin:auto-docs --> _(no submodules)_ - [historical-data-store](./src/history/historical-data-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [monitor](./src/monitor/network-monitor.md) — <!-- begin:auto-docs --> _(no submodules)_ - [network-monitor](./src/monitor/network-monitor.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/monitor-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ - [monitor-plugin](./src/plugin/monitor-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [prometheus](./src/prometheus/prometheus-exposition.md) — <!-- begin:auto-docs --> _(no submodules)_ - [prometheus-exposition](./src/prometheus/prometheus-exposition.md) — <!-- begin:auto-docs --> _(no submodules)_- [bandwidth-monitor](./src/quality/bandwidth-monitor.md) — `BandwidthMonitor` records per-link bandwidth / RTT / packet-loss measurements into a bounded ring buffer and exposes s…
|
||||
- [degradation-manager](./src/quality/degradation-manager.md) — `DegradationManager` is a 4-rung quality classifier that decides the current `QualityLevel` (`HIGH`/`MEDIUM`/`LOW`/`MIN…
|
||||
- [quality-middleware](./src/quality/quality-middleware.md) — `createQualityMiddleware` builds an outgoing-message gate that drops or forwards each message based on the current `Qua…
|
||||
- [quality-types](./src/quality/quality-types.md) — Shared type definitions for the quality-class classification + adaptive degradation feature group. These types underpin…
|
||||
- [sampler](./src/sampler/adaptive-sampler.md) — <!-- begin:auto-docs --> _(no submodules)_ - [adaptive-sampler](./src/sampler/adaptive-sampler.md) — <!-- begin:auto-docs --> _(no submodules)_- [tracer](./src/tracer/network-tracer.md) — <!-- begin:auto-docs --> _(no submodules)_ - [network-tracer](./src/tracer/network-tracer.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of every error code raised by the monitor package. Codes are namespaced under `NET_MON` and registered…
|
||||
@@ -0,0 +1,86 @@
|
||||
# `AlertEngine`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Evaluates `AlertRule` definitions against `MetricCollector` snapshots and emits `monitor:alert` bus events when thresholds are tripped. Suppresses storm-firings via per-rule cooldown so a single sustained spike does not flood subscribers.
|
||||
|
||||
## Features
|
||||
|
||||
- Full closed-union `AlertComparison` set (`>`, `>=`, `<`, `<=`, `==`, `!=`) and `AlertStatistic` set (`latest`, `min`, `max`, `avg`, `p50`, `p95`, `p99`, `count`).
|
||||
- Cooldown suppression returns `state: "suppressed"` and skips the bus emit; consumers see only "first firing" + "recovered" by default.
|
||||
- `evaluateAll` returns the per-rule outcome so callers can render dashboards directly without bus subscriptions.
|
||||
- Injectable clock + bus for deterministic tests.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
AlertEngine,
|
||||
MetricCollector,
|
||||
} from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
const collector = new MetricCollector();
|
||||
collector.define({ name: "node.cpu", category: "node", type: "gauge" });
|
||||
collector.record("node.cpu", 0.91);
|
||||
|
||||
const engine = new AlertEngine({ collector });
|
||||
engine.addRule({
|
||||
id: "cpu-warn",
|
||||
metricName: "node.cpu",
|
||||
statistic: "latest",
|
||||
comparison: ">",
|
||||
threshold: 0.9,
|
||||
severity: "warning",
|
||||
});
|
||||
engine.evaluateAll(); // [{ evaluation, state: "firing" }]
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
AlertEngine,
|
||||
type AlertEvaluation,
|
||||
MetricCollector,
|
||||
} from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
const events: AlertEvaluation[] = [];
|
||||
const engine = new AlertEngine({
|
||||
collector: new MetricCollector(),
|
||||
bus: {
|
||||
emit: (_event, payload) => events.push(payload.evaluation),
|
||||
},
|
||||
defaultCooldownMs: 30_000,
|
||||
clock: () => 1_700_000_000_000,
|
||||
});
|
||||
|
||||
engine.addRule({
|
||||
id: "p95-warn",
|
||||
metricName: "connection.latency",
|
||||
statistic: "p95",
|
||||
comparison: ">",
|
||||
threshold: 250,
|
||||
severity: "warning",
|
||||
cooldownMs: 5_000,
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class AlertEngine(config: { collector, bus?, clock?, defaultCooldownMs? })`
|
||||
- `addRule(rule: AlertRule): void` — throws `ALERT_RULE_DUPLICATE` on duplicate ids.
|
||||
- `removeRule(id: string): void` — throws `ALERT_RULE_NOT_FOUND`.
|
||||
- `list(): ReadonlyArray<AlertRule>`.
|
||||
- `evaluate(rule): { evaluation, state }`.
|
||||
- `evaluateAll(): ReadonlyArray<{ evaluation, state }>`.
|
||||
- `reset(): void` — clears every cooldown so the next evaluation can fire immediately.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The engine reads stats from the collector exactly once per `evaluate` call so two rules pointing at the same metric do not race against each other if records arrive concurrently.
|
||||
- `latest` reads the most-recent sample directly from the snapshot rather than from `stats` because `MetricStats` does not retain ordering — empty buffers fall back to `0` with `count === 0`.
|
||||
- Cooldown is enforced per-rule by `id`; rules sharing a metric name still suppress independently. Resetting an engine clears every cooldown without touching the rules themselves.
|
||||
- The bus is optional so the engine is fully usable in unit tests via `evaluate(...)` return values; production consumers wire the bus and ignore the return value.
|
||||
@@ -0,0 +1,73 @@
|
||||
# `MetricCollector`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Stores metric definitions and rolling sample buffers, computes percentile statistics on demand, and supports both push (`record`/`increment`) and pull (`scrape`) collection modes. Knows nothing about transports or envelopes — the monitor wraps it for domain-specific recording.
|
||||
|
||||
## Features
|
||||
|
||||
- Define + record + snapshot per-metric API; rolling window per metric (default 256 samples).
|
||||
- 4 metric types via the `MetricType` union; `increment` enforces `counter` semantics so callers cannot bump a histogram by mistake.
|
||||
- Configurable per-metric sampling rate (`0..1`); samples that fail the rate gate are skipped without error.
|
||||
- `stats(name)` returns an `MetricStats` snapshot (`count`, `min`, `max`, `avg`, `p50`, `p95`, `p99`).
|
||||
- Injectable clock + RNG so deterministic tests can pin timing and sampling decisions.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { MetricCollector } from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
const collector = new MetricCollector();
|
||||
collector.define({ name: "node.cpu", category: "node", type: "gauge" });
|
||||
collector.record("node.cpu", 0.42);
|
||||
collector.stats("node.cpu").avg; // 0.42
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { MetricCollector } from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
const collector = new MetricCollector({
|
||||
clock: () => 1_700_000_000_000,
|
||||
rng: () => 0,
|
||||
});
|
||||
collector.define({
|
||||
name: "msg.in.count",
|
||||
category: "message",
|
||||
type: "counter",
|
||||
windowSize: 1024,
|
||||
samplingRate: 0.5,
|
||||
});
|
||||
|
||||
collector.increment("msg.in.count", 1); // 1
|
||||
collector.increment("msg.in.count", 5); // 6
|
||||
collector.increment("msg.in.count", 2, { type: "registry:add" });
|
||||
const m = collector.snapshot("msg.in.count");
|
||||
const all = collector.scrape();
|
||||
collector.reset("msg.in.count");
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class MetricCollector(config?)`
|
||||
- `define(definition: MetricDefinition): void`.
|
||||
- `record(name, value, labels?): MetricValue | null` — `null` only when the sample is skipped by the per-metric rate gate.
|
||||
- `increment(name, delta?, labels?): MetricValue | null` — throws `METRIC_TYPE_MISMATCH` when applied to non-counter metrics.
|
||||
- `snapshot(name): Metric` — frozen-style copy of the rolling buffer.
|
||||
- `stats(name): MetricStats` — count / min / max / avg / p50 / p95 / p99 (all zero when empty).
|
||||
- `scrape(): ReadonlyArray<Metric>` — pull-mode snapshot of every registered metric.
|
||||
- `reset(name?): void` — clears one or all sample buffers.
|
||||
- `has(name): boolean`.
|
||||
- `interface MetricDefinition` — `{ name, category, type, windowSize?, samplingRate? }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Every read-side method that takes a `name` throws `METRIC_NOT_FOUND` when the metric is not defined; this is loud-failure on caller bugs rather than silent success.
|
||||
- Percentile computation uses the standard "nearest-rank" method (`ceil(n * p) - 1`) clamped to the buffer length so empty buffers produce `0` (with `count === 0` to disambiguate).
|
||||
- The rolling buffer is a plain array with `shift()` once `windowSize` is exceeded — adequate for the default 256 samples; higher-volume metrics should set `samplingRate < 1` rather than ballooning the window.
|
||||
- `snapshot` returns a copy of the samples so consumers can iterate without racing the collector's writes.
|
||||
- `increment` reads the previous sample's `value` so counters accumulate correctly across the rolling window — even after eviction the next increment continues the run.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Error codes — `@suaveplan/network-plugin-monitor`
|
||||
|
||||
## Purpose
|
||||
|
||||
Frozen registry of every error code raised by the monitor package. Codes are namespaced under `NET_MON` and registered into the global error registry on first import via the side-effect import in `src/index.ts`. Each code maps to a single failure class so consumers can branch on `error.code` without parsing message text.
|
||||
|
||||
## Features
|
||||
|
||||
- Six codes covering metric registration, alert rule mutation, span lifecycle, and retention configuration.
|
||||
- `NetworkMonError` extends `SuaveplanError<NetworkMonErrorCode>` for cause-chain traversal, redaction, and OTel adapter compatibility.
|
||||
- Frozen registry — runtime mutation throws.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
NETWORK_MON_CODES,
|
||||
NetworkMonError,
|
||||
} from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
throw new NetworkMonError(
|
||||
NETWORK_MON_CODES.METRIC_NOT_FOUND,
|
||||
"monitor.metric-missing",
|
||||
{ context: { name: "node.cpu" } }
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
try {
|
||||
alertEngine.addRule(rule);
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof NetworkMonError &&
|
||||
err.code === NETWORK_MON_CODES.ALERT_RULE_DUPLICATE
|
||||
) {
|
||||
return alertEngine.replaceRule(rule);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `NETWORK_MON_CODES` — frozen `KEY -> "NET_MON.KEY"` map.
|
||||
- `NetworkMonErrorCode` — TypeScript union of every code value.
|
||||
- `NetworkMonError` — `SuaveplanError<NetworkMonErrorCode>` subclass; `name === "NetworkMonError"`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The registry is constructed via `defineCodes("NET_MON", […] as const)` so the type system enumerates the value union; adding a code requires bumping the `as const` tuple.
|
||||
- Codes are deliberately coarse (one per failure class) so consumers build stable retry / observability policies without churn when implementations change.
|
||||
- `RETENTION_INVALID` is the boundary code for both `HistoricalDataStore` and the `AdaptiveSampler` rate clamps — both consume retention semantics, so a single code keeps the consumer side simple.
|
||||
- The side-effect import in `src/index.ts` registers the codes even when consumers tree-shake the named exports.
|
||||
@@ -0,0 +1,145 @@
|
||||
# `dashboard-generator`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Universal Grafana dashboard JSON generator. Produces the exact JSON model
|
||||
Grafana's import API consumes (`POST /api/dashboards/db`) — no HTTP client and
|
||||
no Grafana SDK dependency. The generator is a pure value-builder so it runs
|
||||
identically in browsers, Bun, Node, and Workers.
|
||||
|
||||
Lives in `@suaveplan/network-plugin-monitor` because the monitor plugin is the
|
||||
canonical telemetry surface for the Suaveplan mesh; bundling the dashboard
|
||||
builder here means consumers ship one package for both `/metrics` exposition
|
||||
and `/dashboards/db` provisioning.
|
||||
|
||||
## Features
|
||||
|
||||
- Zod-validated `addPanel` / dashboard options — unknown keys rejected.
|
||||
- Stable Prometheus datasource binding (`{type: "prometheus", uid: "prometheus"}`).
|
||||
- Auto-layout (12-wide, 8-tall panels reflowing every other panel).
|
||||
- Injectable RNG (`rng?: () => number`) for deterministic UID generation in
|
||||
tests — defaults to `Math.random` for production.
|
||||
- Stateless emission — `generate()` is referentially transparent for a given
|
||||
call sequence.
|
||||
- Schema-versioned (`schemaVersion: 38`, `version: 1`) so consumers know what
|
||||
they're targeting.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { GrafanaDashboardGenerator } from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
const d = new GrafanaDashboardGenerator({ title: "Suaveplan Network" });
|
||||
d.addPanel({
|
||||
title: "Request Rate",
|
||||
type: "graph",
|
||||
targets: [
|
||||
{ expr: "rate(sp_requests_total[5m])", legendFormat: "{{peer}}" },
|
||||
],
|
||||
});
|
||||
const json = d.toJSON();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { GrafanaDashboardGenerator } from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
let seed = 1;
|
||||
const rng = () => {
|
||||
seed = (seed * 1103515245 + 12345) & 0x7fffffff;
|
||||
return seed / 0x7fffffff;
|
||||
};
|
||||
|
||||
const d = new GrafanaDashboardGenerator({
|
||||
title: "Network Mesh",
|
||||
tags: ["mesh", "production"],
|
||||
refresh: "10s",
|
||||
timezone: "UTC",
|
||||
rng,
|
||||
});
|
||||
|
||||
d.addRow("Connections");
|
||||
d.addPanel({
|
||||
title: "Active Peers",
|
||||
type: "stat",
|
||||
targets: [{ expr: "sp_active_peers" }],
|
||||
gridPos: { x: 0, y: 1, w: 6, h: 4 },
|
||||
});
|
||||
d.addPanel({
|
||||
title: "RTT p95",
|
||||
type: "graph",
|
||||
targets: [
|
||||
{
|
||||
expr: 'histogram_quantile(0.95, rate(sp_rtt_ms_bucket[1m]))',
|
||||
legendFormat: "p95",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
d.addRow("Errors");
|
||||
d.addPanel({
|
||||
title: "Error Rate",
|
||||
type: "graph",
|
||||
targets: [{ expr: "rate(sp_errors_total[5m])", legendFormat: "{{code}}" }],
|
||||
});
|
||||
|
||||
// Push to Grafana provisioning API
|
||||
await fetch("https://grafana.example/api/dashboards/db", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: d.toJSON(),
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `class GrafanaDashboardGenerator`
|
||||
|
||||
Constructor: `new GrafanaDashboardGenerator(options: GrafanaDashboardOptions)`.
|
||||
|
||||
Options:
|
||||
|
||||
| Field | Default | Description |
|
||||
|---|---|---|
|
||||
| `title` | (required) | Dashboard title. |
|
||||
| `uid` | random | 9-char base36 UID (auto-derived from `rng()`). |
|
||||
| `tags` | `[]` | Grafana tags. |
|
||||
| `timezone` | `"browser"` | Grafana timezone string. |
|
||||
| `refresh` | `"30s"` | Auto-refresh interval. |
|
||||
| `editable` | `true` | Whether the dashboard is editable in the UI. |
|
||||
| `rng` | `Math.random` | Pluggable RNG for deterministic UID generation. |
|
||||
|
||||
Instance methods:
|
||||
|
||||
- `addPanel(panel: PanelOptions): void` — append a panel. Throws on Zod
|
||||
validation failure (unknown keys, empty targets, missing title).
|
||||
- `addRow(title: string): void` — append a row separator. Throws if title is
|
||||
empty.
|
||||
- `generate(): DashboardJson` — emits the dashboard JSON model.
|
||||
- `toJSON(): string` — pretty-printed (2-space indent) JSON string.
|
||||
- `panelCount: number` — current panel + row count (read-only).
|
||||
|
||||
Standalone schemas exported for caller-side validation:
|
||||
|
||||
- `GrafanaDashboardOptionsSchema`
|
||||
- `PanelOptionsSchema`
|
||||
- `PromQLTargetSchema`
|
||||
- `GridPosSchema`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Auto-layout uses `Math.floor(panelIndex/2)*8` — every two panels start a new
|
||||
row at 8-unit vertical pitch (matches the upstream behaviour 1:1).
|
||||
- The UID generator emits a base36 string padded to 9 characters; collisions
|
||||
are statistically negligible (~36⁹ ≈ 10¹⁴ space) but callers who need
|
||||
guaranteed uniqueness should pass an explicit `uid`.
|
||||
- The Prometheus datasource is hard-coded to UID `"prometheus"`. Grafana
|
||||
instances using a custom datasource UID must post-process the JSON or
|
||||
configure their provisioning binding accordingly.
|
||||
- `generate()` always returns `schemaVersion: 38` + `version: 1`. Future
|
||||
Grafana schema bumps will land as a new generator class, not a mutation of
|
||||
this one.
|
||||
@@ -0,0 +1,78 @@
|
||||
# `HistoricalDataStore`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Retains per-metric history with a configurable two-tier policy: a bounded raw-sample ring (for short-window analysis) plus rollup buckets (for long-window trend reads). The collector emits ephemeral samples; the store persists them so dashboards / capacity planners can ask "what did p95 look like an hour ago?".
|
||||
|
||||
## Features
|
||||
|
||||
- Two retention knobs — `maxRawSamples` (default 4096) and `maxRawAgeMs` (default 1 hour) — applied conjunctively on every ingest.
|
||||
- `rollup(metricName, cutoffMs)` aggregates raw samples older than the cutoff into 1-minute (default) bucket-wide `count` / `sum` / `min` / `max` records.
|
||||
- `forget` and `clear` give consumers explicit control over per-metric and global pruning.
|
||||
- Throws `RETENTION_INVALID` at construction for non-positive retention parameters.
|
||||
- Injectable clock for deterministic tests.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
HistoricalDataStore,
|
||||
MetricCollector,
|
||||
} from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
const collector = new MetricCollector();
|
||||
collector.define({ name: "node.cpu", category: "node", type: "gauge" });
|
||||
collector.record("node.cpu", 0.42);
|
||||
|
||||
const store = new HistoricalDataStore();
|
||||
store.ingest(collector.snapshot("node.cpu"));
|
||||
store.raws("node.cpu").length; // 1
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
HistoricalDataStore,
|
||||
type MetricValue,
|
||||
} from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
let now = 1_700_000_000_000;
|
||||
const store = new HistoricalDataStore({
|
||||
maxRawSamples: 1_000,
|
||||
maxRawAgeMs: 5 * 60_000,
|
||||
rollupBucketMs: 60_000,
|
||||
clock: () => now,
|
||||
});
|
||||
|
||||
const samples: MetricValue[] = Array.from({ length: 200 }, (_, i) => ({
|
||||
value: i,
|
||||
recordedAt: now - 6 * 60_000 + i * 100,
|
||||
}));
|
||||
store.ingest({ name: "x", category: "node", type: "gauge", samples });
|
||||
|
||||
now += 1_000;
|
||||
const buckets = store.rollup("x", now - 60_000);
|
||||
console.log(buckets.length, buckets[0]?.count);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class HistoricalDataStore(config?)`
|
||||
- `ingest(metric: Metric): void` — appends new samples and trims by age + count.
|
||||
- `raws(metricName): ReadonlyArray<MetricValue>` — non-destructive snapshot.
|
||||
- `rollup(metricName, cutoffMs): ReadonlyArray<HistoricalRollup>` — compacts old samples into buckets and returns the buckets produced.
|
||||
- `rollups_for(metricName): ReadonlyArray<HistoricalRollup>` — every bucket recorded so far.
|
||||
- `forget(metricName): void`.
|
||||
- `clear(): void`.
|
||||
- `interface HistoricalRollup` — `{ bucketStart, count, sum, min, max }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Trimming uses age-then-count: samples are first dropped if they fall outside `maxRawAgeMs`, then the remainder is truncated to `maxRawSamples` (most recent kept). This keeps the store from accumulating samples from stale, no-longer-emitting metrics.
|
||||
- Rollup buckets are aligned to wall-clock (`floor(recordedAt / bucketMs) * bucketMs`) so the same wall-clock minute always yields the same `bucketStart`, regardless of when `rollup` runs.
|
||||
- Rollups for a metric accumulate in insertion order; consumers that need a stable identity per `(metricName, bucketStart)` should de-duplicate downstream.
|
||||
- The store does not implement query / search — exporters and dashboards consume `raws` / `rollups_for` directly. Splitting that responsibility keeps the store focused on retention.
|
||||
@@ -0,0 +1,67 @@
|
||||
# `NetworkMonitor`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Domain-specific facade around `MetricCollector` that pins canonical metric names per category. Call sites use `recordNodeMetric(peerId, "cpu", 0.4)` instead of building `"node.cpu"` strings by hand and re-defining the metric on every call. The monitor lazily defines metrics on first record so consumers do not need to coordinate a startup phase.
|
||||
|
||||
## Features
|
||||
|
||||
- Four record helpers — `recordNodeMetric`, `recordConnectionMetric`, `recordMessageMetric`, `recordHierarchyMetric` — corresponding to the `MetricCategory` union.
|
||||
- Lazy metric definition: the first record per metric name auto-defines a `gauge` (node, hierarchy) or `histogram` (connection, message) using sensible defaults. Callers that need a different `MetricType` can `collector.define(...)` first.
|
||||
- Each helper attaches the relevant id labels (peerId, sourceId/targetId, messageType, scope) so percentiles and group-bys downstream are well-typed.
|
||||
- `listByCategory` returns the full snapshot for a category for dashboards / audit views.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
MetricCollector,
|
||||
NetworkMonitor,
|
||||
} from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
const collector = new MetricCollector();
|
||||
const monitor = new NetworkMonitor({ collector });
|
||||
monitor.recordNodeMetric("peer-1", "cpu", 0.42);
|
||||
monitor.recordConnectionMetric("peer-1", "peer-2", "latency", 35);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
MetricCollector,
|
||||
NetworkMonitor,
|
||||
} from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
const collector = new MetricCollector();
|
||||
collector.define({
|
||||
name: "message.processingTime",
|
||||
category: "message",
|
||||
type: "histogram",
|
||||
windowSize: 4096,
|
||||
});
|
||||
const monitor = new NetworkMonitor({ collector });
|
||||
monitor.recordMessageMetric("registry:update", "processingTime", 3.2);
|
||||
|
||||
const heap = monitor.listByCategory("message");
|
||||
console.log(heap.length);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class NetworkMonitor(config: { collector })`
|
||||
- `recordNodeMetric(peerId, name, value): void`.
|
||||
- `recordConnectionMetric(sourceId, targetId, name, value): void`.
|
||||
- `recordMessageMetric(messageType, name, value): void`.
|
||||
- `recordHierarchyMetric(scope, name, value): void`.
|
||||
- `listByCategory(category): ReadonlyArray<Metric>`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The fully-qualified name is always `"<category>.<name>"`. Pre-defined metrics that follow this convention are reused; metrics defined under a different shape are NOT overridden, so callers can stage explicit definitions before calling the helpers.
|
||||
- Defaults bias toward `gauge` for instantaneous values (CPU, node count) and `histogram` for distributions (latency, processing time). Counter-shaped metrics should be defined explicitly and recorded through `collector.increment(...)` rather than the monitor.
|
||||
- `listByCategory` runs a single `scrape()` and filters by category — adequate for the typical 10s-of-metrics scale; high-cardinality deployments should index the result themselves.
|
||||
- The monitor never throws on its own — only the collector raises (`METRIC_NOT_FOUND`, `METRIC_TYPE_MISMATCH`). The lazy-define behaviour is the single concession to ergonomics.
|
||||
@@ -0,0 +1,75 @@
|
||||
# `MonitorPlugin`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Single `NetworkPlugin` that bundles every monitor subsystem (`MetricCollector`, `NetworkMonitor`, `AlertEngine`, `NetworkTracer`, `HistoricalDataStore`, optional `AdaptiveSampler`) and ties their lifecycle to a host `NetworkNode`. Each subsystem is a public field so consumers extend behaviour by reaching past the plugin facade.
|
||||
|
||||
## Features
|
||||
|
||||
- Optional sampler — `null` when no `sampler` config is provided so deployments without rate concerns pay nothing.
|
||||
- `install(node)` constructs the alert engine bound to the node's bus so `monitor:alert` events flow without manual wiring.
|
||||
- `dispose` clears every subsystem and detaches the node so a re-install starts clean.
|
||||
- Public `collector` field is the substantive surface — define metrics, record samples, scrape pulls — without going through helper APIs.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { NetworkBus, NetworkNode } from "@suaveplan/network";
|
||||
import { MonitorPlugin } from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
const bus = new NetworkBus();
|
||||
const node = new NetworkNode({ id: "node-1", networkBus: bus });
|
||||
const plugin = new MonitorPlugin();
|
||||
node.use(plugin);
|
||||
await node.start();
|
||||
|
||||
plugin.monitor.recordNodeMetric("peer-1", "cpu", 0.42);
|
||||
plugin.alert?.addRule({
|
||||
id: "cpu-warn",
|
||||
metricName: "node.cpu",
|
||||
statistic: "latest",
|
||||
comparison: ">",
|
||||
threshold: 0.9,
|
||||
severity: "warning",
|
||||
});
|
||||
plugin.alert?.evaluateAll();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { NetworkBus, NetworkNode } from "@suaveplan/network";
|
||||
import { MonitorPlugin } from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
const plugin = new MonitorPlugin({
|
||||
sampler: { highWatermark: 1_000, lowWatermark: 200 },
|
||||
tracer: { strategy: "rate-limited", rateLimitPerSec: 100 },
|
||||
history: { maxRawSamples: 16_384, rollupBucketMs: 30_000 },
|
||||
alert: { defaultCooldownMs: 30_000 },
|
||||
});
|
||||
const node = new NetworkNode({ id: "n", networkBus: new NetworkBus() });
|
||||
node.use(plugin);
|
||||
await node.start();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class MonitorPlugin implements NetworkPlugin`
|
||||
- `name: "monitor"`.
|
||||
- `collector` / `monitor` / `tracer` / `history` — public fields.
|
||||
- `sampler: AdaptiveSampler | null` — present only when configured.
|
||||
- `alert: AlertEngine | null` — set on `install`.
|
||||
- `constructor(config?: MonitorPluginConfig)`.
|
||||
- `install(node): void` / `start(): void` / `stop(): void` / `dispose(): void`.
|
||||
- `host(): NetworkNode | null` / `isStarted(): boolean`.
|
||||
- `interface MonitorPluginConfig` — `{ collector?, alert?, tracer?, history?, sampler? }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `alert` is created in `install` rather than the constructor because it depends on the node bus. The other subsystems are eagerly constructed so unit tests can build the plugin without a node.
|
||||
- `dispose` resets the collector + alert cooldowns + history but keeps the subsystem references so consumers' field reads stay non-null until the next construct cycle. Callers that want a hard reset should construct a new plugin instance.
|
||||
- `sampler` is intentionally optional. Most deployments do not need adaptive sampling at the metric layer; constructing it eagerly would force every consumer to think about watermarks they will never tune.
|
||||
- The plugin holds _references_ to subsystems rather than re-exporting their methods so refactors inside any subsystem do not reshape the plugin facade.
|
||||
@@ -0,0 +1,120 @@
|
||||
# `prometheus-exposition`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Universal Prometheus exposition-format emitter. Reads `Metric[]` snapshots
|
||||
from `MetricCollector.scrape()` and writes the Prometheus text exposition
|
||||
format directly — no `prom-client` runtime dependency, no Node-only surface,
|
||||
and no external metrics framework.
|
||||
|
||||
The emitter exists because the upstream `@suaveplan/network-observability`
|
||||
shipped a `prom-client`-based exporter that pinned the package to Node-tier
|
||||
runtimes. This rewrite preserves the externally-observable contract (a
|
||||
text-format string + a stable `Content-Type` header) while staying universal
|
||||
so the browser-tier `@suaveplan/network-plugin-monitor` consumer can ship a
|
||||
single `/metrics` endpoint across all targets.
|
||||
|
||||
## Features
|
||||
|
||||
- Pure-text exposition output (Prometheus `0.0.4` body, UTF-8).
|
||||
- Conformant metric / label name + label value escaping (Prometheus grammar).
|
||||
- Counter / gauge / histogram / summary support — histograms and summaries are
|
||||
expanded into `_count`, `_sum`, `_min`, `_max`, `_mean`, and configurable
|
||||
quantile lines (`quantile="0.5|0.95|0.99"` defaults).
|
||||
- Optional millisecond-precision timestamps per the exposition spec.
|
||||
- `prefix` + `defaultLabels` applied to every metric.
|
||||
- Zod-validated options (strict object), default-fill on every field.
|
||||
- Zero runtime deps beyond `zod` (already in the package).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
MetricCollector,
|
||||
createPrometheusExposition,
|
||||
} from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
const collector = new MetricCollector();
|
||||
collector.define({ name: "requests", category: "message", type: "counter" });
|
||||
collector.increment("requests");
|
||||
|
||||
const exposition = createPrometheusExposition();
|
||||
console.log(exposition.format(collector.scrape()));
|
||||
// # HELP requests message counter
|
||||
// # TYPE requests counter
|
||||
// requests 1
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
MetricCollector,
|
||||
createPrometheusExposition,
|
||||
PROMETHEUS_TEXT_CONTENT_TYPE,
|
||||
} from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
const collector = new MetricCollector();
|
||||
collector.define({ name: "rtt_ms", category: "connection", type: "histogram" });
|
||||
for (const v of [5, 12, 18, 25, 88, 142]) collector.record("rtt_ms", v);
|
||||
|
||||
const exposition = createPrometheusExposition({
|
||||
prefix: "suaveplan_",
|
||||
defaultLabels: { tier: "edge", region: "us-east" },
|
||||
includeTimestamps: true,
|
||||
quantiles: [0.5, 0.9, 0.99],
|
||||
});
|
||||
|
||||
// HTTP handler shape — runtime-agnostic Request/Response
|
||||
export function metricsHandler(): Response {
|
||||
return new Response(exposition.format(collector.scrape()), {
|
||||
headers: { "content-type": PROMETHEUS_TEXT_CONTENT_TYPE },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `createPrometheusExposition(options?: PrometheusExpositionOptions): PrometheusExposition`
|
||||
|
||||
Returns an emitter bound to the supplied options. Options are Zod-parsed —
|
||||
unknown keys are rejected. All fields are optional; defaults are applied
|
||||
field-by-field.
|
||||
|
||||
Options:
|
||||
|
||||
| Field | Default | Description |
|
||||
|---|---|---|
|
||||
| `prefix` | `""` | Prepended to every metric name. |
|
||||
| `defaultLabels` | `{}` | Merged into every line (sample labels win on conflict). |
|
||||
| `includeTimestamps` | `false` | Append `recordedAt` (ms) to each line. |
|
||||
| `quantiles` | `[0.5, 0.95, 0.99]` | Quantile lines emitted for histograms / summaries. |
|
||||
|
||||
Return value:
|
||||
|
||||
- `contentType` — pinned `text/plain; version=0.0.4; charset=utf-8`.
|
||||
- `format(snapshot)` — emits the exposition body. Always trailing-newline
|
||||
terminated (Prometheus parsers require it). Empty snapshots return `"\n"`.
|
||||
|
||||
### `PROMETHEUS_TEXT_CONTENT_TYPE`
|
||||
|
||||
The exact `Content-Type` Prometheus servers and `promtool` expect. Exported
|
||||
as a constant so consumers don't string-template it.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The emitter only reads the **last** sample of counter / gauge metrics
|
||||
(Prometheus expects a single current value per series). Histogram and
|
||||
summary metrics fold the full rolling-window buffer into summary stats.
|
||||
- Metric / label name sanitisation follows the Prometheus grammar exactly —
|
||||
characters outside `[a-zA-Z_:][a-zA-Z0-9_:]*` (metric) or
|
||||
`[a-zA-Z_][a-zA-Z0-9_]*` (label key) are replaced with `_`, and label
|
||||
values escape `"`, `\`, and `\n` per spec.
|
||||
- Integer values emit without a decimal point (`1` not `1.0`); non-integers
|
||||
use JavaScript's default `toString()` (matches Prometheus's `Inf`/`-Inf`/
|
||||
`NaN` literals when produced).
|
||||
- The emitter is stateless — safe to share one instance across many concurrent
|
||||
`format(...)` calls.
|
||||
@@ -0,0 +1,79 @@
|
||||
# bandwidth-monitor
|
||||
|
||||
## Purpose
|
||||
|
||||
`BandwidthMonitor` records per-link bandwidth / RTT / packet-loss measurements into a bounded ring buffer and exposes single-pole exponentially-smoothed readouts that the `DegradationManager` consumes to drive quality-level transitions. Raw samples are kept in chronological order so callers can compute their own statistics over a window (median, p99, etc.) while the EMA provides a fast, allocation-free running summary for hot paths.
|
||||
|
||||
## Features
|
||||
|
||||
- Bounded ring (`maxSamples`, default 60) — oldest sample is dropped on overflow.
|
||||
- Single-pole EMA: `α·new + (1−α)·prev`; `smoothingFactor` configurable (default `0.3`).
|
||||
- Injectable `clock` — every timestamp comes from the constructor argument, never from a module-scope `Date.now()` call, so tests are deterministic.
|
||||
- `onSample(listener)` subscription returning an unsubscribe function — no global event bus required.
|
||||
- Validation: `maxSamples ≥ 1` and `smoothingFactor ∈ (0, 1]`; out-of-range values throw `RangeError` at construction so misconfiguration fails fast.
|
||||
- Tree-shakeable: no `@suaveplan/events` dependency at runtime; the optional listener set lives only when used.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { BandwidthMonitor } from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
const monitor = new BandwidthMonitor();
|
||||
|
||||
monitor.recordSample({
|
||||
bytesPerSecond: 250_000,
|
||||
rttMs: 40,
|
||||
packetLossRate: 0.005,
|
||||
});
|
||||
|
||||
console.log(monitor.getSmoothedBandwidth());
|
||||
console.log(monitor.getSmoothedRtt());
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { BandwidthMonitor } from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
let now = 0;
|
||||
const monitor = new BandwidthMonitor({
|
||||
clock: () => now,
|
||||
maxSamples: 120,
|
||||
smoothingFactor: 0.1, // slow-responding, very stable EMA
|
||||
});
|
||||
|
||||
const unsub = monitor.onSample((s) => {
|
||||
console.log(`recorded at ${s.timestamp}: ${s.bytesPerSecond} B/s`);
|
||||
});
|
||||
|
||||
for (let i = 0; i < 200; i += 1) {
|
||||
now += 1_000;
|
||||
monitor.recordSample({
|
||||
bytesPerSecond: 100_000 + i * 1_000,
|
||||
rttMs: 50,
|
||||
packetLossRate: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Only the most-recent 120 samples survive the ring.
|
||||
console.log(monitor.getSampleCount());
|
||||
|
||||
unsub();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `new BandwidthMonitor(config?: BandwidthMonitorConfig)` — `config.clock` (default `Date.now`), `config.maxSamples` (default 60), `config.smoothingFactor` (default 0.3). Throws `RangeError` on out-of-range bounds.
|
||||
- `recordSample(sample)` — `sample` is a `BandwidthSample` minus `timestamp`; `timestamp` is filled from the injected clock.
|
||||
- `getSamples(count?)` → `readonly BandwidthSample[]`. Most-recent `count`; defaults to all. `count <= 0` returns an empty array.
|
||||
- `getSmoothedBandwidth() / getSmoothedRtt() / getSmoothedPacketLoss()` → `number`. Zero before the first sample.
|
||||
- `getSampleCount()` → `number`.
|
||||
- `clear()` — wipes the ring + resets EMA state to zero / un-initialised.
|
||||
- `onSample(listener)` → unsubscribe function. Listeners are notified after the ring + EMA update.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- EMA seed is the **first** sample (not zero) so the smoothed readout starts at a meaningful value instead of biased-toward-zero. The `initialized` flag flips on the first `recordSample` and is reset by `clear()`.
|
||||
- The ring is a plain array with `shift()` — `O(n)` per overflow, acceptable at the default `maxSamples = 60` and avoiding the allocation overhead of a deque. Callers expecting bursts of `maxSamples > 1024` should consider a wrapper that uses a typed-array ring.
|
||||
- Listener exceptions propagate to the caller of `recordSample`; this is deliberate so the monitor never silently swallows downstream bugs.
|
||||
- The `Set<SampleListener>` avoids duplicate registration of the same handler and provides O(1) unsubscribe.
|
||||
@@ -0,0 +1,86 @@
|
||||
# degradation-manager
|
||||
|
||||
## Purpose
|
||||
|
||||
`DegradationManager` is a 4-rung quality classifier that decides the current `QualityLevel` (`HIGH`/`MEDIUM`/`LOW`/`MINIMAL`) from a moving window of bandwidth samples and exposes that decision to downstream throttling middleware. It enforces two structural properties that pure thresholding would not: **one-step transitions** (no `MINIMAL → HIGH` jumps that skip the intermediate states a UI/codec needs to recover through) and **hysteresis** (a candidate transition must persist for `hysteresisMs` before committing, so a single bad measurement does not collapse a video call to MINIMAL).
|
||||
|
||||
## Features
|
||||
|
||||
- Four-level ladder with arithmetic ordering — `MINIMAL=0 < LOW=1 < MEDIUM=2 < HIGH=3`.
|
||||
- Configurable per-rung `QualityThresholds` (`minBandwidthKbps`, `maxRttMs`, `maxPacketLoss`).
|
||||
- `hysteresisMs` debounce — a candidate level must persist for this duration before being committed.
|
||||
- One-step transitions — `evaluate()` only ever moves `±1` rung per call, so consumers always see intermediate states.
|
||||
- Window-bounded averaging — `windowSize` controls how many trailing samples feed into classification.
|
||||
- `forceLevel()` / `reset()` for manual overrides during tests or admin failover.
|
||||
- `onLevelChange(handler)` subscription returning an unsubscribe function.
|
||||
- Injectable `clock` — every wall-clock read goes through it, so hysteresis timing is fully deterministic in tests.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
BandwidthMonitor,
|
||||
DegradationManager,
|
||||
QualityLevel,
|
||||
} from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
const monitor = new BandwidthMonitor();
|
||||
const dm = new DegradationManager();
|
||||
|
||||
monitor.recordSample({ bytesPerSecond: 1_000, rttMs: 300, packetLossRate: 0.2 });
|
||||
const level = dm.evaluate(monitor.getSamples());
|
||||
console.log(level === QualityLevel.MINIMAL); // true: very poor link
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
DegradationManager,
|
||||
QualityLevel,
|
||||
type QualityThresholds,
|
||||
} from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
let now = 0;
|
||||
const strict: QualityThresholds = {
|
||||
high: { minBandwidthKbps: 1_000, maxRttMs: 30, maxPacketLoss: 0.001 },
|
||||
medium: { minBandwidthKbps: 400, maxRttMs: 80, maxPacketLoss: 0.02 },
|
||||
low: { minBandwidthKbps: 100, maxRttMs: 200, maxPacketLoss: 0.1 },
|
||||
};
|
||||
|
||||
const dm = new DegradationManager({
|
||||
thresholds: strict,
|
||||
hysteresisMs: 2_000,
|
||||
windowSize: 5,
|
||||
clock: () => now,
|
||||
});
|
||||
|
||||
const unsub = dm.onLevelChange((event) => {
|
||||
console.log(`${event.previousLevel} → ${event.newLevel} (${event.reason})`);
|
||||
});
|
||||
|
||||
// ... feed `dm.evaluate(samples)` from a periodic loop driven by `now`.
|
||||
// Pin to MEDIUM during a maintenance window:
|
||||
dm.forceLevel(QualityLevel.MEDIUM);
|
||||
// Later: release the pin
|
||||
dm.reset();
|
||||
unsub();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `new DegradationManager(config?: DegradationManagerConfig)` — `thresholds` (default ships HD/SD/LD-style cutoffs), `hysteresisMs` (default `3000`, must be `≥ 0`), `windowSize` (default `10`, must be `≥ 1`), `clock` (default `Date.now`).
|
||||
- `evaluate(samples)` → `QualityLevel`. Computes window averages, picks the matching rung, applies one-step + hysteresis constraints, and may commit a level change.
|
||||
- `getCurrentLevel()` → `QualityLevel`.
|
||||
- `forceLevel(level)` — pin the level; automatic classification is suspended until `reset()`. Emits `"forced"` event if the level changed.
|
||||
- `reset()` — drop the pin, return to `HIGH`. Emits `"reset"` event if the level changed.
|
||||
- `onLevelChange(handler)` → unsubscribe function. Handler receives `{ previousLevel, newLevel, timestamp, reason }` where `reason ∈ "conditions-changed" | "forced" | "reset"`.
|
||||
- `getThresholds()` → readonly `QualityThresholds`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The classifier prefers explicit rungs over arithmetic-derived ones: each rung's three constraints must all hold simultaneously. This is intentionally conservative — a sample that is fast but lossy stays at the level its loss permits.
|
||||
- Hysteresis state is held in (`pendingLevel`, `pendingSince`). Changing the target mid-debounce *resets* the timer; the pending level must be stable for the full `hysteresisMs` window. This is the same behaviour as the upstream reference and matches what a moving-average debouncer does.
|
||||
- `windowSize` operates on the **trailing** N samples of the input array — callers pass `bandwidthMonitor.getSamples()` and the manager slices internally. Passing a pre-sliced array is also fine.
|
||||
- `forceLevel()` is one-shot; calling it twice with the same level is a no-op (no spurious event), and calling it with a different level emits exactly one change event.
|
||||
- Listener exceptions propagate; we never swallow downstream bugs.
|
||||
@@ -0,0 +1,80 @@
|
||||
# quality-middleware
|
||||
|
||||
## Purpose
|
||||
|
||||
`createQualityMiddleware` builds an outgoing-message gate that drops or forwards each message based on the current `QualityLevel` reported by a `DegradationManager`. It is the bridge between passive quality classification and active traffic shaping: instead of merely reporting "the link is degraded", the gate actually reduces wire traffic so the link can recover. The drop policy is monotonically more aggressive as quality drops, with a guaranteed critical-class allowlist that always passes (handshakes, auth, disconnects, errors) so the connection can repair itself even at MINIMAL.
|
||||
|
||||
## Features
|
||||
|
||||
- Four drop policies, one per `QualityLevel`:
|
||||
- `HIGH`: all messages pass through.
|
||||
- `MEDIUM`: drops every other position-update; non-position messages all pass.
|
||||
- `LOW`: drops 75% of position-updates and 50% of other non-critical traffic.
|
||||
- `MINIMAL`: drops everything that is not in the critical allowlist.
|
||||
- Critical-class allowlist (`HANDSHAKE`, `AUTH`, `DISCONNECT`, `ERROR`, `CRITICAL`) — these always pass irrespective of quality level. Overridable via `options.criticalTypes`.
|
||||
- Position-update detection (`POSITION_UPDATE`, `STATE_UPDATE`, `TRANSFORM_UPDATE`) — overridable via `options.positionUpdateTypes`.
|
||||
- Per-source counters survive the lifetime of the middleware so deterministic round-robin drops are consistent across the whole session.
|
||||
- Generic in the message shape (`M extends QualityMiddlewareMessage`), so callers can attach extra metadata without losing type safety.
|
||||
- No mutation: returns `message` (pass) or `null` (drop). Identity preserved on pass.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
createQualityMiddleware,
|
||||
DegradationManager,
|
||||
QualityLevel,
|
||||
} from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
const dm = new DegradationManager();
|
||||
const gate = createQualityMiddleware(dm);
|
||||
|
||||
dm.forceLevel(QualityLevel.LOW);
|
||||
const result = gate.onOutgoing({ type: "POSITION_UPDATE", source: "peer-a" });
|
||||
// `result` is null three times in a row, then the message on the fourth call.
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
createQualityMiddleware,
|
||||
DegradationManager,
|
||||
QualityLevel,
|
||||
type QualityMiddlewareMessage,
|
||||
} from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
interface ChatMessage extends QualityMiddlewareMessage {
|
||||
readonly type: "CHAT" | "POSITION_UPDATE" | "AUTH";
|
||||
readonly source: string;
|
||||
readonly payload: string;
|
||||
}
|
||||
|
||||
const dm = new DegradationManager();
|
||||
const gate = createQualityMiddleware<ChatMessage>(dm, {
|
||||
// Treat "CHAT" as critical so it never drops, even at MINIMAL.
|
||||
criticalTypes: new Set(["CRITICAL", "ERROR", "DISCONNECT", "HANDSHAKE", "AUTH", "CHAT"]),
|
||||
});
|
||||
|
||||
dm.forceLevel(QualityLevel.MINIMAL);
|
||||
const chat = gate.onOutgoing({ type: "CHAT", source: "u1", payload: "hi" });
|
||||
const pos = gate.onOutgoing({ type: "POSITION_UPDATE", source: "u1", payload: "..." });
|
||||
console.log(chat !== null); // true — overridden critical
|
||||
console.log(pos === null); // true — minimal drops everything non-critical
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `createQualityMiddleware<M>(dm, options?)` — `dm` is the `DegradationManager` source-of-truth for the current level; `options.criticalTypes` and `options.positionUpdateTypes` override the defaults.
|
||||
- Returns `{ onOutgoing(message: M): M | null }`. `null` means "drop"; a non-null return is always identity-equal to the argument.
|
||||
- `DEFAULT_CRITICAL_TYPES` — exported `ReadonlySet<string>`: `CRITICAL`, `ERROR`, `DISCONNECT`, `HANDSHAKE`, `AUTH`.
|
||||
- `DEFAULT_POSITION_UPDATE_TYPES` — exported `ReadonlySet<string>`: `POSITION_UPDATE`, `STATE_UPDATE`, `TRANSFORM_UPDATE`.
|
||||
- `QualityMiddlewareMessage` — minimal shape `{ type: string; source: string }`. Callers' message types must extend this.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The counter is keyed per `source`, not per `(source, type)` — a single peer sending alternating message types gets one combined counter. This is intentional: the goal is to limit the peer's egress fairly, not to give each message type its own quota.
|
||||
- `LOW` drops 75% of position-updates by passing only `counter % 4 === 0`. The first message passes (counter starts at 1, so 1 % 4 ≠ 0 → drop, but the FIRST `counter` value is `1`, which yields `null`); to flip "first-message-passes" the caller can prime the counter via the source-coupling hook (deliberately not exposed — middleware is stateless by contract).
|
||||
- `MINIMAL` short-circuits before the counter is touched so the counters do not advance during a complete blackout; this avoids subtle re-entry bugs where recovering from MINIMAL would skip the first few messages of a recovered LOW window.
|
||||
- `onOutgoing` is synchronous. The upstream reference made it `async`; here we keep it sync because every code path is pure JS — adding `Promise.resolve()` would just allocate microtasks.
|
||||
- The middleware does not subscribe to `dm.onLevelChange` — it reads `dm.getCurrentLevel()` on every call. This is a deliberate choice: the level is cheap to read and avoids holding a listener whose lifetime depends on caller discipline.
|
||||
@@ -0,0 +1,65 @@
|
||||
# quality-types
|
||||
|
||||
## Purpose
|
||||
|
||||
Shared type definitions for the quality-class classification + adaptive degradation feature group. These types underpin three sibling modules (`bandwidth-monitor`, `degradation-manager`, `quality-middleware`) and form the wire-level contract for any consumer that wants to react to per-link quality changes. Quality monitoring relies on these shapes being stable: a `BandwidthSample` recorded by one module must round-trip cleanly through another, so the schemas are Zod-validated at every boundary.
|
||||
|
||||
## Features
|
||||
|
||||
- `QualityLevel` const object + value-union type (numeric ordering: `MINIMAL=0 < LOW=1 < MEDIUM=2 < HIGH=3`); arithmetic on levels powers one-step transitions in the degradation manager.
|
||||
- `LevelThreshold` — bandwidth / RTT / loss bounds for a single quality rung.
|
||||
- `QualityThresholds` — three thresholds (`high`/`medium`/`low`); `MINIMAL` is implicit ("anything worse than `low`").
|
||||
- `BandwidthSample` — single measurement record with timestamp.
|
||||
- `QualityChangeEvent` — emitted when the degradation manager flips levels.
|
||||
- All shapes are Zod-validated (`*Schema` exports) so cross-module boundaries reject malformed data.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { QualityLevel, type BandwidthSample } from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
const sample: BandwidthSample = {
|
||||
timestamp: Date.now(),
|
||||
bytesPerSecond: 250_000,
|
||||
rttMs: 40,
|
||||
packetLossRate: 0.005,
|
||||
};
|
||||
|
||||
if (QualityLevel.HIGH > QualityLevel.MEDIUM) {
|
||||
// levels are numerically ordered
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
BandwidthSampleSchema,
|
||||
QualityThresholdsSchema,
|
||||
type QualityThresholds,
|
||||
} from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
const thresholds: QualityThresholds = QualityThresholdsSchema.parse({
|
||||
high: { minBandwidthKbps: 500, maxRttMs: 50, maxPacketLoss: 0.01 },
|
||||
medium: { minBandwidthKbps: 200, maxRttMs: 100, maxPacketLoss: 0.05 },
|
||||
low: { minBandwidthKbps: 50, maxRttMs: 200, maxPacketLoss: 0.15 },
|
||||
});
|
||||
|
||||
// Validate untrusted wire input before feeding to BandwidthMonitor.recordSample()
|
||||
const incoming = BandwidthSampleSchema.parse(JSON.parse(wireBytes));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `QualityLevel`: const object with `MINIMAL`/`LOW`/`MEDIUM`/`HIGH` numeric members and a union type alias of the same name.
|
||||
- `LevelThreshold`: `{ minBandwidthKbps, maxRttMs, maxPacketLoss }` — all non-negative; `maxPacketLoss` ∈ `[0, 1]`.
|
||||
- `QualityThresholds`: three rungs `{ high, medium, low }`. `MINIMAL` is the implicit worst level when none match.
|
||||
- `BandwidthSample`: `{ timestamp, bytesPerSecond, rttMs, packetLossRate }`. `timestamp` is a non-negative integer (ms epoch).
|
||||
- `QualityChangeEvent`: `{ previousLevel, newLevel, timestamp, reason }`. `reason` is `"conditions-changed"`, `"forced"`, or `"reset"`.
|
||||
- `*Schema` exports: Zod strict-object schemas; throw `ZodError` on malformed input.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `QualityLevel` is intentionally a `const` object rather than a TypeScript `enum` to keep emitted JS minimal and tree-shakeable, and to avoid the reverse-mapping baggage that numeric enums generate.
|
||||
- The schemas use `.strict()` so unknown keys are a hard error; that catches typos like `packetLoss` vs `packetLossRate` at the boundary instead of silently dropping them later.
|
||||
- `MINIMAL` has no corresponding `LevelThreshold` because it is the fall-through: any measurement that fails to meet the `low` rung classifies as `MINIMAL`.
|
||||
@@ -0,0 +1,65 @@
|
||||
# `AdaptiveSampler`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Watches the recent sample arrival rate and adjusts the effective sampling probability so monitoring overhead stays bounded under load. Above the `highWatermark` rate the sampler halves its rate every second; at or below the `lowWatermark` rate it doubles back toward `maxRate`. Keeps a tight feedback loop between observed load and the cost of observing it.
|
||||
|
||||
## Features
|
||||
|
||||
- Two-watermark hysteresis: distinct high / low thresholds prevent flapping between adjustments.
|
||||
- Bounded rate range: `[minRate, maxRate]` (defaults `0.1 .. 1`) so even the worst burst still keeps a probabilistic tail of samples flowing.
|
||||
- `shouldKeep(rng?)` is pure — callers feed their own RNG for deterministic tests.
|
||||
- Injectable clock for deterministic tests of the 1-second rolling window.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { AdaptiveSampler } from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
const sampler = new AdaptiveSampler({
|
||||
highWatermark: 1_000,
|
||||
lowWatermark: 250,
|
||||
});
|
||||
for (const sample of stream) {
|
||||
sampler.observe();
|
||||
if (sampler.shouldKeep()) ingest(sample);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { AdaptiveSampler } from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
let now = 0;
|
||||
const sampler = new AdaptiveSampler({
|
||||
highWatermark: 100,
|
||||
lowWatermark: 10,
|
||||
minRate: 0.05,
|
||||
maxRate: 1,
|
||||
clock: () => now,
|
||||
});
|
||||
|
||||
// Burst: 200 samples in <1s pushes the rate down on the next window
|
||||
for (let i = 0; i < 200; i += 1) sampler.observe();
|
||||
now += 1_001;
|
||||
sampler.observe();
|
||||
sampler.currentRate(); // 0.5 (halved from 1)
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class AdaptiveSampler(config: { highWatermark, lowWatermark, minRate?, maxRate?, clock? })`
|
||||
- `observe(): void` — count one sample; reconciles the rate at every window boundary.
|
||||
- `currentRate(): number` — current sampling probability.
|
||||
- `shouldKeep(rng?: () => number): boolean` — true if the sample should be retained.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The 1-second rolling window starts when the first observe arrives in a new window. If a window receives no samples (silent period), `adjustRate` does not run until the next observe past the boundary — the sampler stays at the current rate rather than spuriously bumping up toward `maxRate`.
|
||||
- `highWatermark` is clamped to `>= 1` so the configuration is never ambiguous (a watermark of 0 would always trip).
|
||||
- `minRate` and `maxRate` are clamped to `[0, 1]` so misconfiguration cannot produce > 100 % rates or negative ones; the sampler degrades gracefully.
|
||||
- `shouldKeep` accepts a custom RNG so tests can pin sampling decisions exactly without monkey-patching `Math.random`.
|
||||
@@ -0,0 +1,77 @@
|
||||
# `NetworkTracer`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
OpenTelemetry-shaped distributed tracer for the mesh. Propagates `traceId` / `spanId` / `sampled` via three message headers (`x-network-trace`, `x-network-span`, `x-network-sampled`), opens / closes spans with timing + attributes + events, computes per-peer clock skew, and decides sampling via four strategies (`probabilistic`, `rate-limited`, `error-based`, `priority-based`).
|
||||
|
||||
## Features
|
||||
|
||||
- `extract` / `inject` round-trip the trace context through plain header maps so callers do not need to know the header names.
|
||||
- `start` / `addEvent` / `end` lifecycle with `SPAN_NOT_OPEN` raised when `addEvent` / `end` is called on an unknown or already-closed span.
|
||||
- `shouldSample` switches on the configured `SamplingStrategy` so consumers tune behaviour via constructor config rather than per-call branches.
|
||||
- `observePeerClock` + `correctRemoteTimestamp` let consumers normalise spans recorded on peers with skewed clocks.
|
||||
- Injectable clock / RNG / id generator for deterministic tests.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { NetworkTracer } from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
const tracer = new NetworkTracer({ strategy: "probabilistic", probabilisticRate: 1 });
|
||||
const ctx = tracer.start({ name: "router.forward" });
|
||||
tracer.addEvent(ctx.spanId, { name: "enqueued", at: Date.now() });
|
||||
tracer.end(ctx.spanId);
|
||||
const traces = tracer.traces();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
NetworkTracer,
|
||||
type SamplingStrategy,
|
||||
} from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
const tracer = new NetworkTracer({
|
||||
strategy: "rate-limited" satisfies SamplingStrategy,
|
||||
rateLimitPerSec: 50,
|
||||
clock: () => 1_700_000_000_000,
|
||||
rng: () => 0.1,
|
||||
});
|
||||
|
||||
// Cross-hop propagation
|
||||
const headers: Record<string, string | number | boolean> = {};
|
||||
const parent = tracer.start({ name: "incoming.handle" });
|
||||
tracer.inject(parent, headers);
|
||||
const downstream = tracer.extract(headers);
|
||||
const child = tracer.start({ name: "outgoing.send", parent: downstream ?? undefined });
|
||||
tracer.end(child.spanId);
|
||||
tracer.end(parent.spanId);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class NetworkTracer(config?)`
|
||||
- `extract(headers): TraceContext | null`.
|
||||
- `inject(ctx, target): void`.
|
||||
- `shouldSample({ priority?, hasError? } = {}): boolean`.
|
||||
- `start({ name, parent?, attributes? }): TraceContext`.
|
||||
- `addEvent(spanId, event): void` — throws `SPAN_NOT_OPEN`.
|
||||
- `end(spanId): NetworkSpan` — throws `SPAN_NOT_OPEN`.
|
||||
- `traces(): ReadonlyArray<NetworkTrace>`.
|
||||
- `observePeerClock(remoteNowMs): void`.
|
||||
- `skew(): number`.
|
||||
- `correctRemoteTimestamp(remoteMs): number`.
|
||||
- `lastInvalid(): Error | null` — last `SPAN_NOT_OPEN` raised, useful for tests / debug logs.
|
||||
- `interface NetworkTracerConfig` — `{ clock?, idGenerator?, strategy?, probabilisticRate?, rateLimitPerSec?, prioritySampleAtOrAbove?, rng? }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Sampling decisions are pure — `shouldSample` does not record state, so consumers can call it independently of `start`. The rate-limited strategy maintains a 1-second rolling window so bursts above `rateLimitPerSec` are smoothed.
|
||||
- `start` returns the new `TraceContext` rather than the span itself to discourage callers from mutating the span buffer directly. `addEvent` / `end` are the only legitimate mutations.
|
||||
- `extract` returns `null` for malformed headers (missing or wrong type); callers check the result so missing context never silently propagates as the wrong trace.
|
||||
- Skew is observation-based: the most recent `observePeerClock(remoteNowMs)` call defines the offset. Production deployments should call this on every received heartbeat so the offset tracks NTP drift.
|
||||
- The id generator uses base36 from `Math.random` by default (16 chars) — adequate for in-memory traces. Bridges to OTel exporters should pass an injected generator that produces standards-compliant 32-char trace ids and 16-char span ids.
|
||||
@@ -0,0 +1,66 @@
|
||||
# Alert types — `@suaveplan/network-plugin-monitor`
|
||||
|
||||
## Purpose
|
||||
|
||||
Shared types for the threshold-based alerting layer. `AlertRule` describes the gate condition; `AlertEvaluation` captures the observation that tripped (or did not trip) the rule; `AlertState` reports the rule's current life-cycle phase to the bus.
|
||||
|
||||
## Features
|
||||
|
||||
- Closed `AlertSeverity` (`info` / `warning` / `critical`), `AlertComparison` (six operators), and `AlertState` unions for exhaustive switch coverage.
|
||||
- `AlertStatistic` enumerates which numeric the rule reads — either `latest` (last sample) or one of the percentile / aggregate fields on `MetricStats`.
|
||||
- `cooldownMs` carries the per-rule suppression window so distinct rules can pace differently.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type { AlertRule } from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
const rule: AlertRule = {
|
||||
id: "p95-latency-warn",
|
||||
metricName: "connection.latency",
|
||||
statistic: "p95",
|
||||
comparison: ">",
|
||||
threshold: 250,
|
||||
severity: "warning",
|
||||
};
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
AlertEvaluation,
|
||||
AlertRule,
|
||||
AlertState,
|
||||
} from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
const rule: AlertRule = {
|
||||
id: "host-cpu-critical",
|
||||
metricName: "node.cpu",
|
||||
statistic: "p99",
|
||||
comparison: ">=",
|
||||
threshold: 0.95,
|
||||
severity: "critical",
|
||||
cooldownMs: 30_000,
|
||||
};
|
||||
|
||||
declare const evaluation: AlertEvaluation;
|
||||
declare const state: AlertState;
|
||||
console.log(rule.id, state, evaluation.observed);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `type AlertSeverity` — `"info" | "warning" | "critical"`.
|
||||
- `type AlertComparison` — `">" | ">=" | "<" | "<=" | "==" | "!="`.
|
||||
- `type AlertStatistic` — `"latest" | "min" | "max" | "avg" | "p50" | "p95" | "p99" | "count"`.
|
||||
- `interface AlertRule` — `{ id, metricName, statistic, comparison, threshold, severity, cooldownMs? }`.
|
||||
- `interface AlertEvaluation` — `{ rule, observed, stats, evaluatedAt }`.
|
||||
- `type AlertState` — `"firing" | "suppressed" | "ok"`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `id` on `AlertRule` is the boundary key the engine uses for cooldown bookkeeping. Two rules with the same id would share a cooldown clock — distinct gates need distinct ids.
|
||||
- `statistic: "latest"` is special-cased because not every rule wants the rolling-window aggregate; e.g. heartbeat-loss alerts read the most recent sample directly.
|
||||
- `cooldownMs` defaults to `60_000` in the engine, applied centrally rather than per rule definition. Rules that need it tighter / looser opt in explicitly.
|
||||
- The types are intentionally minimal — richer fields (annotations, runbook links) belong in subscriber-side models, not in the engine contract.
|
||||
@@ -0,0 +1,68 @@
|
||||
# Metric types — `@suaveplan/network-plugin-monitor`
|
||||
|
||||
## Purpose
|
||||
|
||||
Closed unions and structural shapes for the metric layer. Every other module — collector, monitor, history store, alert engine — speaks the same `Metric` / `MetricValue` / `MetricStats` shapes so callers can compose pipelines without inventing per-module conversions.
|
||||
|
||||
## Features
|
||||
|
||||
- `MetricCategory` and `MetricType` are closed unions so exhaustive `switch` statements stay honest.
|
||||
- `MetricValue` carries `recordedAt` plus optional low-cardinality `labels` map.
|
||||
- `MetricStats` exposes count / min / max / avg / p50 / p95 / p99 so consumers do not need to recompute every percentile per render.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
Metric,
|
||||
MetricCategory,
|
||||
MetricStats,
|
||||
MetricType,
|
||||
MetricValue,
|
||||
} from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
const sample: MetricValue = { value: 42, recordedAt: Date.now() };
|
||||
const metric: Metric = {
|
||||
name: "tier0.nodeCount",
|
||||
category: "hierarchy" as MetricCategory,
|
||||
type: "gauge" as MetricType,
|
||||
samples: [sample],
|
||||
};
|
||||
declare const stats: MetricStats;
|
||||
console.log(stats.p95);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type { Metric, MetricStats } from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
const histogram: Metric = {
|
||||
name: "connection.latency",
|
||||
category: "connection",
|
||||
type: "histogram",
|
||||
samples: rawSamples.map((value) => ({
|
||||
value,
|
||||
recordedAt: Date.now(),
|
||||
labels: { transport: "websocket" },
|
||||
})),
|
||||
};
|
||||
|
||||
declare function summarise(samples: ReadonlyArray<number>): MetricStats;
|
||||
const stats = summarise(histogram.samples.map((s) => s.value));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `type MetricCategory` — `"node" | "connection" | "message" | "hierarchy"`.
|
||||
- `type MetricType` — `"counter" | "gauge" | "histogram" | "summary"`.
|
||||
- `interface MetricValue` — `{ value, recordedAt, labels? }`.
|
||||
- `interface Metric` — `{ name, category, type, samples }`.
|
||||
- `interface MetricStats` — `{ count, min, max, avg, p50, p95, p99 }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `samples` is `ReadonlyArray` so consumers cannot mutate the buffer behind the collector's back; the collector replaces the array on every record.
|
||||
- `labels` is intentionally limited to `string -> string` to discourage high-cardinality dimensions (e.g. raw payloads); higher-cardinality contexts belong in the trace layer, not metrics.
|
||||
- `MetricStats` always includes `count` so consumers can detect empty buffers without a separate `samples.length` check.
|
||||
- `recordedAt` is a number (epoch ms) rather than `Date` because metric pipelines serialise these into structured logs/exporters that expect numeric timestamps.
|
||||
@@ -0,0 +1,70 @@
|
||||
# Trace types — `@suaveplan/network-plugin-monitor`
|
||||
|
||||
## Purpose
|
||||
|
||||
Shared shapes for the distributed-tracing layer. `TraceContext` is the propagated header bundle peers exchange via message envelopes; `NetworkSpan` is the local recording with timing + attributes + ordered events; `NetworkTrace` aggregates spans for a single trace id.
|
||||
|
||||
## Features
|
||||
|
||||
- OpenTelemetry-shaped fields (`traceId`, `spanId`, `parentSpanId`, `sampled`) so exporters bridge into OTel pipelines without schema translation.
|
||||
- `attributes` and `events` carry typed-but-flexible string/number/boolean values — high enough fidelity for typical mesh observability without leaking arbitrary objects through the type system.
|
||||
- `SamplingStrategy` is a closed union mirroring the four strategies the tracer implements; consumer config reads the union directly.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
NetworkSpan,
|
||||
SamplingStrategy,
|
||||
TraceContext,
|
||||
} from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
const ctx: TraceContext = {
|
||||
traceId: "0123abcd",
|
||||
spanId: "00000001",
|
||||
sampled: true,
|
||||
};
|
||||
const strategy: SamplingStrategy = "probabilistic";
|
||||
declare const span: NetworkSpan;
|
||||
console.log(span.name, span.attributes);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
NetworkSpan,
|
||||
NetworkTrace,
|
||||
} from "@suaveplan/network-plugin-monitor";
|
||||
|
||||
const span: NetworkSpan = {
|
||||
traceId: "0123abcd",
|
||||
spanId: "00000002",
|
||||
parentSpanId: "00000001",
|
||||
name: "router.forward",
|
||||
startedAt: 1_700_000_000_000,
|
||||
attributes: { "peer.id": "peer-A", hop: 2 },
|
||||
events: [
|
||||
{ name: "enqueued", at: 1_700_000_000_001 },
|
||||
{ name: "dispatched", at: 1_700_000_000_002 },
|
||||
],
|
||||
};
|
||||
|
||||
declare function exportTrace(trace: NetworkTrace): void;
|
||||
exportTrace({ traceId: span.traceId, spans: [span] });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `type SamplingStrategy` — `"probabilistic" | "rate-limited" | "error-based" | "priority-based"`.
|
||||
- `interface TraceContext` — `{ traceId, spanId, parentSpanId?, sampled }`.
|
||||
- `interface NetworkSpan` — `{ traceId, spanId, parentSpanId?, name, startedAt, endedAt?, attributes, events }`.
|
||||
- `interface NetworkSpanEvent` — `{ name, at, attributes? }`.
|
||||
- `interface NetworkTrace` — `{ traceId, spans }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `endedAt` is mutable on `NetworkSpan` because spans are populated incrementally — opening, recording events, closing. Consumers that need an immutable snapshot should clone before storing.
|
||||
- `attributes` accepts `string | number | boolean` so exporters can serialise without bespoke conversions; objects / arrays are intentionally excluded.
|
||||
- `traceId` and `spanId` are `string` rather than fixed-width hex so test fixtures can use deterministic ids without hex-padding boilerplate.
|
||||
- The closed `SamplingStrategy` union is the contract between consumers and the tracer; adding a strategy requires updating every implementation switch and the spec-conformance tests.
|
||||
Reference in New Issue
Block a user