Testing
Release rolling / release (push) Successful in 25s
CI / test (push) Successful in 28s

This commit is contained in:
Raven Scott
2026-07-18 16:33:43 -04:00
parent f747ffbd25
commit f7e26d1aac
53 changed files with 6491 additions and 740 deletions
+6 -1
View File
@@ -22,7 +22,12 @@ PEARDATA_DEFAULT_ROLE=viewer
# PEARDATA_TIER1_POINTS=1440
# PEARDATA_TIER1_EVERY=60
# ── Netdata-style REST API ───────────────────────────────────
# ── HyperDB (warm history + metadata + linked sync) ──────────
# PEARDATA_HYPERDB=1
# PEARDATA_SWARM=0
# See docs/STORAGE-HYPERDB.md
# ── agent-style REST API ───────────────────────────────────
# PEARDATA_REST=1
# PEARDATA_REST_HOST=127.0.0.1
# PEARDATA_REST_PORT=19999
+2
View File
@@ -9,6 +9,8 @@ coverage/
.pear/
storage/
data/
tmp-test-data/
tmp-hyperdb-test/
*.seed
.cache/
tmp/
+11 -6
View File
@@ -1,8 +1,8 @@
# PearData
**Decentralized, P2P, Netdata-class real-time monitoring for the Pear / Holepunch ecosystem.**
**Decentralized, P2P, real-time monitoring for the Pear / Holepunch ecosystem.**
Each machine runs a lightweight **PearMonitor agent**. A Pear desktop client dials agents by public key and shows fleet + per-node live dashboards. Agents also expose a **Netdata-compatible REST API** (`/api/v1`, `/api/v2`, `/api/v3`) for scripts, Grafana, and Prometheus.
Each machine runs a lightweight **PearMonitor agent**. A Pear desktop client dials agents by public key and shows fleet + per-node live dashboards. Agents also expose an **agent-compatible REST API** (`/api/v1`, `/api/v2`, `/api/v3`) for scripts, Grafana, and Prometheus.
Built from the [pear-app-template](https://github.com/snxraven) HyperDHT + protomux-rpc patterns used by PearDock-class apps (MIT template — not a copy of PearDocks AGPL sources).
@@ -12,7 +12,7 @@ Built from the [pear-app-template](https://github.com/snxraven) HyperDHT + proto
| RPC | **protomux-rpc** + compact-encoding JSON |
| Metrics | 1s collector → tiered buffers → push + query |
| AuthZ | Roles (`viewer` / `operator` / `admin`) + `pd1.` invites + admin seed |
| HTTP | Netdata-style REST on `127.0.0.1:19999` |
| HTTP | agent-style REST on `127.0.0.1:19999` |
| Desktop | **Pear** (`pear-electron` + `pear-bridge` + `<pear-ctrl>`) |
---
@@ -54,12 +54,15 @@ npm run healthcheck
peardata/
├── app.js / index.html / ui/ # Pear fleet dashboard
├── shared/ # Protocol, metrics catalog, schema, crypto
├── spec/ # Generated Hyperschema + HyperDB definitions
├── scripts/build-db.js # Regenerate spec/ (`npm run build:db`)
├── server/ # PearMonitor agent
│ ├── server.js # DHT + pipeline + REST boot
│ ├── server.js # DHT + HyperDB + pipeline + REST boot
│ ├── pipeline.js # collector → store → anomaly → push
│ ├── db/ # HyperDB model + Corestore + swarm replicate
│ ├── handlers/monitor.js # Domain RPCs
│ ├── services/ # collector, store, anomaly, jobs, …
│ ├── rest/ # Netdata-compatible HTTP API
│ ├── services/ # collector, store, warm-flush, anomaly, jobs
│ ├── rest/ # agent-compatible HTTP API
│ └── core/ rpc/ utils/ # Auth, ACL, sessions (template)
├── client/ # Multi-peer connection manager
├── docs/ # Full documentation set
@@ -77,6 +80,7 @@ peardata/
| `npm run start:server` | PearMonitor agent (P2P + REST) |
| `npm test` | brittle unit + integration |
| `npm run mint-invite -- [role]` | Offline `pd1.` invite |
| `npm run build:db` | Regenerate HyperDB `spec/` |
| `npm run healthcheck` | Liveness / remote ping |
| `npm run soak` | Load exercise |
@@ -104,6 +108,7 @@ peardata/
| [Data model](./docs/DATA-MODEL.md) | Metrics, anomalies, health |
| [REST API](./docs/REST-API.md) | `/api/v1\|v2\|v3` |
| [Tech choices](./docs/TECH-CHOICES.md) | Collector, charts, libraries |
| [HyperDB storage](./docs/STORAGE-HYPERDB.md) | Warm history, peer links, swarm sync |
| [Security](./docs/SECURITY.md) | Threat model & hardening |
| [Configuration](./docs/CONFIGURATION.md) | Environment reference |
+11 -2
View File
@@ -1,5 +1,14 @@
#!/usr/bin/env node
/**
* Server binary entry (symlink-friendly).
* PearMonitor agent entry for Node and Bare.
*
* Under Bare: load bare-node-runtime globals (process, Buffer, fetch, …) first.
* package.json `imports` routes Node builtin names → bare-* when packed for Bare/Pear.
*/
import '../server/server.js'
const isBare = typeof globalThis.Bare !== 'undefined'
if (isBare) {
await import('bare-node-runtime/global')
}
await import('../server/server.js')
+14 -4
View File
@@ -1,6 +1,8 @@
/**
* Persistent client DHT identity for stable peerId across reconnects.
* Stored at ~/.config/peardata/identity.json (mode 0600).
*
* Uses fs/path/os/crypto via package.json import maps → bare-* under Bare/Pear.
*/
import fs from 'fs'
import path from 'path'
@@ -11,11 +13,19 @@ import b4a from 'b4a'
const IDENTITY_VERSION = 1
function envGet(name) {
try {
return typeof process !== 'undefined' ? process.env?.[name] : undefined
} catch {
return undefined
}
}
export function getIdentityPath() {
const home =
process.env.PEARDATA_HOME ||
process.env.HOME ||
process.env.USERPROFILE ||
envGet('PEARDATA_HOME') ||
envGet('HOME') ||
envGet('USERPROFILE') ||
(typeof os.homedir === 'function' ? os.homedir() : '') ||
''
return path.join(home, '.config', 'peardata', 'identity.json')
@@ -37,7 +47,7 @@ export function loadOrCreateClientIdentity() {
}
if (!seedHex) {
seedHex = crypto.randomBytes(32).toString('hex')
seedHex = b4a.toString(crypto.randomBytes(32), 'hex')
try {
const dir = path.dirname(filePath)
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
+4 -1
View File
@@ -14,7 +14,10 @@ export class ConnectionManager extends EventEmitter {
this.active = null
/** @type {Map<string, ReturnType<typeof setTimeout>>} */
this._reconnectTimers = new Map()
this.maxReconnectTries = Number(process.env.PEARDATA_MAX_RECONNECT) || 20
this.maxReconnectTries =
Number(
(typeof process !== 'undefined' && process.env?.PEARDATA_MAX_RECONNECT) || 20
) || 20
/** @type {Map<string, number>} */
this._tries = new Map()
}
+1 -1
View File
@@ -7,7 +7,7 @@ Wants=network-online.target
Type=simple
WorkingDirectory=/opt/peardata
EnvironmentFile=-/opt/peardata/.env
ExecStart=/usr/bin/node /opt/peardata/server/server.js
ExecStart=/usr/bin/node /opt/peardata/bin/peardata-server.mjs
ExecStartPost=/usr/bin/node /opt/peardata/scripts/healthcheck.js
Restart=on-failure
RestartSec=3
+13 -7
View File
@@ -1,15 +1,16 @@
# Architecture
PearData is a **decentralized, P2P clone of the Netdata real-time monitoring experience**, built on the same HyperDHT + protomux-rpc patterns as PearDock-class apps (via the pear-app template).
PearData is a **decentralized, P2P real-time host monitoring stack**, built on the same HyperDHT + protomux-rpc patterns as PearDock-class apps (via the pear-app template).
## Design goals
1. **No central control plane** — dial an agent by Ed25519 public key.
2. **Instant live truth** — ~1s metric push to connected desktops.
3. **Dual API** — P2P RPC for the Pear client; Netdata-style REST for scripts/Grafana.
3. **Dual API** — P2P RPC for the Pear client; agent-style REST for scripts/Grafana.
4. **Clear AuthZ** — viewer (pubkey) vs operator/admin (`pd1.` invite / seed proof).
5. **Low agent overhead**Node/`os` + `/proc` collectors, ring buffers, hot-path RPCs.
6. **Ecosystem-ready**shared wire contract in `shared/` for PearDock / PearVirt adapters later.
5. **Low agent overhead**`os`/`bare-os` + `/proc` collectors, ring buffers, hot-path RPCs.
6. **Bare/Pear-ready**`package.json` import maps route builtins → `bare-*`; no Node-only core deps in the Pear path.
7. **Ecosystem-ready** — shared wire contract in `shared/` for PearDock / PearVirt adapters later.
## Mapping to PearDock / template components
@@ -23,7 +24,7 @@ PearData is a **decentralized, P2P clone of the Netdata real-time monitoring exp
| Connection manager / multi-peer | `client/manager.js` |
| Job tray | `server/services/jobs.js` + desktop actions |
| Domain service | **Metrics pipeline** (`collector``store``anomaly` → pushes) |
| Optional HTTP surface | `server/rest/*` (Netdata v1/v2/v3) |
| Optional HTTP surface | `server/rest/*` (REST v1/v2/v3) |
## System context
@@ -67,7 +68,7 @@ Threshold engine evaluates each batch; transitions emit `push:anomaly` / `push:a
### 4. Local REST (optional)
Netdata-compatible HTTP on `127.0.0.1:19999` by default — for local tooling without P2P. Disable with `PEARDATA_REST=0`.
agent-compatible HTTP on `127.0.0.1:19999` by default — for local tooling without P2P. Disable with `PEARDATA_REST=0`.
## Layered stack
@@ -152,8 +153,13 @@ A heavier agent may subscribe to child agents over P2P, downsample into its own
| `server/services/subscriptions.js` | Push fan-out |
| `server/services/jobs.js` | On-demand jobs |
| `server/handlers/monitor.js` | RPC surface |
| `server/rest/*` | Netdata HTTP API |
| `server/rest/*` | Agent HTTP API |
| `server/pipeline.js` | Wire collector→store→push |
| `server/db/*` | HyperDB model, Corestore, swarm replicate |
| `spec/` | Generated Hyperschema + HyperDB defs |
| `scripts/build-db.js` | Schema codegen |
HyperDB design: [STORAGE-HYPERDB.md](./STORAGE-HYPERDB.md).
## Related docs
+21 -3
View File
@@ -57,17 +57,34 @@ Permissions: directory `0700`. Do **not** commit `data/` or `.env`.
| `PEARDATA_SAMPLE_MS` | `1000` | Collector interval |
| `PEARDATA_TIER0_POINTS` | `3600` | High-res ring size (~1h @ 1s) |
| `PEARDATA_TIER1_POINTS` | `1440` | Downsampled ring size |
| `PEARDATA_TIER1_EVERY` | `60` | Samples per tier1 average |
| `PEARDATA_TIER1_EVERY` | `60` | Samples per tier1 average (also HyperDB warm flush) |
---
## REST API (Netdata-style)
## HyperDB storage & sync
| Variable | Default | Description |
|----------|---------|-------------|
| `PEARDATA_HYPERDB` | on | `0` / `off` disables HyperDB |
| `PEARDATA_SWARM` | off | `1` enables Hyperswarm Corestore replication |
Storage: `$PEARDATA_DATA_DIR/corestore` (named core `peardata-meta`).
Full guide: [STORAGE-HYPERDB.md](./STORAGE-HYPERDB.md).
```bash
npm run build:db # regenerate spec/ after schema edits
```
---
## REST API (agent-style)
| Variable | Default | Description |
|----------|---------|-------------|
| `PEARDATA_REST` | on | Set `0` / `off` to disable HTTP API |
| `PEARDATA_REST_HOST` | `127.0.0.1` | Bind address (`0.0.0.0` exposes LAN — firewall!) |
| `PEARDATA_REST_PORT` | `19999` | HTTP port (Netdata classic) |
| `PEARDATA_REST_PORT` | `19999` | HTTP port (classic local agent port) |
| `PEARDATA_REST_CORS` | `*` | `Access-Control-Allow-Origin` |
See [REST-API.md](./REST-API.md).
@@ -104,6 +121,7 @@ See [REST-API.md](./REST-API.md).
| `npm run start:server:bin` | `bin/peardata-server.mjs` |
| `npm test` | brittle suite |
| `npm run mint-invite -- [role] [ttlMs]` | Offline `pd1.` invite |
| `npm run build:db` | Regenerate HyperDB `spec/` |
| `npm run healthcheck` | Liveness |
| `npm run soak` | Load exercise |
+23 -12
View File
@@ -26,20 +26,31 @@ Implemented in `shared/data-model.js`, `shared/metrics.js`, and agent services.
## Metric contexts & charts
Netdata-inspired IDs:
Canonical catalog lives in `shared/metrics.js` (`STATIC_CHART_DEFS` + runtime instance charts). Collector fills these from `/proc` + `/sys` on Linux (`os` / bare-os fallback elsewhere).
| Context | Chart id | Units | Dimensions (MVP) |
|---------|----------|-------|------------------|
| `system.cpu` | `system.cpu` | percentage | user, system, nice, iowait, irq, softirq, idle |
| `system.ram` | `system.ram` | MiB | used, cached, buffers, free |
| `mem.available` | `mem.available` | MiB | avail |
| `system.load` | `system.load` | load | load1, load5, load15 |
| `system.io` | `system.io` | KiB/s | reads, writes |
| `system.net` | `system.net` | kilobits/s | received, sent |
| `system.processes` | `system.processes` | processes | running, blocked, total |
| `system.uptime` | `system.uptime` | seconds | uptime |
### Host-wide (static)
Chart summary objects mirror Netdatas `/api/v1/charts` fields (`id`, `context`, `units`, `dimensions`, `update_every`, `first_entry`, `last_entry`, …).
| Family | Contexts / charts | Units (typical) |
|--------|-------------------|-----------------|
| CPU / scheduler | `system.cpu`, `system.intr`, `system.ctxt`, `system.forks`, `system.processes`, `system.active_processes`, `system.load`, `system.uptime`, `system.entropy` | %, interrupts/s, load, seconds |
| Memory | `system.ram`, `mem.available`, `mem.swap`, `mem.swap_cached`, `mem.kernel`, `mem.slab`, `mem.writeback`, `mem.committed`, `mem.swapio`, `system.pgpgio`, `system.pgfaults` | MiB, KiB/s, faults/s |
| Disk aggregate | `system.io` | KiB/s (`in` / `out`) |
| Network aggregate | `system.net`, `system.ip`, `system.ipv6` | kilobits/s |
| TCP / IPv4 | `ip.tcppackets`, `ip.tcperrors`, `ip.tcpopens`, `ip.tcpsock`, `ipv4.packets`, `ipv4.errors`, `ipv4.udppackets`, `ipv4.udperrors` | packets/s, connections |
| PSI pressure | `system.cpu_some_pressure`, `system.memory_some_pressure`, `system.io_some_pressure` | % (avg10/60/300) |
`system.cpu` dimensions: `guest_nice`, `guest`, `steal`, `softirq`, `irq`, `user`, `system`, `nice`, `iowait`, `idle`.
### Instance charts (registered at runtime)
| Context | Chart id pattern | Notes |
|---------|------------------|-------|
| `cpu.cpu` | `cpu.cpu{N}` | Per-core utilization (same dims as `system.cpu`) |
| `disk.io` / `disk.ops` / `disk.util` | `disk_io.{dev}`, `disk_ops.{dev}`, `disk_util.{dev}` | Per-disk from `/proc/diskstats` |
| `disk.space` / `disk.inodes` | `disk_space.{mount}`, `disk_inodes.{mount}` | Per-mount via `statfs` |
| `net.net` / `net.packets` / `net.errors` / `net.drops` | `net.{iface}`, … | Per-iface from `/proc/net/dev` (loopback skipped) |
Chart summary objects use fields: `id`, `context`, `units`, `dimensions`, `update_every`, `first_entry`, `last_entry`, …
## MetricSample (live push)
+3 -3
View File
@@ -2,9 +2,9 @@
## Add a chart / context
1. Define the chart in `shared/metrics.js` (`CHART_DEFS`).
1. Define the chart in `shared/metrics.js` (`STATIC_CHART_DEFS`, or `registerChart()` for instances).
2. Emit samples from `server/services/collector.js` (or a new collector module).
3. Store + REST/RPC pick it up automatically via `CHART_BY_ID`.
3. Store + REST/RPC pick it up automatically via `getAllChartDefs()` / `CHART_BY_ID`.
4. Document dimensions in [DATA-MODEL.md](./DATA-MODEL.md).
5. Optionally add a canvas panel in `index.html` + `app.js`.
@@ -40,7 +40,7 @@ Update [PROTOCOL.md](./PROTOCOL.md) and add a brittle test.
## Add a REST route
Edit `server/rest/routes.js` — keep Netdata path naming when emulating Agent APIs (`/api/v3/...`).
Edit `server/rest/routes.js` — keep stable `/api/v3/...` path naming.
## Add a job
+16
View File
@@ -44,6 +44,22 @@ curl -s 'http://127.0.0.1:19999/api/v3/allmetrics?format=prometheus' | head
Disable REST: `PEARDATA_REST=0`.
### HyperDB (warm storage)
On by default. Agent banner shows `hyperdb:` public key.
```bash
curl -s http://127.0.0.1:19999/api/v3/db | jq
```
Warm history is written about once per minute (tier1). Linked-node Hyperswarm sync:
```bash
PEARDATA_SWARM=1 npm run start:server
```
See [STORAGE-HYPERDB.md](./STORAGE-HYPERDB.md).
## Mint an invite
```bash
+13 -2
View File
@@ -48,12 +48,12 @@ Auth modes at handshake: public key (viewer), capability token / `pd1.` invite,
|--------|------|------|--------|
| `listContexts` | viewer | — | Context catalog |
| `getContext` | viewer | `{ id }` | Charts in context |
| `listCharts` | viewer | — | Netdata-ish chart map |
| `listCharts` | viewer | — | agent-style chart map |
| `getChart` | viewer | `{ id }` | Chart summary |
| `queryData` | viewer | `{ chart, after, before, points, group, tier }` | Time series |
| `getAllMetrics` | viewer | `{ format: json\|prometheus\|shell }` | Latest export |
`after` / `before`: absolute unix seconds, or relative (negative = relative to `before`/`now`), Netdata-style.
`after` / `before`: absolute unix seconds, or relative (negative = relative to `before`/`now`), agent-style.
### Live subscriptions
@@ -82,6 +82,17 @@ Auth modes at handshake: public key (viewer), capability token / `pd1.` invite,
| `runJob` | operator | `collectOnce`, `snapshot`, `gcBuffers` |
| `cancelJob` | operator | By job id |
### HyperDB / peer links
| Method | Role | Notes |
|--------|------|-------|
| `getDbInfo` | viewer | DB + discovery keys, swarm flag |
| `listPeerLinks` | viewer | Linked peers from HyperDB |
| `linkPeer` | admin | Upsert link; optional swarm join |
| `unlinkPeer` | admin | Remove link |
See [STORAGE-HYPERDB.md](./STORAGE-HYPERDB.md).
### Admin
| Method | Role |
+2 -2
View File
@@ -3,11 +3,11 @@
| Doc | Audience | Contents |
|-----|----------|----------|
| [GETTING-STARTED.md](./GETTING-STARTED.md) | Operators | Install, run agent, REST, desktop, systemd |
| [ROADMAP.md](./ROADMAP.md) | Everyone | Phased MVP → advanced Netdata-class features |
| [ROADMAP.md](./ROADMAP.md) | Everyone | Phased MVP → advanced real-time features |
| [ARCHITECTURE.md](./ARCHITECTURE.md) | Engineers | Planes, PearDock mapping, module map |
| [PROTOCOL.md](./PROTOCOL.md) | Protocol owners | RPC methods, pushes, versioning |
| [DATA-MODEL.md](./DATA-MODEL.md) | Engineers | Metrics, anomalies, alerts, jobs |
| [REST-API.md](./REST-API.md) | Integrators | Netdata-style `/api/v1\|v2\|v3` |
| [REST-API.md](./REST-API.md) | Integrators | agent-style `/api/v1\|v2\|v3` |
| [TECH-CHOICES.md](./TECH-CHOICES.md) | Engineers | Collector, charts, libraries |
| [STORAGE-HYPERDB.md](./STORAGE-HYPERDB.md) | Engineers | HyperDB / Corestore / Autobase sync design |
| [DESKTOP.md](./DESKTOP.md) | UI developers | Pear shell, titlebar, identity |
+16 -15
View File
@@ -1,12 +1,12 @@
# REST API (Netdata-compatible)
# REST API (agent-compatible)
PearMonitor agents expose an optional HTTP API modeled on **Netdata Agent** endpoints (`/api/v1`, `/api/v2`, `/api/v3`).
PearMonitor agents expose an optional HTTP API for local tooling (`/api/v1`, `/api/v2`, `/api/v3`).
Default bind: `http://127.0.0.1:19999` (Netdatas classic port).
Default bind: `http://127.0.0.1:19999`.
Disable: `PEARDATA_REST=0`.
Bind all interfaces (careful): `PEARDATA_REST_HOST=0.0.0.0`.
This is a **compatibility layer**, not a byte-for-byte Netdata clone. Core query/metadata paths are implemented for scripts, Grafana, and Prometheus scrapers.
Core query/metadata paths are implemented for scripts, Grafana, and Prometheus scrapers.
## Quick examples
@@ -36,10 +36,11 @@ curl -s http://127.0.0.1:19999/api/v3/health | jq
| Method | Path | Notes |
|--------|------|-------|
| GET | `/` or `/api` | Service index + P2P pubkey |
| GET | `/` or `/api` | Service index + P2P pubkey + HyperDB keys |
| GET | `/api/v1/info` | Agent info |
| GET | `/api/v2/info` | same |
| GET | `/api/v3/info` | **preferred** |
| GET | `/api/v3/info` | **preferred** (includes `peardata.hyperdb`) |
| GET | `/api/v3/db` | HyperDB public/discovery keys + collections |
| GET | `/api/v3/versions` | Agent / protocol / API versions |
| GET | `/api/v3/me` | Anonymous REST identity note |
| GET | `/api/v3/settings` | Runtime knobs |
@@ -75,7 +76,7 @@ Single-agent MVP returns one node (this host). Parent/fleet aggregation is roadm
| GET | `/api/v2/data` | same |
| GET | `/api/v1/data` | same (legacy) |
**Params (Netdata-style)**
**Params (agent-style)**
| Param | Default | Description |
|-------|---------|-------------|
@@ -121,7 +122,7 @@ Single-agent MVP returns one node (this host). Parent/fleet aggregation is roadm
## Auth model (REST)
- **Default:** localhost-only, no bearer required (like a typical Netdata agent bind).
- **Default:** localhost-only, no bearer required (like a typical local agent bind).
- **P2P remains the secure remote path** (Noise + roles).
- If you bind `0.0.0.0`, put REST behind a firewall, reverse proxy, or Holesail tunnel — do not expose raw metrics to the internet.
- Future: optional bearer gate (`/api/v3/bearer_protection` parity).
@@ -130,15 +131,15 @@ Single-agent MVP returns one node (this host). Parent/fleet aggregation is roadm
`Access-Control-Allow-Origin` defaults to `*` (override with `PEARDATA_REST_CORS`).
## Compatibility notes
## Scope notes
| Netdata | PearData MVP |
|---------|--------------|
| Full ML weights / metric correlations | Simplified health weights |
| Area | PearData |
|------|----------|
| ML weights / metric correlations | Simplified health weights |
| Multi-node parent streaming | Single node; parent planned |
| Cloud POST `/api/v3/spaces/.../data` | Not implemented (agent GET style only) |
| Every chart Netdata ships | Core system charts (expanding) |
| Functions execute via HTTP | Listed; run via P2P jobs |
| Cloud POST spaces APIs | Not implemented (agent GET style only) |
| App/plugin charts (nginx, DB, …) | System/OS charts; plugins later |
| Functions via HTTP | Listed; run via P2P jobs |
## Implementation
+7 -8
View File
@@ -1,13 +1,13 @@
# PearData Roadmap
Phased plan from MVP agent → Netdata-class fleet observability on pure P2P.
Phased plan from MVP agent → real-time fleet observability on pure P2P.
## Guiding principles
1. **Instant value** — connect a pubkey, see live charts in seconds.
2. **Agent efficiency** — stay in Netdatas ballpark for CPU/RAM overhead.
2. **Agent efficiency** — stay in a tight ballpark for CPU/RAM overhead.
3. **PearDock patterns** — HyperDHT identity, protomux-rpc, roles, `pd1.` invites.
4. **Dual access** — P2P desktop + Netdata-style REST (`/api/v1|v2|v3`).
4. **Dual access** — P2P desktop + agent-style REST (`/api/v1|v2|v3`).
5. **Ecosystem glue** — ready for PearDock / PearVirt / HoneyPeer / BareOS later.
---
@@ -18,11 +18,11 @@ Phased plan from MVP agent → Netdata-class fleet observability on pure P2P.
|------|--------|
| Copy pear-app-template → PearData rebrand (`pd1.`, `PEARDATA_*`, `peardata/rpc`) | Done |
| Shared protocol + schema + metrics catalog | Done |
| Agent collector (CPU/RAM/load/net/disk/processes) @ ~1s | Done |
| Agent collector (full system/OS charts + per-cpu/disk/iface/mount) @ ~1s | Done |
| In-memory tiered store (1s + downsample) | Done |
| Threshold anomaly engine + health | Done |
| P2P RPC surface (query, subscribe, alerts, jobs, ACL) | Done |
| Netdata-compatible REST v1/v2/v3 (local :19999) | Done |
| agent-compatible REST v1/v2/v3 (local :19999) | Done |
| Pear desktop fleet overview + live canvas charts | Done |
| Docs: architecture, protocol, data model, REST, roadmap, tech choices | Done |
| systemd unit + CI skeletons | Done |
@@ -43,7 +43,6 @@ Phased plan from MVP agent → Netdata-class fleet observability on pure P2P.
- Multi-peer compare mode (overlay 24 nodes on one chart)
- Reconnection / offline banners with last-known samples
- Agent process title / `peardata-agent` binary naming polish
- Expand collectors: per-core CPU, per-iface net, mount disk space
- Alert silence TTL auto-reenable
- brittle tests for collector, store query, REST routes
- One-line install script (`curl | bash`) for Linux agents
@@ -96,8 +95,8 @@ See **[STORAGE-HYPERDB.md](./STORAGE-HYPERDB.md)** for the full design (from Hol
## Non-goals (for now)
- Replacing Netdata Cloud SaaS multi-tenant product
- Full byte-identical Netdata internal DB format
- Replacing centralized multi-tenant SaaS monitoring products
- Third-party proprietary on-disk metric DB formats
- Shipping a browser-only public dashboard without auth by default (REST stays localhost unless explicitly bound)
---
+1 -1
View File
@@ -31,7 +31,7 @@
## REST API exposure
- Default bind is **localhost only** (`127.0.0.1:19999`).
- REST is intentionally open on that bind (Netdata-agent style) — **do not** set `PEARDATA_REST_HOST=0.0.0.0` without a firewall, reverse proxy, or Holesail tunnel.
- REST is intentionally open on that bind (local-agent style) — **do not** set `PEARDATA_REST_HOST=0.0.0.0` without a firewall, reverse proxy, or Holesail tunnel.
- Prefer **P2P + roles** for remote multi-operator access; use REST for local scrapers/Grafana.
## Production checklist
+256 -238
View File
@@ -1,290 +1,308 @@
# HyperDB storage & linked-node sync
How PearData should adopt Holepunchs **HyperDB + Corestore + Hyperswarm (+ Autobase)** stack for durable storage and P2P sync between linked agents — based on patterns in local clones under `holepunchto_repos` (`hyperdb`, `hyperdb-workshop`, `hyperdb-autobase-workshop`, `corestore`, `hyperswarm`, `autobee`, `pear-hyperdb`).
PearData uses Holepunch **HyperDB + Corestore (+ optional Hyperswarm)** for durable metadata, warm metric history, and P2P sync between linked agents.
## Why HyperDB (not “just SQLite”)
Live 1-second samples stay in the **memory ring** and are pushed over protomux-rpc. HyperDB is for everything that must survive restarts and replicate.
| Need | HyperDB fit |
|------|-------------|
| Typed collections + indexes | Hyperschema + `@ns/collection` + secondary indexes |
| Local high-perf | `HyperDB.rocks(path, def)` |
| P2P replicate | `HyperDB.bee(hypercore, def, { autoUpdate })` over Corestore |
| Multi-writer HA parents | Autobase whose **view** is HyperDB (`extension: false`) |
| Same query API local + remote | Workshops prove one `Registry` class works for both |
---
PearDatas current `server/services/store.js` is an **in-memory ring**. HyperDB replaces durability + sync; the in-memory tier stays as the **hot 1s path**.
## Status (implemented)
## Stack mapping (from Holepunch repos)
| Piece | Status | Location |
|-------|--------|----------|
| Schema codegen | ✅ | `scripts/build-db.js``spec/` |
| Collections + indexes | ✅ | `@peardata/*` in `spec/hyperdb` |
| `PearDataModel` | ✅ | `server/db/model.js` |
| Corestore open/close | ✅ | `server/db/index.js` |
| Warm flush from tier1 | ✅ | `server/services/warm-flush.js` + `store` `warm` event |
| Query fallback (memory → HyperDB) | ✅ | `MetricStore.query()` |
| Agent boot upsert node | ✅ | `server/server.js` |
| RPC: `getDbInfo`, `linkPeer`, `unlinkPeer`, `listPeerLinks` | ✅ | `server/handlers/monitor.js` |
| REST: `GET /api/v3/db` | ✅ | `server/rest/routes.js` |
| Hyperswarm replicate | ✅ stub | `server/db/replicate.js` (`PEARDATA_SWARM=1`) |
| Autobase multi-writer parents | ⏳ Phase C | See roadmap |
Disable HyperDB: `PEARDATA_HYPERDB=0`.
---
## Architecture
```mermaid
flowchart TB
subgraph Agent
COL[collector 1s] --> HOT[Memory tier0 ring]
COL --> DS[Downsample]
DS --> HDB[(HyperDB bee/rocks)]
POL[peer-policy / alerts / links] --> HDB
HOT --> RPC[protomux-rpc + REST]
HDB --> RPC
end
subgraph Sync
CS[Corestore] --> HDB
SW[Hyperswarm] -->|store.replicate| CS
AB[Autobase optional] -->|view| HDB
end
CHILD[Linked child agent] -.->|discoveryKey| SW
PARENT[Parent / peer agent] -.-> SW
DESK[Pear desktop cache] -.-> SW
COL[collector 1s] --> MEM[Memory tier0 ring]
COL --> T1[Memory tier1 downsample]
T1 -->|warm event| FLUSH[warm-flush queue]
FLUSH --> HDB[(HyperDB.bee peardata-meta)]
ANO[anomaly engine] -->|alert events| HDB
LINK[linkPeer RPC] --> HDB
MEM --> PUSH[push:metrics RPC]
MEM --> Q[queryData / REST]
HDB --> Q
CS[Corestore data/corestore] --> HDB
SW[Hyperswarm PEARDATA_SWARM=1] -->|store.replicate| CS
```
| Component | Repo pattern | PearData use |
|-----------|--------------|--------------|
| **Hyperschema + hyperdb/builder** | `hyperdb-workshop/build.js` | `spec/` codegen for collections |
| **HyperDB.bee** | workshop `Registry` | Replicable agent DB |
| **HyperDB.rocks** | `hyperdb` README | Optional local-only fast index |
| **Corestore** | workshop `bin.js` | Named cores: `metrics-meta`, `alerts`, … |
| **Hyperswarm** | `swarm.join(discoveryKey)` + `store.replicate(conn)` | Link nodes / seed DB |
| **protomux-rpc** | already in PearData | Control plane (unchanged) |
| **Autobase + hyperdispatch** | `hyperdb-autobase-workshop` | Multi-writer **parent** / HA registry |
| **autobee** | experimental multiwriter bee | Alternative later; prefer Autobase+HyperDB view for now |
| **pear-hyperdb** | Pear-shaped Model wrapper | Optional UX for desktop-local rocks |
## Critical design rule: dont put 1s samples in HyperDB txs
HyperDB is an **indexable document DB** (put/get/find + flush). Writing every chart every second as HyperDB transactions will:
- Amplify Rocks/Bee write cost
- Create huge replication chatter
- Fight Netdata-class overhead goals
**Split planes:**
### Planes
| Plane | Storage | Sync |
|-------|---------|------|
| **Hot live (1h @ 1s)** | Memory ring (current) | P2P `push:metrics` (current) |
| **Warm history (downsampled)** | Hypercore append **or** HyperDB rows keyed `(chart, tsBucket)` | Corestore replicate |
| **Metadata** (peers, alerts, labels, jobs, ACL cache) | **HyperDB** | Corestore replicate |
| **Fleet / parent consensus** | Autobase → HyperDB view | Swarm on autobase discoveryKey |
| Hot live (~1h @ 1s) | Memory | `push:metrics` |
| Warm history (~1m buckets) | HyperDB `@peardata/metric-point` | Corestore replicate |
| Metadata / links / alerts | HyperDB collections | Corestore replicate |
| Fleet HA (future) | Autobase → HyperDB view | Swarm on autobase key |
## Proposed HyperDB schema (`@peardata/*`)
### Why not HyperDB for every 1s sample
Modeled after workshop `build.js` namespaces.
HyperDB is transactional + indexed. Flushing every chart every second would inflate write amplification and replication traffic. Tier1 downsample (default every 60 samples ≈ 1 minute) is the durable path.
---
## Schema (`@peardata`)
Defined in `scripts/build-db.js`. **Append-only** — never delete fields from committed `spec/` (Holepunch safety rule).
### Collections
```text
@peardata/node
key: nodeId (string / pubkey hex)
fields: hostname, platform, arch, cpus, agentVersion, labels{}, updatedAt
@peardata/peer-link
key: [localNodeId, remotePublicKey]
fields: role, alias, discoveryKey?, linkedAt, lastSeen, syncMode (push|pull|both)
@peardata/alert-config
key: id
fields: chart, dimension, warn, crit, comparator, enabled, info
@peardata/alert-event
key: [id, ts] # or ulid
fields: severity, value, threshold, message, cleared
@peardata/metric-point # WARM tier only (e.g. 1m buckets)
key: [chart, ts]
fields: context, values{} (map), tier
@peardata/job
key: id
fields: name, status, startedAt, finishedAt, result?
```
| Collection | Key | Purpose |
|------------|-----|---------|
| `@peardata/node` | `nodeId` | Agent / host inventory |
| `@peardata/peer-link` | `localNodeId` + `remotePublicKey` | Linked peers + sync mode |
| `@peardata/alert-config` | `id` | Threshold configs |
| `@peardata/alert-event` | `id` + `ts` | Anomaly / alert history |
| `@peardata/metric-point` | `chart` + `ts` | Warm downsampled samples (`valuesJson`) |
| `@peardata/job` | `id` | Persisted job records (optional use) |
### Indexes
```text
@peardata/node-by-hostname → node.hostname
@peardata/peer-link-by-remote → peer-link.remotePublicKey
@peardata/alert-event-by-chart → alert-event.chart + ts
@peardata/metric-point-by-context → metric-point.context + ts
```
| Index | On |
|-------|-----|
| `@peardata/node-by-hostname` | hostname |
| `@peardata/peer-link-by-remote` | remotePublicKey |
| `@peardata/alert-event-by-chart` | chart + ts |
| `@peardata/metric-point-by-context` | context + ts |
| `@peardata/metric-point-by-tier` | tier + chart + ts |
Rebuild with:
### Field notes
- `valuesJson` / `labelsJson` / `resultJson` — JSON strings for open-ended maps (avoids rigid hyperschema maps).
- `tier` on metric-point: `1` = default warm (~1m). Future coarser tiers use `2+`.
- `syncMode` on peer-link: `push` | `pull` | `both`.
### Regenerate after schema edits
```bash
node scripts/build-db.js # Hyperschema + HyperDB.toDisk → spec/
npm run build:db
# or: node scripts/build-db.js
```
## Agent integration shape
Then commit `spec/hyperschema/*` and `spec/hyperdb/*`.
Follow `hyperdb-workshop` / `pear-hyperdb` Model pattern:
---
## Runtime layout
```text
server/
db/
build.js # schema codegen
spec/hyperschema/
spec/hyperdb/
model.js # PearDataModel: putNode, linkPeer, queryWarm, …
replicate.js # Hyperswarm join + store.replicate
services/
store.js # HOT memory (keep)
store-hyperdb.js # WARM + metadata facade used by queryData/REST
$PEARDATA_DATA_DIR/ # default ./data
peer-policy.json # existing ACL file
audit.log
corestore/ # Corestore (HyperDB bee cores)
```
### Boot (single-writer agent — Phase 2)
Named core: **`peardata-meta`**.
```js
const store = new Corestore(dataDir + '/corestore')
const swarm = new Hyperswarm({ keyPair: await store.createKeyPair('swarm') })
swarm.on('connection', (conn) => store.replicate(conn))
Banner fields on agent start:
const metaCore = store.get({ name: 'peardata-meta' })
const db = HyperDB.bee(metaCore, spec, { autoUpdate: true })
- `hyperdb:` — DB public key (hex) for others to open a read replica
- `swarm:``on` / `off`
// announce for linked peers / desktop seeders
swarm.join(metaCore.discoveryKey, { server: true, client: true })
```
---
Collector path:
## Configuration
1. Ingest → memory tier0 (unchanged)
2. Every N samples → downsample → `tx.insert('@peardata/metric-point', …); tx.flush()`
3. Alert transitions → `alert-event` collection
4. `queryData` / REST: memory first, then HyperDB range scan for older windows
| Variable | Default | Meaning |
|----------|---------|---------|
| `PEARDATA_HYPERDB` | on | `0` / `off` disables HyperDB entirely |
| `PEARDATA_DATA_DIR` | `./data` | Policy + corestore root |
| `PEARDATA_SWARM` | off | `1` enables Hyperswarm `store.replicate` |
| `PEARDATA_TIER1_EVERY` | `60` | Samples per warm bucket (~60s @ 1Hz) |
| `PEARDATA_TIER1_POINTS` | `1440` | In-memory tier1 ring (HyperDB keeps longer) |
### Auth note
---
HyperDB replication shares **capability to read the core**, not PearData RPC roles. Keep:
## API surface
- **Noise + MethodRoles** for mutating RPC (`setAlertConfig`, `runJob`)
- Replication topic optionally gated (only invite-linked peers get discoveryKey / capability)
- Do **not** announce writable cores to the public swarm without encryption / allowlist
### RPC
## Linked nodes: sync modes
| Method | Role | Description |
|--------|------|-------------|
| `getDbInfo` | viewer | `{ enabled, publicKeyHex, discoveryKeyHex, swarm }` |
| `listPeerLinks` | viewer | Linked peers from HyperDB |
| `linkPeer` | admin | Upsert link; optionally `joinRemoteTopic` if swarm on |
| `unlinkPeer` | admin | Remove link |
| `queryData` | viewer | Memory first; HyperDB warm if miss / `tier≥1` |
PearData “links” are first-class `@peardata/peer-link` rows + swarm topics.
| Mode | Behavior |
|------|----------|
| **Pull** | Local agent opens remote DB by key (`HyperDB.bee(store.get({ key }), spec, { writable: false, autoUpdate: true })`) and replicates |
| **Push** | Remote peers allowed to replicate our meta/warm cores (seed) |
| **Both** | Mutual swarm join (homelab mesh) |
| **Parent aggregate** | Parent pulls many children; stores namespaced copies or Autobase fleet view |
Discovery:
1. Desktop/admin mints link → stores remote pubkey + optional `dbKey` (z32/hex)
2. Agent joins `discoveryKey` of that core
3. On connection: `store.replicate(conn)` (workshop pattern)
4. `clone.watch` / `autoUpdate` refreshes HyperDB indexes for REST/RPC queries
This is the same pattern as workshop §3.1 Lookups — **read path is swarm + HyperDB**, not constant RPC polling.
## Parent / HA (Phase 4) — Autobase workshop
When you need multi-writer fleet registry or HA parents:
- Autobase bootstrap key shared across parent instances
- `open: (store) => HyperDB.bee(store.get('db-view'), spec, { extension: false, autoUpdate: true })`
- hyperdispatch ops: `add-writer`, `put-alert`, `put-link`, `ingest-rollup`
- RPC (existing PearData / workshop) appends ops; apply mutates HyperDB view
- Clients dial **any** writer; view key stays stable
Do **not** “backup” by copying Corestore folders (workshop warning). Rotate writers via Autobase instead.
## What stays on protomux-rpc
| Keep on RPC | Why |
|-------------|-----|
| Live `push:metrics` | Sub-second UX; not DB-shaped |
| `subscribeMetrics` / handshake / ACL | Session security |
| `runJob`, mintInvite | Mutating control |
| On-demand `queryData` for hot window | Memory path |
| Move to HyperDB (+ replicate) | Why |
|-------------------------------|-----|
| Peer links, aliases, labels | Shared fleet truth |
| Alert config + history | Durable, queryable |
| Warm/cold metric buckets | History without central server |
| Node inventory | Parent `/api/v3/nodes` |
## Phased delivery (recommended)
### Phase A — Local durability (rocks or bee, no swarm yet)
1. Add `hyperdb`, `hyperschema`, `corestore` deps
2. `scripts/build-db.js` + `spec/`
3. Persist alert configs, peer policy, warm downsample into HyperDB
4. REST/RPC history falls back to HyperDB after memory miss
5. Keep REST localhost semantics
### Phase B — Link & replicate
1. Hyperswarm alongside HyperDHT RPC (or reuse connections carefully — often **separate swarm for store.replicate**)
2. `linkPeer` / `unlinkPeer` RPCs write `@peardata/peer-link`
3. Desktop can seed/cache agent DB by key for offline scrubbing
4. Document `pd1.` invite vs **db discovery key** (two layers)
### Phase C — Parent fleet
1. Parent process pulls N child DB keys
2. Composite REST `/api/v3/nodes` + scoped `/api/v3/data`
3. Optional Autobase for parent HA
### Phase D — Polish
1. Encryption at rest (`encryptionKey` on cores / autobee)
2. Blind-peer seeding for always-on warm history
3. Grafana: already have REST Prometheus export; optionally expose hypercore-stats
## Concrete file plan (when implementing)
```text
peardata/
scripts/build-db.js
spec/hyperschema/
spec/hyperdb/
server/db/model.js
server/db/replicate.js
server/services/store-hyperdb.js
docs/STORAGE-HYPERDB.md ← this file
```
Deps (approximate):
`linkPeer` args:
```json
"hyperdb": "^6",
"hyperschema": "^1",
"corestore": "^7",
"hyperswarm": "^4",
"autobase": "^7",
"hyperdispatch": "^1"
{
"remotePublicKey": "<64 hex>",
"alias": "homelab-nas",
"dbKeyHex": "<optional remote db key>",
"discoveryKeyHex": "<optional topic to pull>",
"syncMode": "both",
"role": "viewer"
}
```
(`autobee` only if you prefer that multiwriter path over Autobase+HyperDB view.)
### REST
## Relationship to current PearData roadmap
| Path | Description |
|------|-------------|
| `GET /api/v3/db` | HyperDB keys + collection list |
| `GET /api/v3/info` | Includes `peardata.hyperdb` block |
| `GET /api/v3/data?...` | Uses async store query (warm fallback) |
| Query `source` field | `memory-tier0` \| `memory-tier1` \| `hyperdb-warm` |
| Roadmap item | HyperDB role |
|--------------|--------------|
| Phase 2 tiered storage | Warm/cold collections |
| Phase 3 PearDock/containers | Extra collections / indexes |
| Phase 4 parent peer | Autobase + replicated views |
| REST v3 historical queries | `find` ranges on `@peardata/metric-point` |
```bash
curl -s http://127.0.0.1:19999/api/v3/db | jq
curl -s 'http://127.0.0.1:19999/api/v3/data?chart=system.cpu&after=-3600&tier=1&points=120' | jq '.source'
```
## References (local clones)
---
| Path under `holepunchto_repos` | Takeaway |
|--------------------------------|----------|
| `hyperdb/README.md` | rocks vs bee, find/get/tx, autoUpdate |
| `hyperdb-workshop` | Schema builder, Corestore+Swarm replicate, RPC inserts |
| `hyperdb-autobase-workshop` | Multi-writer view, hyperdispatch, HA |
| `corestore/README.md` | `store.replicate(conn)`, namespacing |
| `pear-hyperdb` | Thin Model wrapper pattern for Pear apps |
| `autobee` | Experimental multiwriter bee alternative |
## How linked-node sync works
## Decision summary
### Phase A (now) — local durability
1. **Yes — incorporate HyperDB** for metadata + warm history + linked-node sync.
2. **Keep memory + RPC pushes** for live 1s Netdata feel.
3. **Sync linked nodes via Corestore replication on Hyperswarm**, not by streaming every sample over RPC.
4. **Use Autobase+HyperDB** when you need multi-writer parents / HA — same pattern as Holepunchs own workshops.
5. **Treat discovery keys as sensitive** — link only invited peers; RPC AuthZ remains authoritative for mutations.
1. Agent opens Corestore + HyperDB on boot.
2. Warm points + alert events persist under `data/corestore`.
3. Restart retains warm history / links / node row.
### Phase B — mesh replicate (`PEARDATA_SWARM=1`)
Workshop pattern (`hyperdb-workshop/bin.js`):
```js
swarm.on('connection', (conn) => store.replicate(conn))
swarm.join(db.discoveryKey, { server: true, client: true })
```
1. Enable swarm on agents that should seed/pull.
2. Share **db public key** + **discovery key** (`getDbInfo` / `/api/v3/db`).
3. Admin calls `linkPeer` with `discoveryKeyHex` + `syncMode: pull|both`.
4. Peers replicate Corestore; HyperDB `autoUpdate` refreshes indexes.
5. Remote warm history becomes queryable locally (parent path).
**Security:** replication grants read of the core to anyone who can join the topic. Prefer:
- Swarm only on trusted LAN / VPN, or
- Future: encrypted cores + allowlisted swarm joins
- Keep **mutations** on protomux-rpc AuthZ (`linkPeer` is admin)
HyperDHT RPC identity ≠ Hyperswarm topic access — treat them as two layers.
### Phase C — Autobase parents (planned)
Same as `hyperdb-autobase-workshop`:
- Autobase bootstrap key across parent writers
- View = `HyperDB.bee(store.get('db-view'), spec, { extension: false })`
- hyperdispatch ops for `put-link`, `put-alert`, rollups
- Do **not** “backup” by copying Corestore folders
---
## Code map
| Path | Role |
|------|------|
| `scripts/build-db.js` | Hyperschema + HyperDB builder |
| `spec/hyperschema/` | Generated encodings + `schema.json` |
| `spec/hyperdb/` | Generated collections/indexes |
| `server/db/model.js` | Typed CRUD facade |
| `server/db/index.js` | Singleton open/close |
| `server/db/replicate.js` | Optional Hyperswarm |
| `server/services/warm-flush.js` | Batch writer |
| `server/services/store.js` | Hot ring + `warm` events + query fallback |
### Model usage example
```js
import { openDb, getDb, closeDb } from './server/db/index.js'
await openDb()
const db = getDb()
await db.putPeerLink({
localNodeId: 'abc123',
remotePublicKey: 'ff'.repeat(32),
syncMode: 'pull',
linkedAt: Date.now(),
})
await db.putMetricPoints([
{ chart: 'system.cpu', context: 'system.cpu', ts: Date.now(), values: { user: 12.3 }, tier: 1 },
])
const rows = await db.queryMetricPoints({
chart: 'system.cpu',
afterMs: Date.now() - 3600_000,
beforeMs: Date.now(),
})
```
---
## Dependencies
From Holepunch stack (see `holepunchto_repos`):
- `hyperdb` — DB engine (bee + rocks)
- `hyperschema` — struct codegen
- `corestore` — named hypercores + replicate
- `hyperswarm` — topic discovery for store sync
- `ready-resource` — open/close lifecycle
Future: `autobase`, `hyperdispatch` for HA parents.
---
## Testing
```bash
npm run build:db
SKIP_INTEGRATION=1 npm test
# includes test/hyperdb.test.js
```
Manual:
```bash
npm run start:server
curl -s http://127.0.0.1:19999/api/v3/db | jq
# wait ~60s for first warm bucket, then:
curl -s 'http://127.0.0.1:19999/api/v3/data?chart=system.cpu&after=-7200&tier=1&points=120' | jq '.source,.points'
```
---
## Operational checklist
- [ ] Commit `spec/` after every `build:db`
- [ ] Back up **keys** (`SERVER_SEED`, swarm keypair in corestore) — not by zipping live corestore while writing
- [ ] Keep REST on localhost; swarm only when linking trusted peers
- [ ] Prefer `pd1.` invites for RPC admin; share db discovery keys only with linked nodes
- [ ] Monitor disk under `data/corestore` as warm retention grows
---
## References
| Repo (local `holepunchto_repos`) | Takeaway |
|----------------------------------|----------|
| `hyperdb` | rocks vs bee, tx/flush, autoUpdate |
| `hyperdb-workshop` | builder + Corestore + Swarm replicate |
| `hyperdb-autobase-workshop` | Multi-writer view for parents |
| `corestore` | `store.replicate(conn)` |
| `pear-hyperdb` | Pear Model wrapper style |
+22 -6
View File
@@ -23,13 +23,29 @@ If fleet scale demands it later:
For MVP, JSON pushes keep the stack simple and debuggable.
## Bare / Pear runtime (no Node builtins)
Pears native runtime is **Bare**. Do not rely on Node core modules existing at runtime.
| Pattern | Use |
|---------|-----|
| `package.json` `imports` map | `fs` / `os` / `path` / `crypto` / `events` / `http` / … → `bare-*` under `"bare"` condition (same idea as PearDock / pear-docs node-compat) |
| `bare-node-runtime/global` | Loaded first in Bare entrypoints (`bin/peardata-server.mjs`, `index.js`) for `process` / `Buffer` / `fetch` |
| `b4a` | Buffers in `shared/` and wire code (prefer over Node `Buffer`) |
| Direct `bare-*` | Optional for new Bare-first modules |
**Keep bare-safe:** `shared/`, `client/`, Pear `app.js` / `index.js` / `ui/`.
**Agent (`server/`):** may keep Node-style import names; under Bare they resolve via the imports map. Boot via `bin/peardata-server.mjs`.
Do **not** use `if (isBare) require('bare-fs') else require('fs')` branches for packable apps — Bares packer walks both sides. Prefer import maps.
## Collection
| Option | Verdict |
|--------|---------|
| **Node `os` + `/proc` (chosen)** | Zero native deps, good enough for MVP, low overhead |
| **`os` + `/proc` via bare-os / bare-fs (chosen)** | Hybrid Node+Bare; low overhead |
| `node-os-utils` | Convenient but extra dep / less control |
| Native bindings (netdata collectors, `systeminformation`) | Higher fidelity; consider Phase 2 for Windows depth |
| Native bindings (`systeminformation`, etc.) | Higher fidelity; consider Phase 2 for Windows depth |
| Shell out to `vmstat`/`iostat` | Avoid on hot path |
**Linux:** `/proc/meminfo`, `/proc/loadavg`, `/proc/net/dev`, `/proc/diskstats`
@@ -58,20 +74,20 @@ Target overhead: single timer, no child processes per tick, ring buffers only.
| Option | Verdict |
|--------|---------|
| **Canvas sparklines (chosen MVP)** | No extra deps inside Pear; snappy for 1s updates |
| **uPlot** | Best next step for Netdata-like interactive charts (tiny, fast) |
| **uPlot** | Best next step for interactive charts (tiny, fast) |
| Chart.js / ECharts | Heavier; fine for secondary views |
| Grafana via REST | External; use `/api/v3/data` + Prometheus export |
## REST
Built-in Node `http` (no Express) — small attack surface, enough for Netdata-style GET APIs.
`http` module (no Express) — under Bare this is `bare-http1` via import maps. Small attack surface for agent-style GET APIs.
## Packaging
| Piece | Approach |
|-------|----------|
| Agent | Node 20+ , systemd unit `deploy/peardata.service` |
| Desktop | Pear (`pear-electron` + `pear-bridge`) |
| Agent | Node 20+ or Bare (`bin/peardata-server.mjs`), systemd unit `deploy/peardata.service` |
| Desktop | Pear (`pear-electron` + `pear-bridge`) with Bare-ready imports |
| Invites | `pd1.` tokens (PearDock-style) |
| Installer | Phase 1 one-liner script |
+2 -1
View File
@@ -16,7 +16,8 @@
| `test/crypto-auth.test.js` | MAC key, capabilities, admin proof, invites, classify input |
| `test/protocol.test.js` | Constants, monitoring `MethodRoles`, schema validators |
| `test/store.test.js` | Metric ring buffer ingest + query |
| `test/rest.test.js` | Netdata-style `/api/v3` routes |
| `test/hyperdb.test.js` | HyperDB model (node, links, warm points, alerts) |
| `test/rest.test.js` | agent-style `/api/v3` routes |
| `test/integration.test.js` | Live HyperDHT agent + handshake + metrics query |
```bash
+1 -1
View File
@@ -14,7 +14,7 @@
<span class="logo"></span>
<div class="app-brand-text">
<strong>PearData</strong>
<span class="muted">P2P · Netdata-class observability</span>
<span class="muted">P2P · real-time observability</span>
</div>
</div>
</div>
+7
View File
@@ -2,9 +2,16 @@
* Pear desktop entrypoint.
* Boots pear-electron UI + pear-bridge HTTP for the HTML app shell.
*
* Under Bare: load bare-node-runtime globals so shared/client code can use
* process/Buffer while package.json `imports` maps builtins → bare-*.
*
* @typedef {import('pear-interface')}
*/
/* global Pear */
if (typeof globalThis.Bare !== 'undefined') {
await import('bare-node-runtime/global')
}
import Runtime from 'pear-electron'
import Bridge from 'pear-bridge'
+462 -72
View File
@@ -10,6 +10,34 @@
"license": "MIT",
"dependencies": {
"b4a": "^1.8.1",
"bare-assert": "^1.1.0",
"bare-buffer": "^3.3.1",
"bare-console": "^6.0.1",
"bare-crypto": "^1.15.3",
"bare-dgram": "^1.0.1",
"bare-dns": "^2.1.4",
"bare-events": "^2.9.1",
"bare-fs": "^4.7.4",
"bare-http1": "^4.5.7",
"bare-https": "^3.0.0",
"bare-module": "^6.1.2",
"bare-net": "^2.3.2",
"bare-node-runtime": "^1.5.0",
"bare-os": "^3.9.3",
"bare-path": "^3.1.1",
"bare-performance": "^2.0.0",
"bare-process": "^4.5.1",
"bare-querystring": "^1.0.0",
"bare-stream": "^2.7.0",
"bare-string-decoder": "^1.0.0",
"bare-subprocess": "^5.2.3",
"bare-timers": "^3.0.0",
"bare-tls": "^3.0.0",
"bare-tty": "^5.0.0",
"bare-url": "^2.4.5",
"bare-utils": "^1.5.1",
"bare-worker": "^4.0.0",
"bare-zlib": "^1.3.1",
"compact-encoding": "^3.3.0",
"corestore": "^7.11.1",
"dotenv": "^17.4.2",
@@ -19,6 +47,7 @@
"hyperdb": "^6.7.0",
"hyperdht": "^6.33.0",
"hyperschema": "^1.21.0",
"hyperswarm": "^4.17.0",
"pear-bridge": "^1.2.5",
"pear-electron": "^1.7.28",
"pear-pipe": "^1.0.6",
@@ -27,6 +56,7 @@
"protomux-rpc": "^1.10.0",
"ready-resource": "^1.2.0",
"safety-catch": "^1.0.3",
"which-runtime": "^1.4.0",
"z32": "^1.1.0"
},
"devDependencies": {
@@ -130,6 +160,15 @@
"integrity": "sha512-zdc8l88eB11Jsz5rDd6sCAgv2kUFXgdrZWoMlgU6JMkfAi1/uuGFC3IEHswKbIRQTk5H3T5CMuechsXYxiaHlQ==",
"license": "Apache-2.0"
},
"node_modules/bare-abort-controller": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/bare-abort-controller/-/bare-abort-controller-1.1.2.tgz",
"integrity": "sha512-wk+JZGZEjm7RqaBAU1KuT8TxYYz7h/xxC7+4IVuDZFqK4dGqwrJ/1/yR8hNiSyaAAVHTTFNBJPH4Rzu+rI3IAg==",
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.7.0"
}
},
"node_modules/bare-addon-resolve": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.10.1.tgz",
@@ -174,11 +213,27 @@
"bare-inspect": "^3.1.2"
}
},
"node_modules/bare-async-hooks": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/bare-async-hooks/-/bare-async-hooks-0.0.0.tgz",
"integrity": "sha512-xNfGwUobaomCGMGAqohAekS3uMCj+4tvI4AoOaJnO7NfpN+dvFdkC5xkeQtmZzs2vxf2TR5J6i5FDd1ImCZERw==",
"license": "Apache-2.0"
},
"node_modules/bare-broadcast-channel": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/bare-broadcast-channel/-/bare-broadcast-channel-0.2.0.tgz",
"integrity": "sha512-MuAwdKWr4cSjNwqvbE3tA9Wn6w69q6iXYnP2Wb0nlDa6sKNSMSfY7LXkV4FzWlq9JFP9d0HkCAKPboTLKnUfWQ==",
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.0.0",
"bare-stream": "^2.7.0",
"bare-structured-clone": "^1.4.0"
}
},
"node_modules/bare-buffer": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/bare-buffer/-/bare-buffer-3.6.2.tgz",
"integrity": "sha512-WT9xx12FJvWbBCkgfjuAeWfY40RW6BKVr2fK9UGrTEYKbvqhcaW0J9AMkA3Sj36Wgi+DUuGXYkZPxn5jVH61LA==",
"devOptional": true,
"license": "Apache-2.0",
"engines": {
"bare": ">=1.20.0"
@@ -188,7 +243,6 @@
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/bare-bundle/-/bare-bundle-1.10.0.tgz",
"integrity": "sha512-4LVlnJAHr00Hh6Vu6ZUJS38rcEtJT3b3vChXSsBsJ2mk1TN0lQ+gzd+Dw5L0aV7uqDZv84smuwW+O02X7PfDlw==",
"dev": true,
"license": "Apache-2.0",
"peerDependencies": {
"bare-buffer": "*",
@@ -207,7 +261,6 @@
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/bare-channel/-/bare-channel-5.2.4.tgz",
"integrity": "sha512-enkaOtvXUiZsLBSbataxV/QBjehCOK+SUTd2JQWUYW2iIOufpPu9jyZ9bqJqkEquktrK/rpEMVEmKsPzoci/sA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.0.0",
@@ -218,6 +271,19 @@
"bare": ">=1.7.0"
}
},
"node_modules/bare-console": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/bare-console/-/bare-console-6.2.0.tgz",
"integrity": "sha512-qQ+Vasa3NwNdVDcUWKIIEG5p7MKO7uABcV/xBR9MDwQg3QklC5ayemaHzBa/ER4h6TKN2PRQN5IppNwUAMtb8Q==",
"license": "Apache-2.0",
"dependencies": {
"bare-format": "^1.0.2",
"bare-hrtime": "^2.0.0",
"bare-logger": "^2.0.0",
"bare-system-logger": "^1.0.2",
"bare-type": "^1.1.0"
}
},
"node_modules/bare-cov": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/bare-cov/-/bare-cov-1.2.2.tgz",
@@ -239,7 +305,6 @@
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/bare-crypto/-/bare-crypto-1.15.3.tgz",
"integrity": "sha512-macV9lbyJTsLPRXJkBtz8ivTGEo3LCyJInLT9IB/PWJ7pRXwvHs/FP4bx/fWw+HZkiepIYCAV2cuU5CR92XWCw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-assert": "^1.2.0",
@@ -269,12 +334,27 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/bare-debug-log/-/bare-debug-log-2.0.0.tgz",
"integrity": "sha512-Vi42PkMQsNV9PUpx2Gl1hikshx5O9FzMJ6o9Nnopseg7qLBBK7Nl31d0RHcfwLEAfmcPApytpc0ZFfq68u22FQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-os": "^3.0.1"
}
},
"node_modules/bare-dgram": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/bare-dgram/-/bare-dgram-1.0.1.tgz",
"integrity": "sha512-EdsyRErrkWgN8fENdrDdXFEE9HAuJ/m6ehXz13fVj9JhdCaLWIA+L8o5aYNRLt66x08RlyG2vbrRAZoxGfcdlg==",
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.5.0",
"udx-native": "^1.11.2"
}
},
"node_modules/bare-diagnostics-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/bare-diagnostics-channel/-/bare-diagnostics-channel-1.1.0.tgz",
"integrity": "sha512-Reu+EQo+eLpB+a5p8UykFEdXndFaRaSgKV38uAMh/qhE2eTeJcdwAwo74hKYyeN8GD3DIFqC9ZlM4bnDc03CIg==",
"license": "Apache-2.0"
},
"node_modules/bare-dns": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/bare-dns/-/bare-dns-2.1.4.tgz",
@@ -288,7 +368,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/bare-encoding/-/bare-encoding-1.0.3.tgz",
"integrity": "sha512-Kqf+t/azs13lUeyK4Tb7ha4wdLRXKWCXQ8w1rVmt7KtoPCPdHD/Xwt7LBIsCSwwGglrcmblo5VOLa5avkJqULA==",
"dev": true,
"license": "Apache-2.0",
"peerDependencies": {
"bare-buffer": "*"
@@ -322,6 +401,44 @@
}
}
},
"node_modules/bare-fetch": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bare-fetch/-/bare-fetch-3.1.0.tgz",
"integrity": "sha512-+l0Ex1cXa50M/UaOYQPyWk76UKJ3odkgHkdcFqTQfbUbHuP/32OE0qVMYveHK4Dc6hG8Y3HQvn5RNC21IbJeKw==",
"license": "Apache-2.0",
"dependencies": {
"bare-form-data": "^1.2.0",
"bare-http1": "^4.5.2",
"bare-https": "^3.0.0",
"bare-mime": "^1.0.0",
"bare-performance": "^2.1.1",
"bare-stream": "^2.9.1",
"bare-url": "^2.4.0",
"bare-zlib": "^1.3.0"
},
"peerDependencies": {
"bare-abort-controller": "*",
"bare-buffer": "*"
},
"peerDependenciesMeta": {
"bare-abort-controller": {
"optional": true
},
"bare-buffer": {
"optional": true
}
}
},
"node_modules/bare-form-data": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/bare-form-data/-/bare-form-data-1.2.2.tgz",
"integrity": "sha512-DQyAkCf5mgKT07orewuvaJfoalw7RBSHia4wgkrG7+seI6aHLB+r6gMRdCGrlO+BmCqMwgTeHAHxDU2NrOjQnQ==",
"license": "Apache-2.0",
"dependencies": {
"bare-buffer": "^3.6.0",
"bare-stream": "^2.6.5"
}
},
"node_modules/bare-format": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bare-format/-/bare-format-1.0.2.tgz",
@@ -395,7 +512,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bare-https/-/bare-https-3.0.0.tgz",
"integrity": "sha512-W1GRSCzn+xXKf5bMcPs/hg6Ga1bxPqb7owGfS+tvlBQfPe5Q2STcanRuKZrgU60v5uKrhXH5cgWwM+DLqvXZgQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-http1": "^4.4.0",
@@ -420,7 +536,6 @@
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/bare-inspector/-/bare-inspector-6.1.0.tgz",
"integrity": "sha512-PRxmZ4gF+K3TLzGubgRFvzdECybTCSKackgNsAdd4e7SdvICxMkGj+p5iX7bSKYSmgDnxgCR+2Z6UXmWykKhvw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.1.0",
@@ -441,11 +556,25 @@
}
}
},
"node_modules/bare-logger": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/bare-logger/-/bare-logger-2.0.3.tgz",
"integrity": "sha512-U6K7NxHdeAHxEgHFDV8zXvgjgDZKQO6LlCrxQvUvlrZEVTnoezSImMWizi85rbZpVsbeI1ZWcLCRGnoIf5EV8Q==",
"license": "Apache-2.0",
"dependencies": {
"bare-format": "^1.0.0"
}
},
"node_modules/bare-mime": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/bare-mime/-/bare-mime-1.0.0.tgz",
"integrity": "sha512-lUOswzBkfqham4zjLDueKOd4Qj3gS56BiZ3q2f0g0adoFhF+HFNupvTUfZBWoicl7fWJ7Hp2RUZjmkY47dxxOQ==",
"license": "Apache-2.0"
},
"node_modules/bare-module": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/bare-module/-/bare-module-6.4.0.tgz",
"integrity": "sha512-Yn4V5g5EqGQL4LYUOmt7fjKzj2JPWyJOqE3lPoeZwfUH5rk4CKUfZj6JhDwbzhBYCqqmUgjgQ5aY8cihAPILLA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-bundle": "^1.3.0",
@@ -471,7 +600,6 @@
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/bare-module-lexer/-/bare-module-lexer-1.6.3.tgz",
"integrity": "sha512-NQY7cnPV3GZlHJphX4nXmPdNPER/Tp17pVi9/he2ODw/GNZ7FXzrZlrS7WMF8zbtWigqW/NMc9aQc2BH8UJXqA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"require-addon": "^1.0.2"
@@ -502,11 +630,34 @@
}
}
},
"node_modules/bare-module-traverse": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/bare-module-traverse/-/bare-module-traverse-2.4.4.tgz",
"integrity": "sha512-zK4bDxqeD15Tvu/C5B1Mi/epl9PavjIhU3SVh5RhDb4Hpet6applPVX8Q9rinu7nT80yzgSSlBn0exzJaNXv0A==",
"license": "Apache-2.0",
"dependencies": {
"bare-addon-resolve": "^1.5.0",
"bare-mime": "^1.0.0",
"bare-module-lexer": "^1.6.0",
"bare-module-resolve": "^1.7.0"
},
"peerDependencies": {
"bare-buffer": "*",
"bare-url": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
},
"bare-url": {
"optional": true
}
}
},
"node_modules/bare-net": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/bare-net/-/bare-net-2.3.2.tgz",
"integrity": "sha512-I+yz+pqbYsBkxDsnu5vkKvy7RSNY9CcAvu2jZT6PsmdXJQG1i3dmD5V7xc3334OVp2absgtUEYLmmuNFlphBzg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.2.2",
@@ -515,6 +666,79 @@
"bare-tcp": "^2.0.0"
}
},
"node_modules/bare-node-runtime": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bare-node-runtime/-/bare-node-runtime-1.5.0.tgz",
"integrity": "sha512-eMRq9WDYd+E0IZ8EG/8iCozZJl511z6vp7kxZyxjDw77ggOZ7bThFpJ72Kztjs8sh0hoe+xhFyZmoqAdVE/Ziw==",
"license": "Apache-2.0",
"dependencies": {
"bare-abort-controller": "^1.0.0",
"bare-assert": "^1.1.0",
"bare-async-hooks": "^0.0.0",
"bare-buffer": "^3.3.1",
"bare-console": "^6.0.1",
"bare-crypto": "^1.11.2",
"bare-dgram": "^1.0.1",
"bare-diagnostics-channel": "^1.1.0",
"bare-dns": "^2.1.4",
"bare-events": "^2.7.0",
"bare-fetch": "^3.0.0",
"bare-fs": "^4.2.3",
"bare-http1": "^4.0.4",
"bare-https": "^3.0.0",
"bare-inspector": "^6.0.1",
"bare-module": "^6.1.2",
"bare-net": "^2.0.2",
"bare-os": "^3.6.2",
"bare-path": "^3.0.0",
"bare-performance": "^2.0.0",
"bare-process": "^4.2.1",
"bare-punycode": "^0.0.0",
"bare-querystring": "^1.0.0",
"bare-readline": "^1.1.0",
"bare-repl": "^6.0.1",
"bare-sqlite": "^0.1.4",
"bare-stream": "^2.7.0",
"bare-string-decoder": "^1.0.0",
"bare-subprocess": "^6.0.0",
"bare-timers": "^3.0.3",
"bare-tls": "^3.0.0",
"bare-tty": "^5.0.3",
"bare-url": "^2.2.2",
"bare-utils": "^1.5.1",
"bare-v8": "^1.0.1",
"bare-vm": "^1.0.0",
"bare-worker": "^4.0.0",
"bare-ws": "^3.0.0",
"bare-zlib": "^1.3.1"
}
},
"node_modules/bare-node-runtime/node_modules/bare-subprocess": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-6.1.0.tgz",
"integrity": "sha512-8L6KtbmreDy4Fc1BdDqaDo9fg0nbedUmTSFQxYPV0uHIM2BBAX8+E0LyMDKgkk0I+mlKto80WYzOnIhT8VDdOg==",
"license": "Apache-2.0",
"dependencies": {
"bare-env": "^3.0.0",
"bare-events": "^2.5.4",
"bare-os": "^3.0.1",
"bare-pipe": "^4.2.0",
"bare-structured-clone": "^1.5.2",
"bare-tcp": "^2.4.1",
"bare-url": "^2.2.2"
},
"engines": {
"bare": ">=1.7.0"
},
"peerDependencies": {
"bare-buffer": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
}
}
},
"node_modules/bare-os": {
"version": "3.9.3",
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.3.tgz",
@@ -530,6 +754,18 @@
"integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==",
"license": "Apache-2.0"
},
"node_modules/bare-performance": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/bare-performance/-/bare-performance-2.1.1.tgz",
"integrity": "sha512-nVlulswnYgXS2Fkbk4ZIKgfIWY/rmeG8ljM9aryPYClgPNzpOgOSLQzVSgU/K+ReLJHq7fEUQ9SBdjEs1QTIfw==",
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.9.1"
},
"engines": {
"bare": ">=1.27.0"
}
},
"node_modules/bare-pipe": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.2.2.tgz",
@@ -565,6 +801,21 @@
"bare-stdio": "^1.0.1"
}
},
"node_modules/bare-punycode": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/bare-punycode/-/bare-punycode-0.0.0.tgz",
"integrity": "sha512-PC2Y6mGLytZPJCB9M7CvO5Zb6uWYVUZ+5t3L6vePFuFM6tdC6SsQ+sIsuf0Sa6LHBoWURX7I9yMMbPXMv7TIdQ==",
"license": "Apache-2.0",
"dependencies": {
"punycode": "^2.3.1"
}
},
"node_modules/bare-querystring": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/bare-querystring/-/bare-querystring-1.1.0.tgz",
"integrity": "sha512-pUtEM6JrX53MbEJFwO92F0Ch7BwZ67KD7LyglcB8/tvkkVdwTgN1f7oIklRe+NTT/WCYZgwjDFYS9efBxDSq8g==",
"license": "Apache-2.0"
},
"node_modules/bare-readline": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/bare-readline/-/bare-readline-1.3.1.tgz",
@@ -575,6 +826,30 @@
"bare-stream": "^2.0.0"
}
},
"node_modules/bare-realm": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/bare-realm/-/bare-realm-2.0.1.tgz",
"integrity": "sha512-kQbYU6AAQu4XBTQesuz2+PpjBx7MJzf5Qw3nSLzQCzbVglx7Ml85MtWEjUe1AeslXqrd+nFPHMEbL55ouISscA==",
"license": "Apache-2.0",
"engines": {
"bare": ">=1.5.0"
}
},
"node_modules/bare-repl": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/bare-repl/-/bare-repl-6.1.1.tgz",
"integrity": "sha512-vV52s+zLcwf9WB6y09KwHrqHrCR0UUgcKz3NPhPAwmO62ctFYbBfjskBch92wWk7vRrF68dHJj3hmYNCe8d18g==",
"license": "Apache-2.0",
"dependencies": {
"bare-inspect": "^3.0.0",
"bare-module": "^6.4.0",
"bare-path": "^3.0.0",
"bare-pipe": "^4.0.0",
"bare-readline": "^1.0.0",
"bare-stream": "^2.0.0",
"bare-tty": "^5.0.0"
}
},
"node_modules/bare-semver": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.1.0.tgz",
@@ -593,6 +868,20 @@
"bare": ">=1.7.0"
}
},
"node_modules/bare-sqlite": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/bare-sqlite/-/bare-sqlite-0.1.4.tgz",
"integrity": "sha512-h2UdmQohSQG98lrMZ31PnhKIZRNT+1cDUZRzboHmsXKp+l4N4UBSJylMDEGL+D4fw6MJfOHWFIzUG20pITGy8Q==",
"license": "Apache-2.0",
"peerDependencies": {
"bare-buffer": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
}
}
},
"node_modules/bare-stdio": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/bare-stdio/-/bare-stdio-1.0.3.tgz",
@@ -631,11 +920,27 @@
}
}
},
"node_modules/bare-string-decoder": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/bare-string-decoder/-/bare-string-decoder-1.0.0.tgz",
"integrity": "sha512-FjFvfHo88U7borNQSj9ijP2JTb1asqY6K28OZrix4dF9iZf5D2wi1+99CgLlHT3HtYqdwxRhDAiKu8rVstt5rQ==",
"license": "Apache-2.0",
"dependencies": {
"text-decoder": "^1.2.3"
},
"peerDependencies": {
"bare-buffer": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
}
}
},
"node_modules/bare-structured-clone": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/bare-structured-clone/-/bare-structured-clone-1.6.0.tgz",
"integrity": "sha512-AZjEERyqF7kAuudlmgrweT2KwwWM0LsGG4Gx/bWyFwJrKU7E3wNG8c09z2DIrdw93p3vDtfOX8fyHOcXfy5m+g==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-buffer": "^3.6.0",
@@ -651,7 +956,6 @@
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/bare-stylize/-/bare-stylize-0.0.1.tgz",
"integrity": "sha512-l3MjmIl476bWijYWf3RbE+osl4iuXSOMudzp0vAqzIK7gPgn/+G3oAxp8Oin9CFF911KBP0LO9kts8Ci8mGZaQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-ansi-escapes": "^2.2.3",
@@ -659,18 +963,15 @@
}
},
"node_modules/bare-subprocess": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-6.1.0.tgz",
"integrity": "sha512-8L6KtbmreDy4Fc1BdDqaDo9fg0nbedUmTSFQxYPV0uHIM2BBAX8+E0LyMDKgkk0I+mlKto80WYzOnIhT8VDdOg==",
"dev": true,
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.2.3.tgz",
"integrity": "sha512-07wwswlV7M3sC9IykbZRZ/jHAkrXFWVLqdBWGv1y0ojCimtRD9hGwxdHmR5FUFmDUZLNsBmTYJNQqgio5+A85Q==",
"license": "Apache-2.0",
"dependencies": {
"bare-env": "^3.0.0",
"bare-events": "^2.5.4",
"bare-os": "^3.0.1",
"bare-pipe": "^4.2.0",
"bare-structured-clone": "^1.5.2",
"bare-tcp": "^2.4.1",
"bare-pipe": "^4.0.0",
"bare-url": "^2.2.2"
},
"engines": {
@@ -685,6 +986,15 @@
}
}
},
"node_modules/bare-system-logger": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/bare-system-logger/-/bare-system-logger-1.0.3.tgz",
"integrity": "sha512-HXTXZyhIIS4ZDYun4J9zKNQZDfLgEkkK8wy9nrTFJE5xq8APsOhDxuEsxP0YcNwC23DW1jaNpqD51zDeXPzIdg==",
"license": "Apache-2.0",
"dependencies": {
"bare-logger": "^2.0.0"
}
},
"node_modules/bare-tcp": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/bare-tcp/-/bare-tcp-2.5.2.tgz",
@@ -707,11 +1017,39 @@
}
}
},
"node_modules/bare-thread": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/bare-thread/-/bare-thread-1.2.4.tgz",
"integrity": "sha512-MnqMCGVp2JJJNXgwUBC8hw+J4FrTeLkLgfCPPl5tQK6MbtV2Efi6LKK5v819VRY0da+AXlCbgssbAVge31NfRg==",
"license": "Apache-2.0",
"dependencies": {
"bare-bundle": "^1.9.0",
"bare-module-resolve": "^1.11.2",
"bare-module-traverse": "^2.0.0",
"bare-url": "^2.4.2"
}
},
"node_modules/bare-timers": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/bare-timers/-/bare-timers-3.2.1.tgz",
"integrity": "sha512-O+9e6Jol/BoYsQUzAFXG0gWUW1GWryFMblGcNBfi3smPrD3rJIKQwpw2FJARl2j/1VHne4a8YaHPK1cxDKfiYQ==",
"license": "Apache-2.0",
"engines": {
"bare": ">=1.7.0"
},
"peerDependencies": {
"bare-abort-controller": "*"
},
"peerDependenciesMeta": {
"bare-abort-controller": {
"optional": true
}
}
},
"node_modules/bare-tls": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/bare-tls/-/bare-tls-3.1.7.tgz",
"integrity": "sha512-AMw8tJlb3LhzAmhgXRcjDrTlNxR3gXXyj6G8eU9iwvCFtiUBD8MxAW7bwunA1gXDukgo40A970jX0APc2jMU7A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-net": "^2.0.1",
@@ -748,7 +1086,6 @@
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/bare-type-stripper/-/bare-type-stripper-0.1.4.tgz",
"integrity": "sha512-FdZhp9XEnQpj8AWFmIft/sVUyKS9XSmB6PhcxBHhuEDxxZM5Kkt8+kFS7eEpLXR7TkaRkNpSENoGH/8lpAmtkA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"require-addon": "^1.0.2"
@@ -775,7 +1112,6 @@
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/bare-utils/-/bare-utils-1.6.0.tgz",
"integrity": "sha512-WhQEIkkAxkSnW7u1QgrI0AfNm5JpMruETXeYsb5qnkBJ0TTfNKygZmsh6rkoHBANaV+C/7Jed7bJP9OmEHG7rQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-debug-log": "^2.0.0",
@@ -786,6 +1122,12 @@
"bare-type": "^1.0.6"
}
},
"node_modules/bare-v8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/bare-v8/-/bare-v8-1.0.1.tgz",
"integrity": "sha512-/cR5ZvFWQRdtTZ4tx0j7TKvTWce8UnnLqm88fwHtJmfM7HODIBVjQGDT7KkDLeD2d/eHP2pzB71Y8/QyiMMKrQ==",
"license": "Apache-2.0"
},
"node_modules/bare-v8-to-istanbul": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bare-v8-to-istanbul/-/bare-v8-to-istanbul-1.0.2.tgz",
@@ -804,11 +1146,33 @@
"which-runtime": "^1.2.1"
}
},
"node_modules/bare-vm": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/bare-vm/-/bare-vm-1.0.1.tgz",
"integrity": "sha512-yLnbRvKt3AhRTmtfTIrYfdTHqGEfIJc+Fgb2DcHejE0HJ+p5adGxxPMvd3893Z7iXVYnalxukNARn4oJSZELHQ==",
"license": "Apache-2.0",
"dependencies": {
"bare-realm": "^2.0.0"
}
},
"node_modules/bare-worker": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/bare-worker/-/bare-worker-4.4.0.tgz",
"integrity": "sha512-zSc1biis9ks03nj/24M7tYS2V0CPhefizzRIxVYdglbbUAgA0zakwSgTKLJshZ6P+9NA55M/eG4k/rBALZVbxg==",
"license": "Apache-2.0",
"dependencies": {
"bare-broadcast-channel": "^0.2.0",
"bare-channel": "^5.1.5",
"bare-events": "^2.2.1",
"bare-module": "^6.4.0",
"bare-stream": "^2.13.3",
"bare-thread": "^1.2.2"
}
},
"node_modules/bare-ws": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bare-ws/-/bare-ws-3.0.0.tgz",
"integrity": "sha512-q2v0UIQ0cFQBXQMp+0FRaoSk1EoYgOhzO7yio0TqBV6Rkfot97mbBYxb1ssw5faVCisVaFxQYY8113SMLYQBow==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-crypto": "^1.2.0",
@@ -830,6 +1194,23 @@
}
}
},
"node_modules/bare-zlib": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/bare-zlib/-/bare-zlib-1.4.1.tgz",
"integrity": "sha512-CsnQl+XyLaUecB9/OUpjqmemung10M7J2UNXz+6NAVrZAI3HC9c5Kxw34aI0jaU9+gb2yUCD31hOrtPZlnE3bA==",
"license": "Apache-2.0",
"dependencies": {
"bare-stream": "^2.0.0"
},
"peerDependencies": {
"bare-buffer": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
}
}
},
"node_modules/big-sparse-array": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/big-sparse-array/-/big-sparse-array-1.0.3.tgz",
@@ -934,6 +1315,33 @@
"brittle-node": "brittle-node.js"
}
},
"node_modules/brittle/node_modules/bare-subprocess": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-6.1.0.tgz",
"integrity": "sha512-8L6KtbmreDy4Fc1BdDqaDo9fg0nbedUmTSFQxYPV0uHIM2BBAX8+E0LyMDKgkk0I+mlKto80WYzOnIhT8VDdOg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-env": "^3.0.0",
"bare-events": "^2.5.4",
"bare-os": "^3.0.1",
"bare-pipe": "^4.2.0",
"bare-structured-clone": "^1.5.2",
"bare-tcp": "^2.4.1",
"bare-url": "^2.2.2"
},
"engines": {
"bare": ">=1.7.0"
},
"peerDependencies": {
"bare-buffer": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
}
}
},
"node_modules/cjs-module-lexer": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz",
@@ -1380,6 +1788,21 @@
"generate-string": "^1.0.1"
}
},
"node_modules/hyperswarm": {
"version": "4.17.0",
"resolved": "https://registry.npmjs.org/hyperswarm/-/hyperswarm-4.17.0.tgz",
"integrity": "sha512-oe86sK961Ueg7rvDN/veFwG8xH+Iv6vObPhGDkPJcDVxk/NduW41ZhAcVDnHzRbm7S0eLU7WaDUvehOYoKSpRQ==",
"license": "MIT",
"dependencies": {
"b4a": "^1.3.1",
"bare-events": "^2.2.0",
"hyperdht": "^6.21.0",
"safety-catch": "^1.0.2",
"shuffled-priority-queue": "^2.1.0",
"streamx": "^2.22.1",
"unslab": "^1.3.0"
}
},
"node_modules/index-encoder": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/index-encoder/-/index-encoder-3.5.0.tgz",
@@ -1636,30 +2059,6 @@
"which-runtime": "^1.2.1"
}
},
"node_modules/pear-electron/node_modules/bare-subprocess": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.2.3.tgz",
"integrity": "sha512-07wwswlV7M3sC9IykbZRZ/jHAkrXFWVLqdBWGv1y0ojCimtRD9hGwxdHmR5FUFmDUZLNsBmTYJNQqgio5+A85Q==",
"license": "Apache-2.0",
"dependencies": {
"bare-env": "^3.0.0",
"bare-events": "^2.5.4",
"bare-os": "^3.0.1",
"bare-pipe": "^4.0.0",
"bare-url": "^2.2.2"
},
"engines": {
"bare": ">=1.7.0"
},
"peerDependencies": {
"bare-buffer": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
}
}
},
"node_modules/pear-electron/node_modules/compact-encoding": {
"version": "2.19.2",
"resolved": "https://registry.npmjs.org/compact-encoding/-/compact-encoding-2.19.2.tgz",
@@ -1835,30 +2234,6 @@
"which-runtime": "^1.3.2"
}
},
"node_modules/pear-run/node_modules/bare-subprocess": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.2.3.tgz",
"integrity": "sha512-07wwswlV7M3sC9IykbZRZ/jHAkrXFWVLqdBWGv1y0ojCimtRD9hGwxdHmR5FUFmDUZLNsBmTYJNQqgio5+A85Q==",
"license": "Apache-2.0",
"dependencies": {
"bare-env": "^3.0.0",
"bare-events": "^2.5.4",
"bare-os": "^3.0.1",
"bare-pipe": "^4.0.0",
"bare-url": "^2.2.2"
},
"engines": {
"bare": ">=1.7.0"
},
"peerDependencies": {
"bare-buffer": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
}
}
},
"node_modules/pear-stamp": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pear-stamp/-/pear-stamp-1.0.2.tgz",
@@ -2170,6 +2545,15 @@
"unix-path-resolve": "^1.0.2"
}
},
"node_modules/shuffled-priority-queue": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/shuffled-priority-queue/-/shuffled-priority-queue-2.1.0.tgz",
"integrity": "sha512-xhdh7fHyMsr0m/w2kDfRJuBFRS96b9l8ZPNWGaQ+PMvnUnZ/Eh+gJJ9NsHBd7P9k0399WYlCLzsy18EaMfyadA==",
"license": "MIT",
"dependencies": {
"unordered-set": "^2.0.1"
}
},
"node_modules/signal-promise": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/signal-promise/-/signal-promise-1.0.3.tgz",
@@ -2385,6 +2769,12 @@
"integrity": "sha512-kG4g5nobBBaMnH2XbrS4sLUXEpx4nY2J3C6KAlAUcnahG2HChxSPVKWYrqEq76iTo+cyMkLUjqxGaQR2tz097Q==",
"license": "MIT"
},
"node_modules/unordered-set": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/unordered-set/-/unordered-set-2.0.1.tgz",
"integrity": "sha512-eUmNTPzdx+q/WvOHW0bgGYLWvWHNT3PTKEQLg0MAQhc0AHASHVHoP/9YytYd4RBVariqno/mEUhVZN98CmD7bg==",
"license": "MIT"
},
"node_modules/unslab": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/unslab/-/unslab-1.3.0.tgz",
+421 -4
View File
@@ -1,7 +1,7 @@
{
"name": "peardata",
"version": "0.1.0",
"description": "Decentralized P2P Netdata-class monitoring PearMonitor agent + Pear desktop + REST v3",
"description": "Decentralized P2P real-time monitoring \u2014 PearMonitor agent + Pear desktop + REST v3",
"type": "module",
"license": "MIT",
"main": "index.js",
@@ -36,14 +36,15 @@
"scripts": {
"dev": "pear run -d .",
"start": "pear run -d .",
"start:server": "node server/server.js",
"server": "node server/server.js",
"start:server": "node bin/peardata-server.mjs",
"server": "node bin/peardata-server.mjs",
"start:server:bin": "node bin/peardata-server.mjs",
"test": "brittle-node test/*.test.js",
"test:integration": "brittle-node test/integration.test.js",
"healthcheck": "node scripts/healthcheck.js",
"soak": "node scripts/soak.js",
"mint-invite": "node scripts/mint-invite.js",
"build:db": "node scripts/build-db.js",
"rename": "bash scripts/rename-template.sh",
"release:notes": "node -e \"console.log('See docs/RELEASE.md')\""
},
@@ -58,6 +59,7 @@
"hyperdb": "^6.7.0",
"hyperdht": "^6.33.0",
"hyperschema": "^1.21.0",
"hyperswarm": "^4.17.0",
"pear-bridge": "^1.2.5",
"pear-electron": "^1.7.28",
"pear-pipe": "^1.0.6",
@@ -66,10 +68,425 @@
"protomux-rpc": "^1.10.0",
"ready-resource": "^1.2.0",
"safety-catch": "^1.0.3",
"z32": "^1.1.0"
"z32": "^1.1.0",
"bare-assert": "^1.1.0",
"bare-buffer": "^3.3.1",
"bare-console": "^6.0.1",
"bare-crypto": "^1.15.3",
"bare-dgram": "^1.0.1",
"bare-dns": "^2.1.4",
"bare-events": "^2.9.1",
"bare-fs": "^4.7.4",
"bare-http1": "^4.5.7",
"bare-https": "^3.0.0",
"bare-module": "^6.1.2",
"bare-net": "^2.3.2",
"bare-node-runtime": "^1.5.0",
"bare-os": "^3.9.3",
"bare-path": "^3.1.1",
"bare-performance": "^2.0.0",
"bare-process": "^4.5.1",
"bare-querystring": "^1.0.0",
"bare-stream": "^2.7.0",
"bare-string-decoder": "^1.0.0",
"bare-subprocess": "^5.2.3",
"bare-timers": "^3.0.0",
"bare-tls": "^3.0.0",
"bare-tty": "^5.0.0",
"bare-url": "^2.4.5",
"bare-utils": "^1.5.1",
"bare-worker": "^4.0.0",
"bare-zlib": "^1.3.1",
"which-runtime": "^1.4.0"
},
"devDependencies": {
"brittle": "^4.1.0",
"pear-interface": "^1.1.0"
},
"imports": {
"assert": {
"bare": "bare-assert",
"default": "assert"
},
"node:assert": {
"bare": "bare-assert",
"default": "assert"
},
"assert/strict": {
"bare": "bare-assert/strict",
"default": "assert/strict"
},
"node:assert/strict": {
"bare": "bare-assert/strict",
"default": "assert/strict"
},
"buffer": {
"bare": "bare-buffer",
"default": "buffer"
},
"node:buffer": {
"bare": "bare-buffer",
"default": "buffer"
},
"child_process": {
"bare": "bare-subprocess",
"default": "child_process"
},
"node:child_process": {
"bare": "bare-subprocess",
"default": "child_process"
},
"console": {
"bare": "bare-console",
"default": "console"
},
"node:console": {
"bare": "bare-console",
"default": "console"
},
"crypto": {
"bare": "bare-crypto",
"default": "crypto"
},
"node:crypto": {
"bare": "bare-crypto",
"default": "crypto"
},
"dgram": {
"bare": "bare-dgram",
"default": "dgram"
},
"node:dgram": {
"bare": "bare-dgram",
"default": "dgram"
},
"dns": {
"bare": "bare-dns",
"default": "dns"
},
"node:dns": {
"bare": "bare-dns",
"default": "dns"
},
"dns/promises": {
"bare": "bare-dns/promises",
"default": "dns/promises"
},
"node:dns/promises": {
"bare": "bare-dns/promises",
"default": "dns/promises"
},
"events": {
"bare": "bare-events",
"default": "events"
},
"node:events": {
"bare": "bare-events",
"default": "events"
},
"fs": {
"bare": "bare-fs",
"default": "fs"
},
"node:fs": {
"bare": "bare-fs",
"default": "fs"
},
"fs/promises": {
"bare": "bare-fs/promises",
"default": "fs/promises"
},
"node:fs/promises": {
"bare": "bare-fs/promises",
"default": "fs/promises"
},
"http": {
"bare": "bare-http1",
"default": "http"
},
"node:http": {
"bare": "bare-http1",
"default": "http"
},
"https": {
"bare": "bare-https",
"default": "https"
},
"node:https": {
"bare": "bare-https",
"default": "https"
},
"module": {
"bare": "bare-module",
"default": "module"
},
"node:module": {
"bare": "bare-module",
"default": "module"
},
"net": {
"bare": "bare-net",
"default": "net"
},
"node:net": {
"bare": "bare-net",
"default": "net"
},
"os": {
"bare": "bare-os",
"default": "os"
},
"node:os": {
"bare": "bare-os",
"default": "os"
},
"path": {
"bare": "bare-path",
"default": "path"
},
"node:path": {
"bare": "bare-path",
"default": "path"
},
"path/posix": {
"bare": "bare-path/posix",
"default": "path/posix"
},
"node:path/posix": {
"bare": "bare-path/posix",
"default": "path/posix"
},
"path/win32": {
"bare": "bare-path/win32",
"default": "path/win32"
},
"node:path/win32": {
"bare": "bare-path/win32",
"default": "path/win32"
},
"perf_hooks": {
"bare": "bare-performance",
"default": "perf_hooks"
},
"node:perf_hooks": {
"bare": "bare-performance",
"default": "perf_hooks"
},
"process": {
"bare": "bare-process",
"default": "process"
},
"node:process": {
"bare": "bare-process",
"default": "process"
},
"querystring": {
"bare": "bare-querystring",
"default": "querystring"
},
"node:querystring": {
"bare": "bare-querystring",
"default": "querystring"
},
"stream": {
"bare": "bare-stream",
"default": "stream"
},
"node:stream": {
"bare": "bare-stream",
"default": "stream"
},
"stream/consumers": {
"bare": "bare-stream/consumers",
"default": "stream/consumers"
},
"node:stream/consumers": {
"bare": "bare-stream/consumers",
"default": "stream/consumers"
},
"stream/promises": {
"bare": "bare-stream/promises",
"default": "stream/promises"
},
"node:stream/promises": {
"bare": "bare-stream/promises",
"default": "stream/promises"
},
"stream/web": {
"bare": "bare-stream/web",
"default": "stream/web"
},
"node:stream/web": {
"bare": "bare-stream/web",
"default": "stream/web"
},
"string_decoder": {
"bare": "bare-string-decoder",
"default": "string_decoder"
},
"node:string_decoder": {
"bare": "bare-string-decoder",
"default": "string_decoder"
},
"timers": {
"bare": "bare-timers",
"default": "timers"
},
"node:timers": {
"bare": "bare-timers",
"default": "timers"
},
"timers/promises": {
"bare": "bare-timers/promises",
"default": "timers/promises"
},
"node:timers/promises": {
"bare": "bare-timers/promises",
"default": "timers/promises"
},
"tls": {
"bare": "bare-tls",
"default": "tls"
},
"node:tls": {
"bare": "bare-tls",
"default": "tls"
},
"tty": {
"bare": "bare-tty",
"default": "tty"
},
"node:tty": {
"bare": "bare-tty",
"default": "tty"
},
"url": {
"bare": "bare-url",
"default": "url"
},
"node:url": {
"bare": "bare-url",
"default": "url"
},
"util": {
"bare": "bare-utils",
"default": "util"
},
"node:util": {
"bare": "bare-utils",
"default": "util"
},
"util/types": {
"bare": "bare-utils/types",
"default": "util/types"
},
"node:util/types": {
"bare": "bare-utils/types",
"default": "util/types"
},
"worker_threads": {
"bare": "bare-worker",
"default": "worker_threads"
},
"node:worker_threads": {
"bare": "bare-worker",
"default": "worker_threads"
},
"zlib": {
"bare": "bare-zlib",
"default": "zlib"
},
"node:zlib": {
"bare": "bare-zlib",
"default": "zlib"
},
"cluster": {
"bare": "bare-node-runtime/unsupported",
"default": "cluster"
},
"node:cluster": {
"bare": "bare-node-runtime/unsupported",
"default": "cluster"
},
"constants": {
"bare": "bare-node-runtime/unsupported",
"default": "constants"
},
"node:constants": {
"bare": "bare-node-runtime/unsupported",
"default": "constants"
},
"domain": {
"bare": "bare-node-runtime/unsupported",
"default": "domain"
},
"node:domain": {
"bare": "bare-node-runtime/unsupported",
"default": "domain"
},
"http2": {
"bare": "bare-node-runtime/unsupported",
"default": "http2"
},
"node:http2": {
"bare": "bare-node-runtime/unsupported",
"default": "http2"
},
"sea": {
"bare": "bare-node-runtime/unsupported",
"default": "sea"
},
"node:sea": {
"bare": "bare-node-runtime/unsupported",
"default": "sea"
},
"sys": {
"bare": "bare-node-runtime/unsupported",
"default": "sys"
},
"node:sys": {
"bare": "bare-node-runtime/unsupported",
"default": "sys"
},
"test": {
"bare": "bare-node-runtime/unsupported",
"default": "test"
},
"node:test": {
"bare": "bare-node-runtime/unsupported",
"default": "test"
},
"test/reporters": {
"bare": "bare-node-runtime/unsupported",
"default": "test/reporters"
},
"node:test/reporters": {
"bare": "bare-node-runtime/unsupported",
"default": "test/reporters"
},
"trace_events": {
"bare": "bare-node-runtime/unsupported",
"default": "trace_events"
},
"node:trace_events": {
"bare": "bare-node-runtime/unsupported",
"default": "trace_events"
},
"wasi": {
"bare": "bare-node-runtime/unsupported",
"default": "wasi"
},
"node:wasi": {
"bare": "bare-node-runtime/unsupported",
"default": "wasi"
},
"fs/*": {
"bare": "bare-fs/*",
"default": "fs/*"
},
"node:fs/*": {
"bare": "bare-fs/*",
"default": "fs/*"
}
}
}
+189
View File
@@ -0,0 +1,189 @@
/**
* Generate Hyperschema + HyperDB definitions for PearData.
*
* Usage: node scripts/build-db.js
*
* SCHEMA SAFETY (Holepunch rules):
* 1. Fields are append-only — deprecate instead of removing
* 2. Do not delete/reset committed files under spec/
* 3. After merge to main, schema changes are permanent
*/
import path from 'path'
import { fileURLToPath } from 'url'
import Hyperschema from 'hyperschema'
import HyperDB from 'hyperdb/builder'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const ROOT = path.join(__dirname, '..')
const SCHEMA_DIR = path.join(ROOT, 'spec', 'hyperschema')
const DB_DIR = path.join(ROOT, 'spec', 'hyperdb')
setupSchema()
setupDb()
console.log('Wrote', SCHEMA_DIR)
console.log('Wrote', DB_DIR)
function setupSchema() {
const schema = Hyperschema.from(SCHEMA_DIR)
const ns = schema.namespace('peardata')
ns.register({
name: 'node',
fields: [
{ name: 'nodeId', type: 'string', required: true },
{ name: 'hostname', type: 'string', required: true },
{ name: 'publicKeyHex', type: 'string', required: false },
{ name: 'platform', type: 'string', required: false },
{ name: 'arch', type: 'string', required: false },
{ name: 'cpus', type: 'uint', required: false },
{ name: 'totalMemMiB', type: 'uint', required: false },
{ name: 'agentVersion', type: 'string', required: false },
{ name: 'labelsJson', type: 'string', required: false },
{ name: 'updatedAt', type: 'uint', required: true },
],
})
ns.register({
name: 'peer-link',
fields: [
{ name: 'localNodeId', type: 'string', required: true },
{ name: 'remotePublicKey', type: 'string', required: true },
{ name: 'role', type: 'string', required: false },
{ name: 'alias', type: 'string', required: false },
{ name: 'dbKeyHex', type: 'string', required: false },
{ name: 'syncMode', type: 'string', required: false }, // push | pull | both
{ name: 'linkedAt', type: 'uint', required: true },
{ name: 'lastSeen', type: 'uint', required: false },
],
})
ns.register({
name: 'alert-config',
fields: [
{ name: 'id', type: 'string', required: true },
{ name: 'chart', type: 'string', required: true },
{ name: 'dimension', type: 'string', required: true },
{ name: 'warn', type: 'float64', required: false },
{ name: 'crit', type: 'float64', required: false },
{ name: 'comparator', type: 'string', required: false },
{ name: 'enabled', type: 'bool', required: false },
{ name: 'info', type: 'string', required: false },
{ name: 'updatedAt', type: 'uint', required: true },
],
})
ns.register({
name: 'alert-event',
fields: [
{ name: 'id', type: 'string', required: true },
{ name: 'ts', type: 'uint', required: true },
{ name: 'chart', type: 'string', required: true },
{ name: 'dimension', type: 'string', required: false },
{ name: 'severity', type: 'string', required: true },
{ name: 'value', type: 'float64', required: false },
{ name: 'threshold', type: 'float64', required: false },
{ name: 'message', type: 'string', required: false },
{ name: 'cleared', type: 'bool', required: false },
],
})
// Warm / downsampled metric buckets (NOT 1s hot path)
ns.register({
name: 'metric-point',
fields: [
{ name: 'chart', type: 'string', required: true },
{ name: 'ts', type: 'uint', required: true }, // unix ms
{ name: 'context', type: 'string', required: true },
{ name: 'tier', type: 'uint', required: true }, // 1 = ~1m, 2 = future coarser
{ name: 'valuesJson', type: 'string', required: true },
],
})
ns.register({
name: 'job',
fields: [
{ name: 'id', type: 'string', required: true },
{ name: 'name', type: 'string', required: true },
{ name: 'status', type: 'string', required: true },
{ name: 'startedAt', type: 'uint', required: false },
{ name: 'finishedAt', type: 'uint', required: false },
{ name: 'resultJson', type: 'string', required: false },
{ name: 'error', type: 'string', required: false },
],
})
Hyperschema.toDisk(schema, { esm: true })
}
function setupDb() {
const db = HyperDB.from(SCHEMA_DIR, DB_DIR)
const ns = db.namespace('peardata')
ns.collections.register({
name: 'node',
schema: '@peardata/node',
key: ['nodeId'],
})
ns.collections.register({
name: 'peer-link',
schema: '@peardata/peer-link',
key: ['localNodeId', 'remotePublicKey'],
})
ns.collections.register({
name: 'alert-config',
schema: '@peardata/alert-config',
key: ['id'],
})
ns.collections.register({
name: 'alert-event',
schema: '@peardata/alert-event',
key: ['id', 'ts'],
})
ns.collections.register({
name: 'metric-point',
schema: '@peardata/metric-point',
key: ['chart', 'ts'],
})
ns.collections.register({
name: 'job',
schema: '@peardata/job',
key: ['id'],
})
ns.indexes.register({
name: 'node-by-hostname',
collection: '@peardata/node',
key: ['hostname'],
})
ns.indexes.register({
name: 'peer-link-by-remote',
collection: '@peardata/peer-link',
key: ['remotePublicKey'],
})
ns.indexes.register({
name: 'alert-event-by-chart',
collection: '@peardata/alert-event',
key: ['chart', 'ts'],
})
ns.indexes.register({
name: 'metric-point-by-context',
collection: '@peardata/metric-point',
key: ['context', 'ts'],
})
ns.indexes.register({
name: 'metric-point-by-tier',
collection: '@peardata/metric-point',
key: ['tier', 'chart', 'ts'],
})
HyperDB.toDisk(db)
}
+2
View File
@@ -13,6 +13,8 @@ const MUTATING = new Set([
'runJob',
'cancelJob',
'exportSnapshot',
'linkPeer',
'unlinkPeer',
'handshake',
])
+83
View File
@@ -0,0 +1,83 @@
/**
* Open / close the agent HyperDB (Corestore-backed bee).
*
* Storage layout (under PEARDATA_DATA_DIR):
* data/corestore/ — Corestore rocks/hypercores
* Named core: "peardata-meta"
*
* Disable with PEARDATA_HYPERDB=0
*/
import path from 'path'
import Corestore from 'corestore'
import { PearDataModel } from './model.js'
import logger from '../utils/logger.js'
const log = logger.child('db')
/** @type {import('corestore')|null} */
let store = null
/** @type {PearDataModel|null} */
let model = null
export function isHyperDbEnabled() {
const v = process.env.PEARDATA_HYPERDB
if (v === '0' || v === 'off' || v === 'false') return false
return true
}
export function getDataDir() {
return process.env.PEARDATA_DATA_DIR || path.resolve('data')
}
/**
* @returns {Promise<PearDataModel|null>}
*/
export async function openDb() {
if (!isHyperDbEnabled()) {
log.info('HyperDB disabled (PEARDATA_HYPERDB=0)')
return null
}
if (model) return model
const dataDir = getDataDir()
const storePath = path.join(dataDir, 'corestore')
store = new Corestore(storePath)
await store.ready()
const core = store.get({ name: 'peardata-meta' })
model = new PearDataModel(core, { autoUpdate: true })
await model.ready()
log.info('HyperDB ready', {
storePath,
publicKeyHex: model.publicKeyHex,
discoveryKeyHex: model.discoveryKeyHex,
})
return model
}
/**
* @returns {PearDataModel|null}
*/
export function getDb() {
return model
}
/**
* @returns {import('corestore')|null}
*/
export function getCorestore() {
return store
}
export async function closeDb() {
if (model) {
await model.close().catch(() => {})
model = null
}
if (store) {
await store.close().catch(() => {})
store = null
}
log.info('HyperDB closed')
}
+319
View File
@@ -0,0 +1,319 @@
/**
* PearDataModel — HyperDB facade for metadata, peer links, alerts, warm metrics.
*
* Pattern mirrors holepunch hyperdb-workshop Registry:
* HyperDB.bee(core, spec, { autoUpdate: true })
* exclusiveTransaction / transaction + flush
*/
import ReadyResource from 'ready-resource'
import HyperDB from 'hyperdb'
import b4a from 'b4a'
import spec from '../../spec/hyperdb/index.js'
export class PearDataModel extends ReadyResource {
/**
* @param {import('hypercore')} core
* @param {{ autoUpdate?: boolean, writable?: boolean, extension?: boolean }} [opts]
*/
constructor(core, opts = {}) {
super()
this.db = HyperDB.bee(core, spec, {
autoUpdate: opts.autoUpdate !== false,
writable: opts.writable !== false,
extension: opts.extension !== false,
})
}
get publicKey() {
return this.db.core.key
}
get publicKeyHex() {
return b4a.toString(this.db.core.key, 'hex')
}
get discoveryKey() {
return this.db.core.discoveryKey
}
get discoveryKeyHex() {
return b4a.toString(this.db.core.discoveryKey, 'hex')
}
async _open() {
await this.db.ready()
}
async _close() {
await this.db.close()
}
// ── nodes ─────────────────────────────────────────────────
/**
* @param {object} node
*/
async putNode(node) {
await this.ready()
const tx = await this.db.exclusiveTransaction()
try {
await tx.insert('@peardata/node', {
nodeId: String(node.nodeId),
hostname: String(node.hostname || ''),
publicKeyHex: node.publicKeyHex || null,
platform: node.platform || null,
arch: node.arch || null,
cpus: node.cpus ?? null,
totalMemMiB: node.totalMemMiB != null ? Math.floor(node.totalMemMiB) : null,
agentVersion: node.agentVersion || null,
labelsJson: node.labels ? JSON.stringify(node.labels) : node.labelsJson || null,
updatedAt: node.updatedAt || Date.now(),
})
await tx.flush()
} catch (err) {
await tx.close().catch(() => {})
throw err
}
}
async getNode(nodeId) {
await this.ready()
return this.db.get('@peardata/node', { nodeId: String(nodeId) })
}
async listNodes() {
await this.ready()
return this.db.find('@peardata/node').toArray()
}
// ── peer links ────────────────────────────────────────────
/**
* @param {object} link
*/
async putPeerLink(link) {
await this.ready()
const tx = await this.db.exclusiveTransaction()
try {
await tx.insert('@peardata/peer-link', {
localNodeId: String(link.localNodeId),
remotePublicKey: String(link.remotePublicKey).toLowerCase(),
role: link.role || 'viewer',
alias: link.alias || null,
dbKeyHex: link.dbKeyHex || null,
syncMode: link.syncMode || 'both',
linkedAt: link.linkedAt || Date.now(),
lastSeen: link.lastSeen || null,
})
await tx.flush()
} catch (err) {
await tx.close().catch(() => {})
throw err
}
}
async deletePeerLink(localNodeId, remotePublicKey) {
await this.ready()
const tx = await this.db.exclusiveTransaction()
try {
await tx.delete('@peardata/peer-link', {
localNodeId: String(localNodeId),
remotePublicKey: String(remotePublicKey).toLowerCase(),
})
await tx.flush()
} catch (err) {
await tx.close().catch(() => {})
throw err
}
}
async listPeerLinks(localNodeId = null) {
await this.ready()
if (!localNodeId) {
return this.db.find('@peardata/peer-link').toArray()
}
return this.db
.find('@peardata/peer-link', {
gte: { localNodeId: String(localNodeId), remotePublicKey: '' },
lte: { localNodeId: String(localNodeId), remotePublicKey: '\uffff' },
})
.toArray()
}
// ── alert configs ─────────────────────────────────────────
async putAlertConfig(cfg) {
await this.ready()
const tx = await this.db.exclusiveTransaction()
try {
await tx.insert('@peardata/alert-config', {
id: String(cfg.id),
chart: String(cfg.chart),
dimension: String(cfg.dimension),
warn: cfg.warn ?? null,
crit: cfg.crit ?? null,
comparator: cfg.comparator || '>',
enabled: cfg.enabled !== false,
info: cfg.info || null,
updatedAt: Date.now(),
})
await tx.flush()
} catch (err) {
await tx.close().catch(() => {})
throw err
}
}
async getAlertConfig(id) {
await this.ready()
return this.db.get('@peardata/alert-config', { id: String(id) })
}
async listAlertConfigs() {
await this.ready()
return this.db.find('@peardata/alert-config').toArray()
}
// ── alert events ──────────────────────────────────────────
async putAlertEvent(ev) {
await this.ready()
const tx = await this.db.exclusiveTransaction()
try {
await tx.insert('@peardata/alert-event', {
id: String(ev.id),
ts: Number(ev.ts) || Date.now(),
chart: String(ev.chart || ''),
dimension: ev.dimension || null,
severity: String(ev.severity || 'warning'),
value: ev.value ?? null,
threshold: ev.threshold ?? null,
message: ev.message || null,
cleared: Boolean(ev.cleared),
})
await tx.flush()
} catch (err) {
await tx.close().catch(() => {})
throw err
}
}
async listAlertEvents({ chart = null, limit = 100 } = {}) {
await this.ready()
let rows
if (chart) {
rows = await this.db
.find(
'@peardata/alert-event-by-chart',
{
gte: { chart: String(chart), ts: 0 },
lte: { chart: String(chart), ts: Number.MAX_SAFE_INTEGER },
},
{ reverse: true, limit }
)
.toArray()
} else {
rows = await this.db
.find('@peardata/alert-event', {}, { reverse: true, limit })
.toArray()
}
return rows
}
// ── warm metric points ────────────────────────────────────
/**
* Batch-insert warm (downsampled) metric points.
* @param {Array<{ chart: string, context: string, ts: number, tier?: number, values: object }>} points
*/
async putMetricPoints(points) {
if (!points?.length) return { inserted: 0 }
await this.ready()
const tx = await this.db.exclusiveTransaction()
try {
for (const p of points) {
await tx.insert('@peardata/metric-point', {
chart: String(p.chart),
ts: Number(p.ts),
context: String(p.context || p.chart),
tier: p.tier ?? 1,
valuesJson: JSON.stringify(p.values || {}),
})
}
await tx.flush()
return { inserted: points.length }
} catch (err) {
await tx.close().catch(() => {})
throw err
}
}
/**
* Query warm points for a chart in [afterMs, beforeMs].
* @param {{ chart: string, afterMs: number, beforeMs: number, limit?: number, tier?: number }} opts
*/
async queryMetricPoints(opts) {
await this.ready()
const chart = String(opts.chart)
const afterMs = Number(opts.afterMs) || 0
const beforeMs = Number(opts.beforeMs) || Date.now()
const limit = Math.min(opts.limit || 10_000, 50_000)
const rows = await this.db
.find(
'@peardata/metric-point',
{
gte: { chart, ts: afterMs },
lte: { chart, ts: beforeMs },
},
{ limit }
)
.toArray()
return rows
.filter((r) => (opts.tier == null ? true : r.tier === opts.tier))
.map((r) => ({
chart: r.chart,
context: r.context,
ts: r.ts,
tier: r.tier,
values: safeJson(r.valuesJson),
}))
}
// ── jobs ──────────────────────────────────────────────────
async putJob(job) {
await this.ready()
const tx = await this.db.exclusiveTransaction()
try {
await tx.insert('@peardata/job', {
id: String(job.id),
name: String(job.name),
status: String(job.status),
startedAt: job.startedAt ?? null,
finishedAt: job.finishedAt ?? null,
resultJson: job.result != null ? JSON.stringify(job.result) : job.resultJson || null,
error: job.error || null,
})
await tx.flush()
} catch (err) {
await tx.close().catch(() => {})
throw err
}
}
async listJobs(limit = 50) {
await this.ready()
const rows = await this.db.find('@peardata/job', {}, { reverse: true, limit }).toArray()
return rows
}
}
function safeJson(s) {
try {
return JSON.parse(s || '{}')
} catch {
return {}
}
}
+101
View File
@@ -0,0 +1,101 @@
/**
* Hyperswarm replication for the agent Corestore / HyperDB.
*
* Pattern (hyperdb-workshop bin.js):
* swarm.on('connection', (conn) => store.replicate(conn))
* swarm.join(db.discoveryKey, { server: true, client: true })
*
* Enable with PEARDATA_SWARM=1 (off by default — Phase B).
* Requires HyperDB to be open first.
*/
import Hyperswarm from 'hyperswarm'
import b4a from 'b4a'
import { getCorestore, getDb } from './index.js'
import logger from '../utils/logger.js'
const log = logger.child('db:replicate')
/** @type {import('hyperswarm')|null} */
let swarm = null
export function isSwarmEnabled() {
const v = process.env.PEARDATA_SWARM
return v === '1' || v === 'true' || v === 'on'
}
/**
* @returns {Promise<import('hyperswarm')|null>}
*/
export async function startReplication() {
if (!isSwarmEnabled()) {
log.info('Swarm replication disabled (set PEARDATA_SWARM=1 to enable)')
return null
}
const store = getCorestore()
const model = getDb()
if (!store || !model) {
log.warn('Cannot start swarm — HyperDB not open')
return null
}
if (swarm) return swarm
swarm = new Hyperswarm({
keyPair: await store.createKeyPair('peardata-swarm'),
})
swarm.on('connection', (conn, info) => {
const peer = info?.publicKey ? b4a.toString(info.publicKey, 'hex').slice(0, 12) : '?'
log.info('Swarm peer connected', { peer })
store.replicate(conn)
conn.on('close', () => log.info('Swarm peer disconnected', { peer }))
conn.on('error', (err) => log.warn('Swarm conn error', { peer, error: err.message }))
})
const topic = model.discoveryKey
swarm.join(topic, { server: true, client: true })
await swarm.flush()
log.info('Swarm joined HyperDB topic', {
discoveryKeyHex: model.discoveryKeyHex,
dbKeyHex: model.publicKeyHex,
})
return swarm
}
/**
* Join an additional remote DB discovery topic (pull mode for linked peers).
* @param {string|Buffer} discoveryKeyHexOrBuf
*/
export async function joinRemoteTopic(discoveryKeyHexOrBuf) {
if (!swarm) {
throw new Error('Swarm not started — enable PEARDATA_SWARM=1')
}
const key =
typeof discoveryKeyHexOrBuf === 'string'
? b4a.from(discoveryKeyHexOrBuf, 'hex')
: discoveryKeyHexOrBuf
swarm.join(key, { server: false, client: true })
log.info('Joined remote topic', {
discoveryKeyHex:
typeof discoveryKeyHexOrBuf === 'string'
? discoveryKeyHexOrBuf
: b4a.toString(discoveryKeyHexOrBuf, 'hex'),
})
}
export async function stopReplication() {
if (!swarm) return
try {
await swarm.destroy()
} catch {
// ignore
}
swarm = null
log.info('Swarm stopped')
}
export function getSwarm() {
return swarm
}
+67 -7
View File
@@ -10,7 +10,12 @@ import {
Roles,
} from '../../shared/protocol.js'
import { SCHEMA_VERSION } from '../../shared/schema.js'
import { CHART_DEFS, CHART_BY_ID, CONTEXT_IDS, chartSummary } from '../../shared/metrics.js'
import {
getAllChartDefs,
getContextIds,
CHART_BY_ID,
chartSummary,
} from '../../shared/metrics.js'
import { peers } from '../core/peer-registry.js'
import {
listPolicyPeers,
@@ -37,6 +42,8 @@ import {
} from '../services/subscriptions.js'
import { getJobs, knownJobNames } from '../services/jobs.js'
import { formatAllMetrics } from '../rest/formatters.js'
import { getDb } from '../db/index.js'
import { joinRemoteTopic, isSwarmEnabled } from '../db/replicate.js'
/**
* @param {import('../rpc/session.js').PeerSession} session
@@ -61,11 +68,13 @@ export function registerMonitorHandlers(session) {
publicKeyHex: getServerPublicKeyHex(),
hostname: os.hostname(),
platform: `${os.platform()}/${os.arch()}`,
uptimeSec: Math.floor(process.uptime()),
uptimeSec: Math.floor(
typeof process?.uptime === 'function' ? process.uptime() : 0
),
connectedPeers: peers.size(),
node: process.version,
node: typeof process?.version === 'string' ? process.version : 'bare',
role: 'agent',
charts: CHART_DEFS.length,
charts: getAllChartDefs().length,
}))
session.respond('getAuthStatus', async () => ({
@@ -88,8 +97,8 @@ export function registerMonitorHandlers(session) {
session.respond('getHealth', async () => anomalies.getHealth())
session.respond('listContexts', async () => ({
contexts: CONTEXT_IDS.map((id) => {
const charts = CHART_DEFS.filter((c) => c.context === id)
contexts: getContextIds().map((id) => {
const charts = getAllChartDefs().filter((c) => c.context === id)
return {
id,
family: charts[0]?.family || id.split('.')[0],
@@ -100,7 +109,7 @@ export function registerMonitorHandlers(session) {
}))
session.respond('getContext', async (args) => {
const charts = CHART_DEFS.filter((c) => c.context === args.id || c.id === args.id)
const charts = getAllChartDefs().filter((c) => c.context === args.id || c.id === args.id)
if (!charts.length) return { error: 'unknown context', id: args.id }
return {
id: args.id,
@@ -126,6 +135,57 @@ export function registerMonitorHandlers(session) {
session.respond('queryData', async (args) => store.query(args), { hot: true })
session.respond('getDbInfo', async () => {
const db = getDb()
if (!db) return { enabled: false }
return {
enabled: true,
publicKeyHex: db.publicKeyHex,
discoveryKeyHex: db.discoveryKeyHex,
swarm: isSwarmEnabled(),
}
})
session.respond('listPeerLinks', async () => {
const db = getDb()
if (!db) return { links: [], enabled: false }
const links = await db.listPeerLinks(getServerPublicKeyHex().slice(0, 16))
return { links, enabled: true }
})
session.respond('linkPeer', async (args) => {
const db = getDb()
if (!db) return { success: false, error: 'HyperDB disabled' }
const localNodeId = getServerPublicKeyHex().slice(0, 16)
await db.putPeerLink({
localNodeId,
remotePublicKey: args.remotePublicKey,
role: args.role || 'viewer',
alias: args.alias || null,
dbKeyHex: args.dbKeyHex || null,
syncMode: args.syncMode || 'both',
})
if (isSwarmEnabled() && args.discoveryKeyHex && (args.syncMode === 'pull' || args.syncMode === 'both')) {
try {
await joinRemoteTopic(args.discoveryKeyHex)
} catch (err) {
return {
success: true,
linked: true,
swarmWarning: err.message,
}
}
}
return { success: true, linked: true }
})
session.respond('unlinkPeer', async (args) => {
const db = getDb()
if (!db) return { success: false, error: 'HyperDB disabled' }
await db.deletePeerLink(getServerPublicKeyHex().slice(0, 16), args.remotePublicKey)
return { success: true }
})
session.respond('getAllMetrics', async (args) => formatAllMetrics(args.format || 'json'))
session.respond('subscribeMetrics', async (args, s) => subscribeMetrics(s, args), {
+18 -2
View File
@@ -1,5 +1,6 @@
/**
* Agent data pipeline: collector → store → anomaly → push fan-out.
* Agent data pipeline: collector → memory store → anomaly → push
* ↘ warm HyperDB flush
*/
import os from 'os'
import { getCollector } from './services/collector.js'
@@ -10,18 +11,25 @@ import {
broadcastAnomaly,
broadcastHealth,
} from './services/subscriptions.js'
import { enqueueWarmPoint, flushWarmPending, persistAlertEvent } from './services/warm-flush.js'
import { getDb } from './db/index.js'
import { Pushes } from '../shared/protocol.js'
import { peers } from './core/peer-registry.js'
import logger from './utils/logger.js'
const log = logger.child('pipeline')
let healthEvery = 0
let warmFlushEvery = 0
export function startPipeline() {
const collector = getCollector()
const store = getStore()
const anomalies = getAnomalyEngine(os.cpus().length)
store.on('warm', (point) => {
enqueueWarmPoint(point)
})
collector.on('samples', (batch) => {
store.ingest(batch)
broadcastMetrics(batch)
@@ -29,6 +37,7 @@ export function startPipeline() {
const fired = anomalies.evaluate(batch)
for (const ev of fired) {
broadcastAnomaly(ev)
persistAlertEvent(ev)
if (!ev.cleared) {
peers.broadcast(Pushes.alert, {
id: ev.id,
@@ -44,9 +53,16 @@ export function startPipeline() {
healthEvery = 0
broadcastHealth(anomalies.getHealth())
}
// flush warm HyperDB batch every ~10s
warmFlushEvery++
if (warmFlushEvery >= 10) {
warmFlushEvery = 0
flushWarmPending().catch(() => {})
}
})
collector.start()
log.info('Metrics pipeline started')
log.info('Metrics pipeline started', { hyperdb: Boolean(getDb()) })
return { collector, store, anomalies }
}
+1 -1
View File
@@ -58,7 +58,7 @@ function toShell(latest) {
for (const [chart, point] of Object.entries(latest)) {
for (const [dim, val] of Object.entries(point.values)) {
if (val == null || Number.isNaN(val)) continue
lines.push(`NETDATA_${chart.replace(/\./g, '_').toUpperCase()}_${dim.toUpperCase()}="${val}"`)
lines.push(`PEARDATA_${chart.replace(/\./g, '_').toUpperCase()}_${dim.toUpperCase()}="${val}"`)
}
}
return lines.join('\n') + '\n'
+6 -5
View File
@@ -1,10 +1,11 @@
/**
* Optional Netdata-style HTTP API bound to the PearMonitor agent.
* Optional agent-style HTTP API bound to the PearMonitor agent.
*
* Default: 127.0.0.1:19999 (Netdata's classic port) — local only.
* Default: 127.0.0.1:19999 — local only.
* Disable with PEARDATA_REST=0
*/
import http from 'http'
import b4a from 'b4a'
import { handleRest } from './routes.js'
import logger from '../utils/logger.js'
@@ -22,7 +23,7 @@ export function startRestServer() {
const host = process.env.PEARDATA_REST_HOST || '127.0.0.1'
const port = Number(process.env.PEARDATA_REST_PORT) || 19999
const server = http.createServer((req, res) => {
const server = http.createServer(async (req, res) => {
try {
const url = new URL(req.url || '/', `http://${host}:${port}`)
if (req.method !== 'GET' && req.method !== 'HEAD' && req.method !== 'OPTIONS') {
@@ -36,7 +37,7 @@ export function startRestServer() {
return
}
const result = handleRest(url.pathname, url.searchParams)
const result = await handleRest(url.pathname, url.searchParams)
const headers = {
...corsHeaders(),
'content-type': result.contentType || 'application/json',
@@ -47,7 +48,7 @@ export function startRestServer() {
typeof result.body === 'string' ? result.body : JSON.stringify(result.body, null, 0)
if (req.method === 'HEAD') {
headers['content-length'] = Buffer.byteLength(body)
headers['content-length'] = b4a.byteLength(body)
res.writeHead(result.status || 200, headers)
res.end()
return
+55 -18
View File
@@ -1,12 +1,16 @@
/**
* Netdata-compatible REST route handlers (v1 + v2 + v3).
* Agent REST route handlers (v1 + v2 + v3).
*
* Local-agent style GET endpoints. Not a byte-for-byte clone of every
* Netdata field, but intentional compatibility for charts/data/contexts/nodes/info/allmetrics.
* Local-agent style GET endpoints for charts/data/contexts/nodes/info/allmetrics.
*/
import os from 'os'
import { APP_NAME, APP_VERSION, PROTOCOL, PROTOCOL_VERSION } from '../../shared/protocol.js'
import { CHART_DEFS, CHART_BY_ID, CONTEXT_IDS, chartSummary } from '../../shared/metrics.js'
import {
getAllChartDefs,
getContextIds,
CHART_BY_ID,
chartSummary,
} from '../../shared/metrics.js'
import { getStore } from '../services/store.js'
import { getCollector } from '../services/collector.js'
import { getAnomalyEngine } from '../services/anomaly.js'
@@ -14,13 +18,15 @@ import { listAlerts, getAlert } from '../services/alerts.js'
import { getServerPublicKeyHex } from '../core/auth-keys.js'
import { formatAllMetrics } from './formatters.js'
import { peers } from '../core/peer-registry.js'
import { getDb, isHyperDbEnabled } from '../db/index.js'
import { isSwarmEnabled } from '../db/replicate.js'
/**
* @param {string} pathname
* @param {URLSearchParams} query
* @returns {{ status: number, contentType: string, body: any }}
*/
export function handleRest(pathname, query) {
export async function handleRest(pathname, query) {
const path = pathname.replace(/\/+$/, '') || '/'
// ── info / versions ──────────────────────────────────────
@@ -57,8 +63,8 @@ export function handleRest(pathname, query) {
if (path === '/api/v2/contexts' || path === '/api/v3/contexts') {
return json({
contexts: Object.fromEntries(
CONTEXT_IDS.map((id) => {
const charts = CHART_DEFS.filter((c) => c.context === id)
getContextIds().map((id) => {
const charts = getAllChartDefs().filter((c) => c.context === id)
return [
id,
{
@@ -74,7 +80,7 @@ export function handleRest(pathname, query) {
}
if (path === '/api/v3/context' || path === '/api/v2/context') {
const id = query.get('context') || query.get('id') || ''
const charts = CHART_DEFS.filter((c) => c.context === id || c.id === id)
const charts = getAllChartDefs().filter((c) => c.context === id || c.id === id)
if (!charts.length) return err(404, 'unknown context')
return json({
id,
@@ -113,8 +119,8 @@ export function handleRest(pathname, query) {
const chartId = chart.split(',')[0].trim()
const resolved = CHART_BY_ID.has(chartId)
? chartId
: CHART_DEFS.find((c) => c.context === chartId)?.id || chartId
const result = getStore().query({
: getAllChartDefs().find((c) => c.context === chartId)?.id || chartId
const result = await getStore().query({
chart: resolved,
after: num(query.get('after'), -60),
before: num(query.get('before'), 0),
@@ -150,14 +156,16 @@ export function handleRest(pathname, query) {
}
if (path === '/api/v3/q' || path === '/api/v2/q') {
const q = (query.get('q') || query.get('query') || '').toLowerCase()
const hits = CHART_DEFS.filter(
(c) =>
!q ||
c.id.includes(q) ||
c.title.toLowerCase().includes(q) ||
c.context.includes(q) ||
c.family.includes(q)
).map((c) => ({ type: 'chart', id: c.id, title: c.title, context: c.context }))
const hits = getAllChartDefs()
.filter(
(c) =>
!q ||
c.id.includes(q) ||
c.title.toLowerCase().includes(q) ||
c.context.includes(q) ||
c.family.includes(q)
)
.map((c) => ({ type: 'chart', id: c.id, title: c.title, context: c.context }))
return json({ results: hits, q })
}
@@ -242,6 +250,25 @@ export function handleRest(pathname, query) {
if (path === '/api/v1/health' || path === '/health' || path === '/api/v3/health') {
return json(getAnomalyEngine().getHealth())
}
if (path === '/api/v3/db' || path === '/api/v2/db') {
const db = getDb()
if (!db) return json({ enabled: false, hyperdb: isHyperDbEnabled() })
return json({
enabled: true,
publicKeyHex: db.publicKeyHex,
discoveryKeyHex: db.discoveryKeyHex,
swarm: isSwarmEnabled(),
collections: [
'@peardata/node',
'@peardata/peer-link',
'@peardata/alert-config',
'@peardata/alert-event',
'@peardata/metric-point',
'@peardata/job',
],
})
}
if (path === '/' || path === '/api') {
return json({
name: APP_NAME,
@@ -249,6 +276,9 @@ export function handleRest(pathname, query) {
apis: ['/api/v1', '/api/v2', '/api/v3'],
docs: 'See docs/REST-API.md',
p2p: { protocol: PROTOCOL, publicKeyHex: getServerPublicKeyHex() },
hyperdb: getDb()
? { publicKeyHex: getDb().publicKeyHex, discoveryKeyHex: getDb().discoveryKeyHex }
: null,
})
}
@@ -275,6 +305,13 @@ function infoPayload() {
protocol: PROTOCOL,
protocolVersion: PROTOCOL_VERSION,
publicKeyHex: getServerPublicKeyHex(),
hyperdb: getDb()
? {
publicKeyHex: getDb().publicKeyHex,
discoveryKeyHex: getDb().discoveryKeyHex,
swarm: isSwarmEnabled(),
}
: null,
},
}
}
+37 -2
View File
@@ -2,9 +2,11 @@
* PearData / PearMonitor agent entry point.
*
* - HyperDHT listener + protomux-rpc (P2P control + metric streams)
* - Optional Netdata-compatible REST API (default :19999)
* - 1s system metrics pipeline
* - HyperDB (Corestore) for warm history + metadata
* - Optional Hyperswarm replication (PEARDATA_SWARM=1)
* - Optional agent-compatible REST API (default :19999)
*/
import os from 'os'
import DHT from 'hyperdht'
import b4a from 'b4a'
import gracefulGoodbye from 'graceful-goodbye'
@@ -16,9 +18,12 @@ import { registerAllHandlers, cleanupSession } from './rpc/register.js'
import { isPeerRevoked, loadPeerPolicy } from './core/peer-policy.js'
import { isInsecureOpenAdmin } from '../shared/crypto-auth.js'
import { APP_NAME, APP_VERSION } from '../shared/protocol.js'
import { openDb, closeDb, getDb } from './db/index.js'
import { startReplication, stopReplication } from './db/replicate.js'
import { startPipeline } from './pipeline.js'
import { startRestServer } from './rest/http-server.js'
import { getCollector } from './services/collector.js'
import { flushWarmPending } from './services/warm-flush.js'
import logger from './utils/logger.js'
const bootStarted = Date.now()
@@ -43,6 +48,26 @@ if (isInsecureOpenAdmin()) {
)
}
const db = await openDb()
if (db) {
try {
await db.putNode({
nodeId: publicKeyHex.slice(0, 16),
hostname: os.hostname(),
publicKeyHex,
platform: os.platform(),
arch: os.arch(),
cpus: os.cpus().length,
totalMemMiB: Math.floor(os.totalmem() / (1024 * 1024)),
agentVersion: APP_VERSION,
updatedAt: Date.now(),
})
} catch (err) {
log.warn('Failed to upsert local node record', { error: err.message })
}
}
await startReplication()
startPipeline()
const restServer = startRestServer()
@@ -91,6 +116,7 @@ await server.listen(keyPair)
const bootMs = Date.now() - bootStarted
const restPort = Number(process.env.PEARDATA_REST_PORT) || 19999
const restHost = process.env.PEARDATA_REST_HOST || '127.0.0.1'
const model = getDb()
logger.banner({
title: `${APP_NAME} agent v${APP_VERSION}`,
@@ -98,6 +124,8 @@ logger.banner({
connect: `Client → dial ${publicKeyHex}`,
admin: 'Use SERVER_SEED as admin proof, or mint pd1 invites',
rest: restServer ? `http://${restHost}:${restPort}/api/v3/info` : 'disabled',
hyperdb: model ? model.publicKeyHex : 'disabled',
swarm: process.env.PEARDATA_SWARM === '1' ? 'on' : 'off',
insecure: isInsecureOpenAdmin() ? 'OPEN ADMIN (dev)' : 'secure defaults',
bootMs: `${bootMs}ms`,
})
@@ -111,6 +139,11 @@ async function shutdown() {
} catch {
// ignore
}
try {
await flushWarmPending()
} catch {
// ignore
}
for (const s of peers.list()) {
try {
s.destroy()
@@ -131,6 +164,8 @@ async function shutdown() {
} catch {
// ignore
}
await stopReplication()
await closeDb()
process.exit(0)
}
+20
View File
@@ -20,6 +20,26 @@ export const DEFAULT_THRESHOLDS = [
enabled: true,
info: 'CPU user time high',
},
{
id: 'cpu_iowait_high',
chart: 'system.cpu',
dimension: 'iowait',
warn: 40,
crit: 70,
comparator: '>',
enabled: true,
info: 'CPU iowait high',
},
{
id: 'cpu_steal_high',
chart: 'system.cpu',
dimension: 'steal',
warn: 10,
crit: 25,
comparator: '>',
enabled: true,
info: 'CPU steal time high (hypervisor contention)',
},
{
id: 'load1_high',
chart: 'system.load',
File diff suppressed because it is too large Load Diff
+45 -8
View File
@@ -1,20 +1,23 @@
/**
* In-memory tiered metric ring buffers.
*
* Tier 0: high-res (1s) short retention
* Tier 1: downsampled (avg over window) longer retention
* Tier 0: high-res (1s) short retention (hot path)
* Tier 1: downsampled averages — also flushed to HyperDB warm storage
*
* Future: Hypercore / disk-backed persistence.
* See docs/STORAGE-HYPERDB.md
*/
import { EventEmitter } from 'events'
import { SAMPLE_INTERVAL_MS, CHART_BY_ID, chartSummary } from '../../shared/metrics.js'
import { getDb } from '../db/index.js'
function envInt(name, fallback) {
const n = Number(process.env[name])
return Number.isFinite(n) && n > 0 ? n : fallback
}
export class MetricStore {
export class MetricStore extends EventEmitter {
constructor() {
super()
this.tier0Max = envInt('PEARDATA_TIER0_POINTS', 3600) // 1h @ 1s
this.tier1Max = envInt('PEARDATA_TIER1_POINTS', 1440) // 24h @ 1m
this.tier1Every = envInt('PEARDATA_TIER1_EVERY', 60) // downsample every N samples
@@ -54,12 +57,20 @@ export class MetricStore {
for (const [k, v] of Object.entries(entry.acc)) {
avg[k] = entry.accCount ? v / entry.accCount : null
}
entry.tier1.push({ ts: s.ts, values: avg })
const warm = { ts: s.ts, values: avg }
entry.tier1.push(warm)
if (entry.tier1.length > this.tier1Max) {
entry.tier1.splice(0, entry.tier1.length - this.tier1Max)
}
entry.acc = null
entry.accCount = 0
this.emit('warm', {
chart: s.chart,
context: s.context,
ts: warm.ts,
values: warm.values,
tier: 1,
})
}
}
}
@@ -91,11 +102,12 @@ export class MetricStore {
}
/**
* Query points for a chart (Netdata-like after/before/points).
* Query points for a chart (after/before/points windowing).
* Memory first; optional HyperDB warm fallback when window exceeds hot buffer.
*
* @param {{ chart: string, after?: number, before?: number, points?: number, group?: string, tier?: number }} opts
*/
query(opts) {
async query(opts) {
const chart = opts.chart
const entry = this.series.get(chart)
const def = CHART_BY_ID.get(chart)
@@ -116,10 +128,34 @@ export class MetricStore {
const beforeMs = before * 1000
let windowed = src.filter((p) => p.ts >= afterMs && p.ts <= beforeMs)
let source = useTier1 ? 'memory-tier1' : 'memory-tier0'
// HyperDB warm fallback when memory misses (or explicit tier>=1 with sparse memory)
if (!windowed.length || (opts.tier >= 1 && windowed.length < (opts.points || 60) / 2)) {
const db = getDb()
if (db) {
try {
const warm = await db.queryMetricPoints({
chart,
afterMs,
beforeMs,
limit: opts.points || 10_000,
tier: 1,
})
if (warm.length) {
windowed = warm
source = 'hyperdb-warm'
}
} catch {
// keep memory result
}
}
}
if (!windowed.length && src.length) {
// fall back to latest N
const n = Math.min(opts.points || 60, src.length)
windowed = src.slice(-n)
source = useTier1 ? 'memory-tier1' : 'memory-tier0'
}
const want = Math.min(opts.points || windowed.length || 60, 10_000)
@@ -145,6 +181,7 @@ export class MetricStore {
before: before,
points: data.length,
format: 'json',
source,
}
}
+65
View File
@@ -0,0 +1,65 @@
/**
* Flush memory tier1 (downsampled) points into HyperDB warm storage.
*
* Called from the pipeline when the in-memory store rolls a tier1 bucket,
* or on a periodic timer as a safety net.
*/
import { getDb } from '../db/index.js'
import logger from '../utils/logger.js'
const log = logger.child('warm-flush')
/** @type {Array<{ chart: string, context: string, ts: number, values: object }>} */
let pending = []
let flushing = false
/**
* Queue a warm point for HyperDB persistence.
* @param {{ chart: string, context: string, ts: number, values: object }} point
*/
export function enqueueWarmPoint(point) {
pending.push({
chart: point.chart,
context: point.context || point.chart,
ts: point.ts,
values: point.values,
tier: 1,
})
}
/**
* Flush pending warm points to HyperDB.
*/
export async function flushWarmPending() {
const db = getDb()
if (!db || !pending.length || flushing) return { inserted: 0 }
flushing = true
const batch = pending
pending = []
try {
const res = await db.putMetricPoints(batch)
log.debug?.('Flushed warm points', res)
return res
} catch (err) {
// re-queue on failure (bounded)
pending = batch.concat(pending).slice(-5000)
log.error('Warm flush failed', { error: err.message, requeued: batch.length })
return { inserted: 0, error: err.message }
} finally {
flushing = false
}
}
/**
* Persist an anomaly/alert event to HyperDB.
* @param {object} ev
*/
export async function persistAlertEvent(ev) {
const db = getDb()
if (!db) return
try {
await db.putAlertEvent(ev)
} catch (err) {
log.warn('Failed to persist alert event', { error: err.message })
}
}
+51 -40
View File
@@ -2,6 +2,7 @@
* HMAC capability grants + admin seed proof for handshake auth.
*
* Pure helpers shared by server and client. Never logs secrets.
* Uses `b4a` + `crypto` (mapped to bare-crypto under Bare/Pear).
*
* Capability token format:
* base64url(JSON payload) + "." + base64url(HMAC-SHA256(macKey, payloadBytes))
@@ -13,32 +14,39 @@
* pd1.<base64url JSON { publicKeyHex, capability, role, ... }>
*/
import crypto from 'crypto'
import b4a from 'b4a'
import { Roles } from './protocol.js'
const SALT = Buffer.from('peardata-hmac-v1', 'utf8')
const INFO_CAPABILITY = Buffer.from('capability', 'utf8')
const ADMIN_PREFIX = Buffer.from('peardata-admin-v1', 'utf8')
const SALT = b4a.from('peardata-hmac-v1')
const INFO_CAPABILITY = b4a.from('capability')
const ADMIN_PREFIX = b4a.from('peardata-admin-v1')
const VALID_ROLES = new Set([Roles.viewer, Roles.operator, Roles.admin])
export const INVITE_PREFIX = 'pd1.'
function asU8(value) {
if (value == null) return b4a.alloc(0)
if (value instanceof Uint8Array) return b4a.from(value)
return b4a.from(value)
}
/**
* @param {string|Uint8Array|Buffer} seedHexOrBuf
* @returns {Buffer}
* @param {string|Uint8Array} seedHexOrBuf
* @returns {Uint8Array}
*/
export function deriveMacKey(seedHexOrBuf) {
const ikm = toSeedBuffer(seedHexOrBuf)
if (typeof crypto.hkdfSync === 'function') {
return Buffer.from(crypto.hkdfSync('sha256', ikm, SALT, INFO_CAPABILITY, 32))
return asU8(crypto.hkdfSync('sha256', ikm, SALT, INFO_CAPABILITY, 32))
}
const prk = crypto.createHmac('sha256', SALT).update(ikm).digest()
const info = Buffer.concat([INFO_CAPABILITY, Buffer.from([0x01])])
return crypto.createHmac('sha256', prk).update(info).digest()
const info = b4a.concat([INFO_CAPABILITY, b4a.from([0x01])])
return asU8(crypto.createHmac('sha256', prk).update(info).digest())
}
function toSeedBuffer(seedHexOrBuf) {
if (Buffer.isBuffer(seedHexOrBuf) || seedHexOrBuf instanceof Uint8Array) {
const buf = Buffer.from(seedHexOrBuf)
if (seedHexOrBuf instanceof Uint8Array) {
const buf = b4a.from(seedHexOrBuf)
if (buf.length !== 32) throw new Error('Seed must be 32 bytes')
return buf
}
@@ -48,12 +56,12 @@ function toSeedBuffer(seedHexOrBuf) {
if (!/^[0-9a-f]{64}$/.test(hex)) {
throw new Error('Seed must be 64 hex characters (32 bytes)')
}
return Buffer.from(hex, 'hex')
return b4a.from(hex, 'hex')
}
export function b64url(buf) {
return Buffer.from(buf)
.toString('base64')
return b4a
.toString(asU8(buf), 'base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '')
@@ -62,7 +70,7 @@ export function b64url(buf) {
export function b64urlDecode(s) {
const str = String(s || '').replace(/-/g, '+').replace(/_/g, '/')
const pad = str.length % 4 === 0 ? '' : '='.repeat(4 - (str.length % 4))
return Buffer.from(str + pad, 'base64')
return b4a.from(str + pad, 'base64')
}
export function canonicalizePayload(payload) {
@@ -74,32 +82,30 @@ export function canonicalizePayload(payload) {
jti: payload.jti,
iat: payload.iat,
}
return Buffer.from(JSON.stringify(ordered), 'utf8')
return b4a.from(JSON.stringify(ordered))
}
function resolveMacKey(macKeyOrSeed) {
if (Buffer.isBuffer(macKeyOrSeed) || macKeyOrSeed instanceof Uint8Array) {
const buf = Buffer.from(macKeyOrSeed)
if (macKeyOrSeed instanceof Uint8Array) {
const buf = b4a.from(macKeyOrSeed)
if (buf.length === 32) return buf
return deriveMacKey(macKeyOrSeed)
}
if (typeof macKeyOrSeed === 'string' && /^[0-9a-fA-F]{64}$/.test(macKeyOrSeed.trim())) {
return deriveMacKey(macKeyOrSeed.trim())
}
if (Buffer.isBuffer(macKeyOrSeed) || macKeyOrSeed instanceof Uint8Array) {
return deriveMacKey(macKeyOrSeed)
}
throw new Error('Invalid mac key or seed')
}
export function safeEqual(a, b) {
if (!Buffer.isBuffer(a)) a = Buffer.from(a)
if (!Buffer.isBuffer(b)) b = Buffer.from(b)
if (a.length !== b.length) return false
return crypto.timingSafeEqual(a, b)
const aa = asU8(a)
const bb = asU8(b)
if (aa.length !== bb.length) return false
return crypto.timingSafeEqual(aa, bb)
}
/**
* @param {Buffer|string} macKeyOrSeed
* @param {Uint8Array|string} macKeyOrSeed
* @param {{ role?: string, ttlMs?: number|null, peerId?: string|null, jti?: string, forever?: boolean }} opts
*/
export function signCapability(macKeyOrSeed, opts = {}) {
@@ -124,7 +130,7 @@ export function signCapability(macKeyOrSeed, opts = {}) {
role,
peerId: opts.peerId ? String(opts.peerId).toLowerCase() : null,
exp,
jti: opts.jti || crypto.randomBytes(16).toString('hex'),
jti: opts.jti || b4a.toString(crypto.randomBytes(16), 'hex'),
iat: now,
}
const body = canonicalizePayload(payload)
@@ -133,7 +139,7 @@ export function signCapability(macKeyOrSeed, opts = {}) {
}
/**
* @param {Buffer|string} macKeyOrSeed
* @param {Uint8Array|string} macKeyOrSeed
* @param {string} token
* @param {{ peerId?: string, now?: number, allowSpentCheck?: (jti: string) => boolean }} [opts]
*/
@@ -165,7 +171,7 @@ export function verifyCapability(macKeyOrSeed, token, opts = {}) {
let payload
try {
payload = JSON.parse(body.toString('utf8'))
payload = JSON.parse(b4a.toString(body, 'utf8'))
} catch {
return { ok: false, error: 'Capability payload not JSON', code: 'CAPABILITY_INVALID' }
}
@@ -217,18 +223,18 @@ function hmacAdmin(macKey, nonce, peerId, serverPk) {
return crypto
.createHmac('sha256', macKey)
.update(ADMIN_PREFIX)
.update(Buffer.from(nonce, 'utf8'))
.update(Buffer.from(peerId, 'utf8'))
.update(Buffer.from(serverPk, 'utf8'))
.update(b4a.from(nonce))
.update(b4a.from(peerId))
.update(b4a.from(serverPk))
.digest()
}
/**
* @param {Buffer|string} macKeyOrSeed
* @param {Uint8Array|string} macKeyOrSeed
* @param {{ nonce?: string, peerId: string, serverPublicKeyHex: string }} opts
*/
export function createAdminProof(macKeyOrSeed, opts) {
const nonce = String(opts.nonce || crypto.randomBytes(16).toString('hex'))
const nonce = String(opts.nonce || b4a.toString(crypto.randomBytes(16), 'hex'))
if (!/^[0-9a-fA-F]{16,64}$/.test(nonce)) {
throw new Error('Admin proof nonce must be 16-64 hex characters')
}
@@ -239,11 +245,11 @@ export function createAdminProof(macKeyOrSeed, opts) {
const macKey = resolveMacKey(macKeyOrSeed)
const mac = hmacAdmin(macKey, nonce, peerId, serverPk)
return { nonce: nonce.toLowerCase(), mac: mac.toString('hex') }
return { nonce: nonce.toLowerCase(), mac: b4a.toString(asU8(mac), 'hex') }
}
/**
* @param {Buffer|string} macKeyOrSeed
* @param {Uint8Array|string} macKeyOrSeed
* @param {{ nonce?: string, mac?: string }|null} proof
* @param {{ peerId: string, serverPublicKeyHex: string }} ctx
*/
@@ -264,7 +270,7 @@ export function verifyAdminProof(macKeyOrSeed, proof, ctx) {
const macKey = resolveMacKey(macKeyOrSeed)
const expected = hmacAdmin(macKey, nonce, peerId, serverPk)
const got = Buffer.from(macHex, 'hex')
const got = b4a.from(macHex, 'hex')
if (!safeEqual(got, expected)) {
return { ok: false, error: 'Admin proof verification failed', code: 'ADMIN_PROOF_FAILED' }
}
@@ -291,7 +297,7 @@ export function encodeInvite(pkg) {
if (!body.capability || !body.capability.includes('.')) {
throw new Error('encodeInvite: invalid capability')
}
return `${INVITE_PREFIX}${b64url(Buffer.from(JSON.stringify(body), 'utf8'))}`
return `${INVITE_PREFIX}${b64url(b4a.from(JSON.stringify(body)))}`
}
/**
@@ -304,7 +310,7 @@ export function decodeInvite(invite) {
return { ok: false, error: 'Not a pd1 invite', code: 'INVITE_INVALID' }
}
try {
const json = b64urlDecode(s.slice(INVITE_PREFIX.length)).toString('utf8')
const json = b4a.toString(b64urlDecode(s.slice(INVITE_PREFIX.length)), 'utf8')
const pkg = JSON.parse(json)
if (!pkg?.publicKeyHex || !pkg?.capability) {
return { ok: false, error: 'Invite missing fields', code: 'INVITE_INVALID' }
@@ -343,6 +349,11 @@ export function classifyConnectionInput(input) {
}
export function isInsecureOpenAdmin() {
const v = String(process.env.PEARDATA_INSECURE_OPEN_ADMIN || '').toLowerCase()
return v === '1' || v === 'true' || v === 'yes'
try {
const env = typeof process !== 'undefined' ? process.env : null
const v = String(env?.PEARDATA_INSECURE_OPEN_ADMIN || '').toLowerCase()
return v === '1' || v === 'true' || v === 'yes'
} catch {
return false
}
}
+718 -69
View File
@@ -1,8 +1,9 @@
/**
* Metric contexts, charts, and dimension catalog (Netdata-inspired).
* Metric contexts, charts, and dimension catalog.
*
* Context IDs follow Netdata style: family.metric (e.g. system.cpu).
* Chart IDs are unique per agent instance.
* Context IDs follow family.metric style (e.g. system.cpu).
* Static charts cover host-wide system stats; instance charts
* (per-CPU, per-disk, per-iface, per-mount) are registered at runtime.
*/
export const SAMPLE_INTERVAL_MS = 1000
@@ -19,11 +20,13 @@ export const SAMPLE_INTERVAL_MS = 1000
* chartType: 'line'|'area'|'stacked',
* priority: number,
* dimensions: DimensionDef[],
* plugin?: string,
* }} ChartDef
*/
/** @type {ChartDef[]} */
export const CHART_DEFS = [
export const STATIC_CHART_DEFS = [
// ── CPU / scheduler ───────────────────────────────────────
{
id: 'system.cpu',
name: 'system.cpu',
@@ -33,74 +36,55 @@ export const CHART_DEFS = [
family: 'cpu',
chartType: 'stacked',
priority: 100,
plugin: 'proc',
dimensions: [
{ id: 'guest_nice', name: 'guest_nice', algorithm: 'absolute' },
{ id: 'guest', name: 'guest', algorithm: 'absolute' },
{ id: 'steal', name: 'steal', algorithm: 'absolute' },
{ id: 'softirq', name: 'softirq', algorithm: 'absolute' },
{ id: 'irq', name: 'irq', algorithm: 'absolute' },
{ id: 'user', name: 'user', algorithm: 'absolute' },
{ id: 'system', name: 'system', algorithm: 'absolute' },
{ id: 'nice', name: 'nice', algorithm: 'absolute' },
{ id: 'iowait', name: 'iowait', algorithm: 'absolute' },
{ id: 'irq', name: 'irq', algorithm: 'absolute' },
{ id: 'softirq', name: 'softirq', algorithm: 'absolute' },
{ id: 'idle', name: 'idle', algorithm: 'absolute' },
],
},
{
id: 'system.ram',
name: 'system.ram',
context: 'system.ram',
title: 'System RAM',
units: 'MiB',
family: 'memory',
chartType: 'stacked',
priority: 200,
dimensions: [
{ id: 'used', name: 'used', algorithm: 'absolute' },
{ id: 'cached', name: 'cached', algorithm: 'absolute' },
{ id: 'buffers', name: 'buffers', algorithm: 'absolute' },
{ id: 'free', name: 'free', algorithm: 'absolute' },
],
},
{
id: 'system.load',
name: 'system.load',
context: 'system.load',
title: 'System Load Average',
units: 'load',
family: 'load',
id: 'system.intr',
name: 'system.intr',
context: 'system.intr',
title: 'CPU Interrupts',
units: 'interrupts/s',
family: 'interrupts',
chartType: 'line',
priority: 300,
dimensions: [
{ id: 'load1', name: 'load1', algorithm: 'absolute' },
{ id: 'load5', name: 'load5', algorithm: 'absolute' },
{ id: 'load15', name: 'load15', algorithm: 'absolute' },
],
priority: 110,
plugin: 'proc',
dimensions: [{ id: 'interrupts', name: 'interrupts', algorithm: 'incremental' }],
},
{
id: 'system.io',
name: 'system.io',
context: 'system.io',
title: 'Disk I/O',
units: 'KiB/s',
family: 'disk',
chartType: 'area',
priority: 400,
dimensions: [
{ id: 'reads', name: 'reads', algorithm: 'incremental' },
{ id: 'writes', name: 'writes', algorithm: 'incremental' },
],
id: 'system.ctxt',
name: 'system.ctxt',
context: 'system.ctxt',
title: 'CPU Context Switches',
units: 'context switches/s',
family: 'processes',
chartType: 'line',
priority: 120,
plugin: 'proc',
dimensions: [{ id: 'switches', name: 'switches', algorithm: 'incremental' }],
},
{
id: 'system.net',
name: 'system.net',
context: 'system.net',
title: 'Bandwidth',
units: 'kilobits/s',
family: 'network',
chartType: 'area',
priority: 500,
dimensions: [
{ id: 'received', name: 'received', algorithm: 'incremental' },
{ id: 'sent', name: 'sent', algorithm: 'incremental' },
],
id: 'system.forks',
name: 'system.forks',
context: 'system.forks',
title: 'Started Processes',
units: 'processes/s',
family: 'processes',
chartType: 'line',
priority: 130,
plugin: 'proc',
dimensions: [{ id: 'started', name: 'started', algorithm: 'incremental' }],
},
{
id: 'system.processes',
@@ -110,11 +94,39 @@ export const CHART_DEFS = [
units: 'processes',
family: 'processes',
chartType: 'line',
priority: 600,
priority: 140,
plugin: 'proc',
dimensions: [
{ id: 'running', name: 'running', algorithm: 'absolute' },
{ id: 'blocked', name: 'blocked', algorithm: 'absolute' },
{ id: 'total', name: 'total', algorithm: 'absolute' },
],
},
{
id: 'system.active_processes',
name: 'system.active_processes',
context: 'system.active_processes',
title: 'System Active Processes',
units: 'processes',
family: 'processes',
chartType: 'line',
priority: 145,
plugin: 'proc',
dimensions: [{ id: 'active', name: 'active', algorithm: 'absolute' }],
},
{
id: 'system.load',
name: 'system.load',
context: 'system.load',
title: 'System Load Average',
units: 'load',
family: 'load',
chartType: 'line',
priority: 150,
plugin: 'proc',
dimensions: [
{ id: 'load1', name: 'load1', algorithm: 'absolute' },
{ id: 'load5', name: 'load5', algorithm: 'absolute' },
{ id: 'load15', name: 'load15', algorithm: 'absolute' },
],
},
{
@@ -125,37 +137,673 @@ export const CHART_DEFS = [
units: 'seconds',
family: 'uptime',
chartType: 'line',
priority: 700,
priority: 160,
plugin: 'proc',
dimensions: [{ id: 'uptime', name: 'uptime', algorithm: 'absolute' }],
},
{
id: 'system.entropy',
name: 'system.entropy',
context: 'system.entropy',
title: 'Available Entropy',
units: 'entropy',
family: 'entropy',
chartType: 'line',
priority: 170,
plugin: 'proc',
dimensions: [{ id: 'entropy', name: 'entropy', algorithm: 'absolute' }],
},
// ── Memory ────────────────────────────────────────────────
{
id: 'system.ram',
name: 'system.ram',
context: 'system.ram',
title: 'System RAM',
units: 'MiB',
family: 'ram',
chartType: 'stacked',
priority: 200,
plugin: 'proc',
dimensions: [
{ id: 'free', name: 'free', algorithm: 'absolute' },
{ id: 'used', name: 'used', algorithm: 'absolute' },
{ id: 'cached', name: 'cached', algorithm: 'absolute' },
{ id: 'buffers', name: 'buffers', algorithm: 'absolute' },
],
},
{
id: 'mem.available',
name: 'mem.available',
context: 'mem.available',
title: 'Available RAM',
title: 'Available RAM for applications',
units: 'MiB',
family: 'memory',
family: 'ram',
chartType: 'area',
priority: 210,
plugin: 'proc',
dimensions: [{ id: 'avail', name: 'avail', algorithm: 'absolute' }],
},
{
id: 'mem.swap',
name: 'mem.swap',
context: 'mem.swap',
title: 'System Swap',
units: 'MiB',
family: 'swap',
chartType: 'stacked',
priority: 220,
plugin: 'proc',
dimensions: [
{ id: 'free', name: 'free', algorithm: 'absolute' },
{ id: 'used', name: 'used', algorithm: 'absolute' },
],
},
{
id: 'mem.swap_cached',
name: 'mem.swap_cached',
context: 'mem.swap_cached',
title: 'Swap Cached',
units: 'MiB',
family: 'swap',
chartType: 'area',
priority: 225,
plugin: 'proc',
dimensions: [{ id: 'cached', name: 'cached', algorithm: 'absolute' }],
},
{
id: 'mem.kernel',
name: 'mem.kernel',
context: 'mem.kernel',
title: 'Memory Used by Kernel',
units: 'MiB',
family: 'kernel',
chartType: 'stacked',
priority: 230,
plugin: 'proc',
dimensions: [
{ id: 'slab', name: 'slab', algorithm: 'absolute' },
{ id: 'kernel_stack', name: 'kernel_stack', algorithm: 'absolute' },
{ id: 'page_tables', name: 'page_tables', algorithm: 'absolute' },
{ id: 'vmalloc_used', name: 'vmalloc_used', algorithm: 'absolute' },
],
},
{
id: 'mem.slab',
name: 'mem.slab',
context: 'mem.slab',
title: 'Reclaimable Kernel Memory',
units: 'MiB',
family: 'slab',
chartType: 'stacked',
priority: 235,
plugin: 'proc',
dimensions: [
{ id: 'reclaimable', name: 'reclaimable', algorithm: 'absolute' },
{ id: 'unreclaimable', name: 'unreclaimable', algorithm: 'absolute' },
],
},
{
id: 'mem.writeback',
name: 'mem.writeback',
context: 'mem.writeback',
title: 'Writeback Memory',
units: 'MiB',
family: 'writeback',
chartType: 'line',
priority: 240,
plugin: 'proc',
dimensions: [
{ id: 'dirty', name: 'dirty', algorithm: 'absolute' },
{ id: 'writeback', name: 'writeback', algorithm: 'absolute' },
],
},
{
id: 'mem.committed',
name: 'mem.committed',
context: 'mem.committed',
title: 'Committed (Allocated) Memory',
units: 'MiB',
family: 'committed',
chartType: 'area',
priority: 245,
plugin: 'proc',
dimensions: [{ id: 'Committed_AS', name: 'Committed_AS', algorithm: 'absolute' }],
},
{
id: 'mem.swapio',
name: 'mem.swapio',
context: 'mem.swapio',
title: 'Swap I/O',
units: 'KiB/s',
family: 'swap',
chartType: 'area',
priority: 250,
plugin: 'proc',
dimensions: [
{ id: 'in', name: 'in', algorithm: 'incremental' },
{ id: 'out', name: 'out', algorithm: 'incremental' },
],
},
{
id: 'system.pgpgio',
name: 'system.pgpgio',
context: 'system.pgpgio',
title: 'Memory Page I/O',
units: 'KiB/s',
family: 'pgpgio',
chartType: 'area',
priority: 260,
plugin: 'proc',
dimensions: [
{ id: 'in', name: 'in', algorithm: 'incremental' },
{ id: 'out', name: 'out', algorithm: 'incremental' },
],
},
{
id: 'system.pgfaults',
name: 'system.pgfaults',
context: 'system.pgfaults',
title: 'Memory Page Faults',
units: 'faults/s',
family: 'pgfaults',
chartType: 'line',
priority: 270,
plugin: 'proc',
dimensions: [
{ id: 'minor', name: 'minor', algorithm: 'incremental' },
{ id: 'major', name: 'major', algorithm: 'incremental' },
],
},
// ── Aggregate disk / network ──────────────────────────────
{
id: 'system.io',
name: 'system.io',
context: 'system.io',
title: 'Disk I/O',
units: 'KiB/s',
family: 'disk',
chartType: 'area',
priority: 400,
plugin: 'proc',
dimensions: [
{ id: 'in', name: 'in', algorithm: 'incremental' },
{ id: 'out', name: 'out', algorithm: 'incremental' },
],
},
{
id: 'system.net',
name: 'system.net',
context: 'system.net',
title: 'Physical Network Interfaces Bandwidth',
units: 'kilobits/s',
family: 'network',
chartType: 'area',
priority: 500,
plugin: 'proc',
dimensions: [
{ id: 'received', name: 'received', algorithm: 'incremental' },
{ id: 'sent', name: 'sent', algorithm: 'incremental' },
],
},
{
id: 'system.ip',
name: 'system.ip',
context: 'system.ip',
title: 'IP Bandwidth',
units: 'kilobits/s',
family: 'ip',
chartType: 'area',
priority: 600,
plugin: 'proc',
dimensions: [
{ id: 'received', name: 'received', algorithm: 'incremental' },
{ id: 'sent', name: 'sent', algorithm: 'incremental' },
],
},
{
id: 'system.ipv6',
name: 'system.ipv6',
context: 'system.ipv6',
title: 'IPv6 Bandwidth',
units: 'kilobits/s',
family: 'ipv6',
chartType: 'area',
priority: 610,
plugin: 'proc',
dimensions: [
{ id: 'received', name: 'received', algorithm: 'incremental' },
{ id: 'sent', name: 'sent', algorithm: 'incremental' },
],
},
// ── IP / TCP / UDP aggregates ─────────────────────────────
{
id: 'ip.tcppackets',
name: 'ip.tcppackets',
context: 'ip.tcppackets',
title: 'TCP Packets',
units: 'packets/s',
family: 'tcp',
chartType: 'line',
priority: 700,
plugin: 'proc',
dimensions: [
{ id: 'received', name: 'received', algorithm: 'incremental' },
{ id: 'sent', name: 'sent', algorithm: 'incremental' },
],
},
{
id: 'ip.tcperrors',
name: 'ip.tcperrors',
context: 'ip.tcperrors',
title: 'TCP Errors',
units: 'packets/s',
family: 'tcp',
chartType: 'line',
priority: 710,
plugin: 'proc',
dimensions: [
{ id: 'InErrs', name: 'InErrs', algorithm: 'incremental' },
{ id: 'InCsumErrors', name: 'InCsumErrors', algorithm: 'incremental' },
{ id: 'RetransSegs', name: 'RetransSegs', algorithm: 'incremental' },
],
},
{
id: 'ip.tcpopens',
name: 'ip.tcpopens',
context: 'ip.tcpopens',
title: 'TCP Connections',
units: 'connections/s',
family: 'tcp',
chartType: 'line',
priority: 720,
plugin: 'proc',
dimensions: [
{ id: 'active', name: 'active', algorithm: 'incremental' },
{ id: 'passive', name: 'passive', algorithm: 'incremental' },
],
},
{
id: 'ip.tcpsock',
name: 'ip.tcpsock',
context: 'ip.tcpsock',
title: 'TCP Connections Current',
units: 'connections',
family: 'tcp',
chartType: 'line',
priority: 725,
plugin: 'proc',
dimensions: [{ id: 'connections', name: 'connections', algorithm: 'absolute' }],
},
{
id: 'ipv4.packets',
name: 'ipv4.packets',
context: 'ipv4.packets',
title: 'IPv4 Packets',
units: 'packets/s',
family: 'packets',
chartType: 'line',
priority: 740,
plugin: 'proc',
dimensions: [
{ id: 'received', name: 'received', algorithm: 'incremental' },
{ id: 'sent', name: 'sent', algorithm: 'incremental' },
{ id: 'forwarded', name: 'forwarded', algorithm: 'incremental' },
{ id: 'delivered', name: 'delivered', algorithm: 'incremental' },
],
},
{
id: 'ipv4.errors',
name: 'ipv4.errors',
context: 'ipv4.errors',
title: 'IPv4 Errors',
units: 'packets/s',
family: 'errors',
chartType: 'line',
priority: 750,
plugin: 'proc',
dimensions: [
{ id: 'InDiscards', name: 'InDiscards', algorithm: 'incremental' },
{ id: 'OutDiscards', name: 'OutDiscards', algorithm: 'incremental' },
{ id: 'InHdrErrors', name: 'InHdrErrors', algorithm: 'incremental' },
{ id: 'OutNoRoutes', name: 'OutNoRoutes', algorithm: 'incremental' },
],
},
{
id: 'ipv4.udppackets',
name: 'ipv4.udppackets',
context: 'ipv4.udppackets',
title: 'IPv4 UDP Packets',
units: 'packets/s',
family: 'udp',
chartType: 'line',
priority: 760,
plugin: 'proc',
dimensions: [
{ id: 'received', name: 'received', algorithm: 'incremental' },
{ id: 'sent', name: 'sent', algorithm: 'incremental' },
],
},
{
id: 'ipv4.udperrors',
name: 'ipv4.udperrors',
context: 'ipv4.udperrors',
title: 'IPv4 UDP Errors',
units: 'packets/s',
family: 'udp',
chartType: 'line',
priority: 770,
plugin: 'proc',
dimensions: [
{ id: 'RcvbufErrors', name: 'RcvbufErrors', algorithm: 'incremental' },
{ id: 'SndbufErrors', name: 'SndbufErrors', algorithm: 'incremental' },
{ id: 'InErrors', name: 'InErrors', algorithm: 'incremental' },
{ id: 'NoPorts', name: 'NoPorts', algorithm: 'incremental' },
],
},
// ── Pressure stall (PSI) ──────────────────────────────────
{
id: 'system.cpu_some_pressure',
name: 'system.cpu_some_pressure',
context: 'system.cpu_some_pressure',
title: 'CPU Some Pressure',
units: 'percentage',
family: 'pressure',
chartType: 'line',
priority: 800,
plugin: 'proc',
dimensions: [
{ id: 'some10', name: 'some10', algorithm: 'absolute' },
{ id: 'some60', name: 'some60', algorithm: 'absolute' },
{ id: 'some300', name: 'some300', algorithm: 'absolute' },
],
},
{
id: 'system.memory_some_pressure',
name: 'system.memory_some_pressure',
context: 'system.memory_some_pressure',
title: 'Memory Some Pressure',
units: 'percentage',
family: 'pressure',
chartType: 'line',
priority: 810,
plugin: 'proc',
dimensions: [
{ id: 'some10', name: 'some10', algorithm: 'absolute' },
{ id: 'some60', name: 'some60', algorithm: 'absolute' },
{ id: 'some300', name: 'some300', algorithm: 'absolute' },
],
},
{
id: 'system.io_some_pressure',
name: 'system.io_some_pressure',
context: 'system.io_some_pressure',
title: 'I/O Some Pressure',
units: 'percentage',
family: 'pressure',
chartType: 'line',
priority: 820,
plugin: 'proc',
dimensions: [
{ id: 'some10', name: 'some10', algorithm: 'absolute' },
{ id: 'some60', name: 'some60', algorithm: 'absolute' },
{ id: 'some300', name: 'some300', algorithm: 'absolute' },
],
},
]
/** @type {Map<string, ChartDef>} */
export const CHART_BY_ID = new Map(CHART_DEFS.map((c) => [c.id, c]))
const runtimeCharts = new Map()
/** Unique context ids */
export const CONTEXT_IDS = [...new Set(CHART_DEFS.map((c) => c.context))]
/**
* Register or refresh a dynamic instance chart (per-cpu / disk / iface / mount).
* @param {ChartDef} def
*/
export function registerChart(def) {
runtimeCharts.set(def.id, def)
}
/**
* @returns {ChartDef[]}
*/
export function getAllChartDefs() {
return [...STATIC_CHART_DEFS, ...runtimeCharts.values()]
}
/** Alias — prefer getAllChartDefs() when instance charts matter. */
export const CHART_DEFS = STATIC_CHART_DEFS
export const CHART_BY_ID = {
get(id) {
const sid = String(id)
return STATIC_CHART_DEFS.find((c) => c.id === sid) || runtimeCharts.get(sid) || null
},
has(id) {
return this.get(id) != null
},
keys() {
return getAllChartDefs().map((c) => c.id)
},
values() {
return getAllChartDefs()
},
get size() {
return getAllChartDefs().length
},
}
export function getContextIds() {
return [...new Set(getAllChartDefs().map((c) => c.context))]
}
/** @deprecated use getContextIds() */
export const CONTEXT_IDS = {
map(fn) {
return getContextIds().map(fn)
},
includes(id) {
return getContextIds().includes(id)
},
[Symbol.iterator]() {
return getContextIds()[Symbol.iterator]()
},
get length() {
return getContextIds().length
},
}
/**
* @param {string} context
*/
export function chartsForContext(context) {
return CHART_DEFS.filter((c) => c.context === context)
return getAllChartDefs().filter((c) => c.context === context)
}
/**
* Build a Netdata-ish chart summary object.
* Instance chart helpers
*/
export function makeCpuCoreChart(coreId) {
return {
id: `cpu.cpu${coreId}`,
name: `cpu.cpu${coreId}`,
context: 'cpu.cpu',
title: `Core ${coreId} utilization`,
units: 'percentage',
family: `cpu${coreId}`,
chartType: 'stacked',
priority: 1000 + Number(coreId),
plugin: 'proc',
dimensions: STATIC_CHART_DEFS.find((c) => c.id === 'system.cpu').dimensions.slice(),
}
}
export function makeDiskIoChart(disk) {
return {
id: `disk_io.${disk}`,
name: `disk_io.${disk}`,
context: 'disk.io',
title: `Disk ${disk} I/O`,
units: 'KiB/s',
family: disk,
chartType: 'area',
priority: 2000,
plugin: 'proc',
dimensions: [
{ id: 'reads', name: 'reads', algorithm: 'incremental' },
{ id: 'writes', name: 'writes', algorithm: 'incremental' },
],
}
}
export function makeDiskOpsChart(disk) {
return {
id: `disk_ops.${disk}`,
name: `disk_ops.${disk}`,
context: 'disk.ops',
title: `Disk ${disk} Completed I/O Operations`,
units: 'operations/s',
family: disk,
chartType: 'line',
priority: 2010,
plugin: 'proc',
dimensions: [
{ id: 'reads', name: 'reads', algorithm: 'incremental' },
{ id: 'writes', name: 'writes', algorithm: 'incremental' },
],
}
}
export function makeDiskUtilChart(disk) {
return {
id: `disk_util.${disk}`,
name: `disk_util.${disk}`,
context: 'disk.util',
title: `Disk ${disk} Utilization Time`,
units: '% of time working',
family: disk,
chartType: 'area',
priority: 2020,
plugin: 'proc',
dimensions: [{ id: 'utilization', name: 'utilization', algorithm: 'absolute' }],
}
}
export function makeNetChart(iface) {
return {
id: `net.${iface}`,
name: `net.${iface}`,
context: 'net.net',
title: `Bandwidth ${iface}`,
units: 'kilobits/s',
family: iface,
chartType: 'area',
priority: 3000,
plugin: 'proc',
dimensions: [
{ id: 'received', name: 'received', algorithm: 'incremental' },
{ id: 'sent', name: 'sent', algorithm: 'incremental' },
],
}
}
export function makeNetPacketsChart(iface) {
return {
id: `net_packets.${iface}`,
name: `net_packets.${iface}`,
context: 'net.packets',
title: `Packets ${iface}`,
units: 'packets/s',
family: iface,
chartType: 'line',
priority: 3010,
plugin: 'proc',
dimensions: [
{ id: 'received', name: 'received', algorithm: 'incremental' },
{ id: 'sent', name: 'sent', algorithm: 'incremental' },
{ id: 'multicast', name: 'multicast', algorithm: 'incremental' },
],
}
}
export function makeNetErrorsChart(iface) {
return {
id: `net_errors.${iface}`,
name: `net_errors.${iface}`,
context: 'net.errors',
title: `Interface ${iface} Errors`,
units: 'errors/s',
family: iface,
chartType: 'line',
priority: 3020,
plugin: 'proc',
dimensions: [
{ id: 'inbound', name: 'inbound', algorithm: 'incremental' },
{ id: 'outbound', name: 'outbound', algorithm: 'incremental' },
],
}
}
export function makeNetDropsChart(iface) {
return {
id: `net_drops.${iface}`,
name: `net_drops.${iface}`,
context: 'net.drops',
title: `Interface ${iface} Drops`,
units: 'drops/s',
family: iface,
chartType: 'line',
priority: 3030,
plugin: 'proc',
dimensions: [
{ id: 'inbound', name: 'inbound', algorithm: 'incremental' },
{ id: 'outbound', name: 'outbound', algorithm: 'incremental' },
],
}
}
export function makeDiskSpaceChart(mountId, mountPath) {
return {
id: `disk_space.${mountId}`,
name: `disk_space.${mountId}`,
context: 'disk.space',
title: `Disk Space Usage ${mountPath}`,
units: 'GiB',
family: mountPath,
chartType: 'stacked',
priority: 4000,
plugin: 'diskspace',
dimensions: [
{ id: 'avail', name: 'avail', algorithm: 'absolute' },
{ id: 'used', name: 'used', algorithm: 'absolute' },
{ id: 'reserved_for_root', name: 'reserved_for_root', algorithm: 'absolute' },
],
}
}
export function makeDiskInodesChart(mountId, mountPath) {
return {
id: `disk_inodes.${mountId}`,
name: `disk_inodes.${mountId}`,
context: 'disk.inodes',
title: `Disk Files (inodes) Usage ${mountPath}`,
units: 'inodes',
family: mountPath,
chartType: 'stacked',
priority: 4010,
plugin: 'diskspace',
dimensions: [
{ id: 'avail', name: 'avail', algorithm: 'absolute' },
{ id: 'used', name: 'used', algorithm: 'absolute' },
{ id: 'reserved_for_root', name: 'reserved_for_root', algorithm: 'absolute' },
],
}
}
/**
* Build a chart summary object for discovery APIs.
* @param {ChartDef} def
* @param {{ firstEntry?: number, lastEntry?: number, updateEvery?: number }} [meta]
*/
@@ -172,13 +820,14 @@ export function chartSummary(def, meta = {}) {
return {
id: def.id,
name: def.name,
type: 'system',
type: def.plugin || 'system',
family: def.family,
context: def.context,
title: def.title,
units: def.units,
chart_type: def.chartType,
priority: def.priority,
plugin: def.plugin || 'proc',
update_every: meta.updateEvery ?? SAMPLE_INTERVAL_MS / 1000,
first_entry: meta.firstEntry ?? 0,
last_entry: meta.lastEntry ?? 0,
+6
View File
@@ -82,6 +82,12 @@ export const MethodRoles = Object.freeze({
runJob: Roles.operator,
cancelJob: Roles.operator,
// HyperDB peer links / db info
getDbInfo: Roles.viewer,
listPeerLinks: Roles.viewer,
linkPeer: Roles.admin,
unlinkPeer: Roles.admin,
// admin / fleet
mintInvite: Roles.admin,
listPeers: Roles.admin,
+22
View File
@@ -136,8 +136,30 @@ export function validateMethodArgs(method, args = {}) {
}
case 'exportSnapshot':
case 'getDbInfo':
case 'listPeerLinks':
return { ok: true, args }
case 'linkPeer': {
const remotePublicKey = String(args.remotePublicKey || args.peerId || '').toLowerCase()
if (!/^[0-9a-f]{64}$/.test(remotePublicKey)) {
return { ok: false, error: 'remotePublicKey must be 64 hex characters' }
}
const syncMode = String(args.syncMode || 'both')
if (!['push', 'pull', 'both'].includes(syncMode)) {
return { ok: false, error: 'syncMode must be push|pull|both' }
}
return { ok: true, args: { ...args, remotePublicKey, syncMode } }
}
case 'unlinkPeer': {
const remotePublicKey = String(args.remotePublicKey || args.peerId || '').toLowerCase()
if (!/^[0-9a-f]{64}$/.test(remotePublicKey)) {
return { ok: false, error: 'remotePublicKey must be 64 hex characters' }
}
return { ok: true, args: { ...args, remotePublicKey } }
}
default:
return { ok: true, args }
}
+177
View File
@@ -0,0 +1,177 @@
{
"version": 1,
"offset": 0,
"schema": [
{
"name": "node",
"namespace": "peardata",
"id": 0,
"type": 1,
"version": 1,
"versionField": null,
"indexes": [
"@peardata/node-by-hostname"
],
"schema": "@peardata/node",
"derived": false,
"key": [
"nodeId"
],
"trigger": null
},
{
"name": "peer-link",
"namespace": "peardata",
"id": 1,
"type": 1,
"version": 1,
"versionField": null,
"indexes": [
"@peardata/peer-link-by-remote"
],
"schema": "@peardata/peer-link",
"derived": false,
"key": [
"localNodeId",
"remotePublicKey"
],
"trigger": null
},
{
"name": "alert-config",
"namespace": "peardata",
"id": 2,
"type": 1,
"version": 1,
"versionField": null,
"indexes": [],
"schema": "@peardata/alert-config",
"derived": false,
"key": [
"id"
],
"trigger": null
},
{
"name": "alert-event",
"namespace": "peardata",
"id": 3,
"type": 1,
"version": 1,
"versionField": null,
"indexes": [
"@peardata/alert-event-by-chart"
],
"schema": "@peardata/alert-event",
"derived": false,
"key": [
"id",
"ts"
],
"trigger": null
},
{
"name": "metric-point",
"namespace": "peardata",
"id": 4,
"type": 1,
"version": 1,
"versionField": null,
"indexes": [
"@peardata/metric-point-by-context",
"@peardata/metric-point-by-tier"
],
"schema": "@peardata/metric-point",
"derived": false,
"key": [
"chart",
"ts"
],
"trigger": null
},
{
"name": "job",
"namespace": "peardata",
"id": 5,
"type": 1,
"version": 1,
"versionField": null,
"indexes": [],
"schema": "@peardata/job",
"derived": false,
"key": [
"id"
],
"trigger": null
},
{
"name": "node-by-hostname",
"namespace": "peardata",
"id": 6,
"type": 2,
"version": 1,
"collection": "@peardata/node",
"unique": false,
"deprecated": false,
"key": [
"hostname"
]
},
{
"name": "peer-link-by-remote",
"namespace": "peardata",
"id": 7,
"type": 2,
"version": 1,
"collection": "@peardata/peer-link",
"unique": false,
"deprecated": false,
"key": [
"remotePublicKey"
]
},
{
"name": "alert-event-by-chart",
"namespace": "peardata",
"id": 8,
"type": 2,
"version": 1,
"collection": "@peardata/alert-event",
"unique": false,
"deprecated": false,
"key": [
"chart",
"ts"
]
},
{
"name": "metric-point-by-context",
"namespace": "peardata",
"id": 9,
"type": 2,
"version": 1,
"collection": "@peardata/metric-point",
"unique": false,
"deprecated": false,
"key": [
"context",
"ts"
]
},
{
"name": "metric-point-by-tier",
"namespace": "peardata",
"id": 10,
"type": 2,
"version": 1,
"collection": "@peardata/metric-point",
"unique": false,
"deprecated": false,
"key": [
"tier",
"chart",
"ts"
]
}
]
}
+757
View File
@@ -0,0 +1,757 @@
// This file is autogenerated by the hyperdb compiler
/* eslint-disable camelcase */
import { IndexEncoder, c, b4a } from 'hyperdb/runtime'
import { version, getEncoding, setVersion } from './messages.js'
const versions = { schema: version, db: 1 }
// '@peardata/node' collection key
const collection0_key = new IndexEncoder([
IndexEncoder.STRING
], { prefix: 0 })
function collection0_indexify (record) {
const a = record.nodeId
return a === undefined ? [] : [a]
}
// '@peardata/node' value encoding
const collection0_enc = getEncoding('@peardata/node/hyperdb#0')
// '@peardata/node' reconstruction function
function collection0_reconstruct (schemaVersion, keyBuf, valueBuf) {
const key = collection0_key.decode(keyBuf)
setVersion(schemaVersion)
const state = { start: 0, end: valueBuf.byteLength, buffer: valueBuf }
const type = c.uint.decode(state)
if (type !== 0) throw new Error('Unknown collection type: ' + type)
collection0.decodedVersion = c.uint.decode(state)
const record = collection0_enc.decode(state)
record.nodeId = key[0]
return record
}
// '@peardata/node' key reconstruction function
function collection0_reconstruct_key (keyBuf) {
const key = collection0_key.decode(keyBuf)
return {
nodeId: key[0]
}
}
// '@peardata/node'
const collection0 = {
name: '@peardata/node',
id: 0,
version: 1,
encodeKey (record) {
const key = [record.nodeId]
return collection0_key.encode(key)
},
encodeKeyRange ({ gt, lt, gte, lte } = {}) {
return collection0_key.encodeRange({
gt: gt ? collection0_indexify(gt) : null,
lt: lt ? collection0_indexify(lt) : null,
gte: gte ? collection0_indexify(gte) : null,
lte: lte ? collection0_indexify(lte) : null
})
},
encodeValue (schemaVersion, collectionVersion, record) {
setVersion(schemaVersion)
const state = { start: 0, end: 2, buffer: null }
collection0_enc.preencode(state, record)
state.buffer = b4a.allocUnsafe(state.end)
state.buffer[state.start++] = 0
state.buffer[state.start++] = collectionVersion
collection0_enc.encode(state, record)
return state.buffer
},
trigger: null,
reconstruct: collection0_reconstruct,
reconstructKey: collection0_reconstruct_key,
indexes: [],
decodedVersion: 0
}
// '@peardata/peer-link' collection key
const collection1_key = new IndexEncoder([
IndexEncoder.STRING,
IndexEncoder.STRING
], { prefix: 1 })
function collection1_indexify (record) {
const arr = []
const a0 = record.localNodeId
if (a0 === undefined) return arr
arr.push(a0)
const a1 = record.remotePublicKey
if (a1 === undefined) return arr
arr.push(a1)
return arr
}
// '@peardata/peer-link' value encoding
const collection1_enc = getEncoding('@peardata/peer-link/hyperdb#1')
// '@peardata/peer-link' reconstruction function
function collection1_reconstruct (schemaVersion, keyBuf, valueBuf) {
const key = collection1_key.decode(keyBuf)
setVersion(schemaVersion)
const state = { start: 0, end: valueBuf.byteLength, buffer: valueBuf }
const type = c.uint.decode(state)
if (type !== 0) throw new Error('Unknown collection type: ' + type)
collection1.decodedVersion = c.uint.decode(state)
const record = collection1_enc.decode(state)
record.localNodeId = key[0]
record.remotePublicKey = key[1]
return record
}
// '@peardata/peer-link' key reconstruction function
function collection1_reconstruct_key (keyBuf) {
const key = collection1_key.decode(keyBuf)
return {
localNodeId: key[0],
remotePublicKey: key[1]
}
}
// '@peardata/peer-link'
const collection1 = {
name: '@peardata/peer-link',
id: 1,
version: 1,
encodeKey (record) {
const key = [record.localNodeId, record.remotePublicKey]
return collection1_key.encode(key)
},
encodeKeyRange ({ gt, lt, gte, lte } = {}) {
return collection1_key.encodeRange({
gt: gt ? collection1_indexify(gt) : null,
lt: lt ? collection1_indexify(lt) : null,
gte: gte ? collection1_indexify(gte) : null,
lte: lte ? collection1_indexify(lte) : null
})
},
encodeValue (schemaVersion, collectionVersion, record) {
setVersion(schemaVersion)
const state = { start: 0, end: 2, buffer: null }
collection1_enc.preencode(state, record)
state.buffer = b4a.allocUnsafe(state.end)
state.buffer[state.start++] = 0
state.buffer[state.start++] = collectionVersion
collection1_enc.encode(state, record)
return state.buffer
},
trigger: null,
reconstruct: collection1_reconstruct,
reconstructKey: collection1_reconstruct_key,
indexes: [],
decodedVersion: 0
}
// '@peardata/alert-config' collection key
const collection2_key = new IndexEncoder([
IndexEncoder.STRING
], { prefix: 2 })
function collection2_indexify (record) {
const a = record.id
return a === undefined ? [] : [a]
}
// '@peardata/alert-config' value encoding
const collection2_enc = getEncoding('@peardata/alert-config/hyperdb#2')
// '@peardata/alert-config' reconstruction function
function collection2_reconstruct (schemaVersion, keyBuf, valueBuf) {
const key = collection2_key.decode(keyBuf)
setVersion(schemaVersion)
const state = { start: 0, end: valueBuf.byteLength, buffer: valueBuf }
const type = c.uint.decode(state)
if (type !== 0) throw new Error('Unknown collection type: ' + type)
collection2.decodedVersion = c.uint.decode(state)
const record = collection2_enc.decode(state)
record.id = key[0]
return record
}
// '@peardata/alert-config' key reconstruction function
function collection2_reconstruct_key (keyBuf) {
const key = collection2_key.decode(keyBuf)
return {
id: key[0]
}
}
// '@peardata/alert-config'
const collection2 = {
name: '@peardata/alert-config',
id: 2,
version: 1,
encodeKey (record) {
const key = [record.id]
return collection2_key.encode(key)
},
encodeKeyRange ({ gt, lt, gte, lte } = {}) {
return collection2_key.encodeRange({
gt: gt ? collection2_indexify(gt) : null,
lt: lt ? collection2_indexify(lt) : null,
gte: gte ? collection2_indexify(gte) : null,
lte: lte ? collection2_indexify(lte) : null
})
},
encodeValue (schemaVersion, collectionVersion, record) {
setVersion(schemaVersion)
const state = { start: 0, end: 2, buffer: null }
collection2_enc.preencode(state, record)
state.buffer = b4a.allocUnsafe(state.end)
state.buffer[state.start++] = 0
state.buffer[state.start++] = collectionVersion
collection2_enc.encode(state, record)
return state.buffer
},
trigger: null,
reconstruct: collection2_reconstruct,
reconstructKey: collection2_reconstruct_key,
indexes: [],
decodedVersion: 0
}
// '@peardata/alert-event' collection key
const collection3_key = new IndexEncoder([
IndexEncoder.STRING,
IndexEncoder.UINT
], { prefix: 3 })
function collection3_indexify (record) {
const arr = []
const a0 = record.id
if (a0 === undefined) return arr
arr.push(a0)
const a1 = record.ts
if (a1 === undefined) return arr
arr.push(a1)
return arr
}
// '@peardata/alert-event' value encoding
const collection3_enc = getEncoding('@peardata/alert-event/hyperdb#3')
// '@peardata/alert-event' reconstruction function
function collection3_reconstruct (schemaVersion, keyBuf, valueBuf) {
const key = collection3_key.decode(keyBuf)
setVersion(schemaVersion)
const state = { start: 0, end: valueBuf.byteLength, buffer: valueBuf }
const type = c.uint.decode(state)
if (type !== 0) throw new Error('Unknown collection type: ' + type)
collection3.decodedVersion = c.uint.decode(state)
const record = collection3_enc.decode(state)
record.id = key[0]
record.ts = key[1]
return record
}
// '@peardata/alert-event' key reconstruction function
function collection3_reconstruct_key (keyBuf) {
const key = collection3_key.decode(keyBuf)
return {
id: key[0],
ts: key[1]
}
}
// '@peardata/alert-event'
const collection3 = {
name: '@peardata/alert-event',
id: 3,
version: 1,
encodeKey (record) {
const key = [record.id, record.ts]
return collection3_key.encode(key)
},
encodeKeyRange ({ gt, lt, gte, lte } = {}) {
return collection3_key.encodeRange({
gt: gt ? collection3_indexify(gt) : null,
lt: lt ? collection3_indexify(lt) : null,
gte: gte ? collection3_indexify(gte) : null,
lte: lte ? collection3_indexify(lte) : null
})
},
encodeValue (schemaVersion, collectionVersion, record) {
setVersion(schemaVersion)
const state = { start: 0, end: 2, buffer: null }
collection3_enc.preencode(state, record)
state.buffer = b4a.allocUnsafe(state.end)
state.buffer[state.start++] = 0
state.buffer[state.start++] = collectionVersion
collection3_enc.encode(state, record)
return state.buffer
},
trigger: null,
reconstruct: collection3_reconstruct,
reconstructKey: collection3_reconstruct_key,
indexes: [],
decodedVersion: 0
}
// '@peardata/metric-point' collection key
const collection4_key = new IndexEncoder([
IndexEncoder.STRING,
IndexEncoder.UINT
], { prefix: 4 })
function collection4_indexify (record) {
const arr = []
const a0 = record.chart
if (a0 === undefined) return arr
arr.push(a0)
const a1 = record.ts
if (a1 === undefined) return arr
arr.push(a1)
return arr
}
// '@peardata/metric-point' value encoding
const collection4_enc = getEncoding('@peardata/metric-point/hyperdb#4')
// '@peardata/metric-point' reconstruction function
function collection4_reconstruct (schemaVersion, keyBuf, valueBuf) {
const key = collection4_key.decode(keyBuf)
setVersion(schemaVersion)
const state = { start: 0, end: valueBuf.byteLength, buffer: valueBuf }
const type = c.uint.decode(state)
if (type !== 0) throw new Error('Unknown collection type: ' + type)
collection4.decodedVersion = c.uint.decode(state)
const record = collection4_enc.decode(state)
record.chart = key[0]
record.ts = key[1]
return record
}
// '@peardata/metric-point' key reconstruction function
function collection4_reconstruct_key (keyBuf) {
const key = collection4_key.decode(keyBuf)
return {
chart: key[0],
ts: key[1]
}
}
// '@peardata/metric-point'
const collection4 = {
name: '@peardata/metric-point',
id: 4,
version: 1,
encodeKey (record) {
const key = [record.chart, record.ts]
return collection4_key.encode(key)
},
encodeKeyRange ({ gt, lt, gte, lte } = {}) {
return collection4_key.encodeRange({
gt: gt ? collection4_indexify(gt) : null,
lt: lt ? collection4_indexify(lt) : null,
gte: gte ? collection4_indexify(gte) : null,
lte: lte ? collection4_indexify(lte) : null
})
},
encodeValue (schemaVersion, collectionVersion, record) {
setVersion(schemaVersion)
const state = { start: 0, end: 2, buffer: null }
collection4_enc.preencode(state, record)
state.buffer = b4a.allocUnsafe(state.end)
state.buffer[state.start++] = 0
state.buffer[state.start++] = collectionVersion
collection4_enc.encode(state, record)
return state.buffer
},
trigger: null,
reconstruct: collection4_reconstruct,
reconstructKey: collection4_reconstruct_key,
indexes: [],
decodedVersion: 0
}
// '@peardata/job' collection key
const collection5_key = new IndexEncoder([
IndexEncoder.STRING
], { prefix: 5 })
function collection5_indexify (record) {
const a = record.id
return a === undefined ? [] : [a]
}
// '@peardata/job' value encoding
const collection5_enc = getEncoding('@peardata/job/hyperdb#5')
// '@peardata/job' reconstruction function
function collection5_reconstruct (schemaVersion, keyBuf, valueBuf) {
const key = collection5_key.decode(keyBuf)
setVersion(schemaVersion)
const state = { start: 0, end: valueBuf.byteLength, buffer: valueBuf }
const type = c.uint.decode(state)
if (type !== 0) throw new Error('Unknown collection type: ' + type)
collection5.decodedVersion = c.uint.decode(state)
const record = collection5_enc.decode(state)
record.id = key[0]
return record
}
// '@peardata/job' key reconstruction function
function collection5_reconstruct_key (keyBuf) {
const key = collection5_key.decode(keyBuf)
return {
id: key[0]
}
}
// '@peardata/job'
const collection5 = {
name: '@peardata/job',
id: 5,
version: 1,
encodeKey (record) {
const key = [record.id]
return collection5_key.encode(key)
},
encodeKeyRange ({ gt, lt, gte, lte } = {}) {
return collection5_key.encodeRange({
gt: gt ? collection5_indexify(gt) : null,
lt: lt ? collection5_indexify(lt) : null,
gte: gte ? collection5_indexify(gte) : null,
lte: lte ? collection5_indexify(lte) : null
})
},
encodeValue (schemaVersion, collectionVersion, record) {
setVersion(schemaVersion)
const state = { start: 0, end: 2, buffer: null }
collection5_enc.preencode(state, record)
state.buffer = b4a.allocUnsafe(state.end)
state.buffer[state.start++] = 0
state.buffer[state.start++] = collectionVersion
collection5_enc.encode(state, record)
return state.buffer
},
trigger: null,
reconstruct: collection5_reconstruct,
reconstructKey: collection5_reconstruct_key,
indexes: [],
decodedVersion: 0
}
// '@peardata/node-by-hostname' collection key
const index6_key = new IndexEncoder([
IndexEncoder.STRING,
IndexEncoder.STRING
], { prefix: 6 })
function index6_indexify (record) {
const arr = []
const a0 = record.hostname
if (a0 === undefined) return arr
arr.push(a0)
const a1 = record.nodeId
if (a1 === undefined) return arr
arr.push(a1)
return arr
}
// '@peardata/node-by-hostname'
const index6 = {
name: '@peardata/node-by-hostname',
version: 1,
id: 6,
encodeKey (record) {
return index6_key.encode(index6_indexify(record))
},
encodeKeyRange ({ gt, lt, gte, lte } = {}) {
return index6_key.encodeRange({
gt: gt ? index6_indexify(gt) : null,
lt: lt ? index6_indexify(lt) : null,
gte: gte ? index6_indexify(gte) : null,
lte: lte ? index6_indexify(lte) : null
})
},
encodeValue: (record) => index6.collection.encodeKey(record),
encodeIndexKeys (record, context) {
return [index6_key.encode([record.hostname, record.nodeId])]
},
reconstruct: (keyBuf, valueBuf) => valueBuf,
offset: collection0.indexes.length,
collection: collection0
}
collection0.indexes.push(index6)
// '@peardata/peer-link-by-remote' collection key
const index7_key = new IndexEncoder([
IndexEncoder.STRING,
IndexEncoder.STRING,
IndexEncoder.STRING
], { prefix: 7 })
function index7_indexify (record) {
const arr = []
const a0 = record.remotePublicKey
if (a0 === undefined) return arr
arr.push(a0)
const a1 = record.localNodeId
if (a1 === undefined) return arr
arr.push(a1)
const a2 = record.remotePublicKey
if (a2 === undefined) return arr
arr.push(a2)
return arr
}
// '@peardata/peer-link-by-remote'
const index7 = {
name: '@peardata/peer-link-by-remote',
version: 1,
id: 7,
encodeKey (record) {
return index7_key.encode(index7_indexify(record))
},
encodeKeyRange ({ gt, lt, gte, lte } = {}) {
return index7_key.encodeRange({
gt: gt ? index7_indexify(gt) : null,
lt: lt ? index7_indexify(lt) : null,
gte: gte ? index7_indexify(gte) : null,
lte: lte ? index7_indexify(lte) : null
})
},
encodeValue: (record) => index7.collection.encodeKey(record),
encodeIndexKeys (record, context) {
return [index7_key.encode([record.remotePublicKey, record.localNodeId, record.remotePublicKey])]
},
reconstruct: (keyBuf, valueBuf) => valueBuf,
offset: collection1.indexes.length,
collection: collection1
}
collection1.indexes.push(index7)
// '@peardata/alert-event-by-chart' collection key
const index8_key = new IndexEncoder([
IndexEncoder.STRING,
IndexEncoder.UINT,
IndexEncoder.STRING,
IndexEncoder.UINT
], { prefix: 8 })
function index8_indexify (record) {
const arr = []
const a0 = record.chart
if (a0 === undefined) return arr
arr.push(a0)
const a1 = record.ts
if (a1 === undefined) return arr
arr.push(a1)
const a2 = record.id
if (a2 === undefined) return arr
arr.push(a2)
const a3 = record.ts
if (a3 === undefined) return arr
arr.push(a3)
return arr
}
// '@peardata/alert-event-by-chart'
const index8 = {
name: '@peardata/alert-event-by-chart',
version: 1,
id: 8,
encodeKey (record) {
return index8_key.encode(index8_indexify(record))
},
encodeKeyRange ({ gt, lt, gte, lte } = {}) {
return index8_key.encodeRange({
gt: gt ? index8_indexify(gt) : null,
lt: lt ? index8_indexify(lt) : null,
gte: gte ? index8_indexify(gte) : null,
lte: lte ? index8_indexify(lte) : null
})
},
encodeValue: (record) => index8.collection.encodeKey(record),
encodeIndexKeys (record, context) {
return [index8_key.encode([record.chart, record.ts, record.id, record.ts])]
},
reconstruct: (keyBuf, valueBuf) => valueBuf,
offset: collection3.indexes.length,
collection: collection3
}
collection3.indexes.push(index8)
// '@peardata/metric-point-by-context' collection key
const index9_key = new IndexEncoder([
IndexEncoder.STRING,
IndexEncoder.UINT,
IndexEncoder.STRING,
IndexEncoder.UINT
], { prefix: 9 })
function index9_indexify (record) {
const arr = []
const a0 = record.context
if (a0 === undefined) return arr
arr.push(a0)
const a1 = record.ts
if (a1 === undefined) return arr
arr.push(a1)
const a2 = record.chart
if (a2 === undefined) return arr
arr.push(a2)
const a3 = record.ts
if (a3 === undefined) return arr
arr.push(a3)
return arr
}
// '@peardata/metric-point-by-context'
const index9 = {
name: '@peardata/metric-point-by-context',
version: 1,
id: 9,
encodeKey (record) {
return index9_key.encode(index9_indexify(record))
},
encodeKeyRange ({ gt, lt, gte, lte } = {}) {
return index9_key.encodeRange({
gt: gt ? index9_indexify(gt) : null,
lt: lt ? index9_indexify(lt) : null,
gte: gte ? index9_indexify(gte) : null,
lte: lte ? index9_indexify(lte) : null
})
},
encodeValue: (record) => index9.collection.encodeKey(record),
encodeIndexKeys (record, context) {
return [index9_key.encode([record.context, record.ts, record.chart, record.ts])]
},
reconstruct: (keyBuf, valueBuf) => valueBuf,
offset: collection4.indexes.length,
collection: collection4
}
collection4.indexes.push(index9)
// '@peardata/metric-point-by-tier' collection key
const index10_key = new IndexEncoder([
IndexEncoder.UINT,
IndexEncoder.STRING,
IndexEncoder.UINT,
IndexEncoder.STRING,
IndexEncoder.UINT
], { prefix: 10 })
function index10_indexify (record) {
const arr = []
const a0 = record.tier
if (a0 === undefined) return arr
arr.push(a0)
const a1 = record.chart
if (a1 === undefined) return arr
arr.push(a1)
const a2 = record.ts
if (a2 === undefined) return arr
arr.push(a2)
const a3 = record.chart
if (a3 === undefined) return arr
arr.push(a3)
const a4 = record.ts
if (a4 === undefined) return arr
arr.push(a4)
return arr
}
// '@peardata/metric-point-by-tier'
const index10 = {
name: '@peardata/metric-point-by-tier',
version: 1,
id: 10,
encodeKey (record) {
return index10_key.encode(index10_indexify(record))
},
encodeKeyRange ({ gt, lt, gte, lte } = {}) {
return index10_key.encodeRange({
gt: gt ? index10_indexify(gt) : null,
lt: lt ? index10_indexify(lt) : null,
gte: gte ? index10_indexify(gte) : null,
lte: lte ? index10_indexify(lte) : null
})
},
encodeValue: (record) => index10.collection.encodeKey(record),
encodeIndexKeys (record, context) {
return [index10_key.encode([record.tier, record.chart, record.ts, record.chart, record.ts])]
},
reconstruct: (keyBuf, valueBuf) => valueBuf,
offset: collection4.indexes.length,
collection: collection4
}
collection4.indexes.push(index10)
const collections = [
collection0,
collection1,
collection2,
collection3,
collection4,
collection5
]
const indexes = [
index6,
index7,
index8,
index9,
index10
]
export default { versions, collections, indexes, resolveCollection, resolveIndex }
function resolveCollection (name) {
switch (name) {
case '@peardata/node': return collection0
case '@peardata/peer-link': return collection1
case '@peardata/alert-config': return collection2
case '@peardata/alert-event': return collection3
case '@peardata/metric-point': return collection4
case '@peardata/job': return collection5
default: return null
}
}
function resolveIndex (name) {
switch (name) {
case '@peardata/node-by-hostname': return index6
case '@peardata/peer-link-by-remote': return index7
case '@peardata/alert-event-by-chart': return index8
case '@peardata/metric-point-by-context': return index9
case '@peardata/metric-point-by-tier': return index10
default: return null
}
}
+647
View File
@@ -0,0 +1,647 @@
// This file is autogenerated by the hyperschema compiler
// Schema Version: 1
/* eslint-disable camelcase */
/* eslint-disable quotes */
/* eslint-disable space-before-function-paren */
import { c } from 'hyperschema/runtime'
const VERSION = 1
// eslint-disable-next-line no-unused-vars
let version = VERSION
// @peardata/node
const encoding0 = {
preencode(state, m) {
c.string.preencode(state, m.nodeId)
c.string.preencode(state, m.hostname)
state.end++ // max flag is 64 so always one byte
if (m.publicKeyHex) c.string.preencode(state, m.publicKeyHex)
if (m.platform) c.string.preencode(state, m.platform)
if (m.arch) c.string.preencode(state, m.arch)
if (m.cpus) c.uint.preencode(state, m.cpus)
if (m.totalMemMiB) c.uint.preencode(state, m.totalMemMiB)
if (m.agentVersion) c.string.preencode(state, m.agentVersion)
if (m.labelsJson) c.string.preencode(state, m.labelsJson)
c.uint.preencode(state, m.updatedAt)
},
encode(state, m) {
const flags =
(m.publicKeyHex ? 1 : 0) |
(m.platform ? 2 : 0) |
(m.arch ? 4 : 0) |
(m.cpus ? 8 : 0) |
(m.totalMemMiB ? 16 : 0) |
(m.agentVersion ? 32 : 0) |
(m.labelsJson ? 64 : 0)
c.string.encode(state, m.nodeId)
c.string.encode(state, m.hostname)
c.uint.encode(state, flags)
if (m.publicKeyHex) c.string.encode(state, m.publicKeyHex)
if (m.platform) c.string.encode(state, m.platform)
if (m.arch) c.string.encode(state, m.arch)
if (m.cpus) c.uint.encode(state, m.cpus)
if (m.totalMemMiB) c.uint.encode(state, m.totalMemMiB)
if (m.agentVersion) c.string.encode(state, m.agentVersion)
if (m.labelsJson) c.string.encode(state, m.labelsJson)
c.uint.encode(state, m.updatedAt)
},
decode(state) {
const r0 = c.string.decode(state)
const r1 = c.string.decode(state)
const flags = c.uint.decode(state)
return {
nodeId: r0,
hostname: r1,
publicKeyHex: (flags & 1) !== 0 ? c.string.decode(state) : null,
platform: (flags & 2) !== 0 ? c.string.decode(state) : null,
arch: (flags & 4) !== 0 ? c.string.decode(state) : null,
cpus: (flags & 8) !== 0 ? c.uint.decode(state) : 0,
totalMemMiB: (flags & 16) !== 0 ? c.uint.decode(state) : 0,
agentVersion: (flags & 32) !== 0 ? c.string.decode(state) : null,
labelsJson: (flags & 64) !== 0 ? c.string.decode(state) : null,
updatedAt: c.uint.decode(state)
}
}
}
// @peardata/peer-link
const encoding1 = {
preencode(state, m) {
c.string.preencode(state, m.localNodeId)
c.string.preencode(state, m.remotePublicKey)
state.end++ // max flag is 16 so always one byte
if (m.role) c.string.preencode(state, m.role)
if (m.alias) c.string.preencode(state, m.alias)
if (m.dbKeyHex) c.string.preencode(state, m.dbKeyHex)
if (m.syncMode) c.string.preencode(state, m.syncMode)
c.uint.preencode(state, m.linkedAt)
if (m.lastSeen) c.uint.preencode(state, m.lastSeen)
},
encode(state, m) {
const flags =
(m.role ? 1 : 0) |
(m.alias ? 2 : 0) |
(m.dbKeyHex ? 4 : 0) |
(m.syncMode ? 8 : 0) |
(m.lastSeen ? 16 : 0)
c.string.encode(state, m.localNodeId)
c.string.encode(state, m.remotePublicKey)
c.uint.encode(state, flags)
if (m.role) c.string.encode(state, m.role)
if (m.alias) c.string.encode(state, m.alias)
if (m.dbKeyHex) c.string.encode(state, m.dbKeyHex)
if (m.syncMode) c.string.encode(state, m.syncMode)
c.uint.encode(state, m.linkedAt)
if (m.lastSeen) c.uint.encode(state, m.lastSeen)
},
decode(state) {
const r0 = c.string.decode(state)
const r1 = c.string.decode(state)
const flags = c.uint.decode(state)
return {
localNodeId: r0,
remotePublicKey: r1,
role: (flags & 1) !== 0 ? c.string.decode(state) : null,
alias: (flags & 2) !== 0 ? c.string.decode(state) : null,
dbKeyHex: (flags & 4) !== 0 ? c.string.decode(state) : null,
syncMode: (flags & 8) !== 0 ? c.string.decode(state) : null,
linkedAt: c.uint.decode(state),
lastSeen: (flags & 16) !== 0 ? c.uint.decode(state) : 0
}
}
}
// @peardata/alert-config
const encoding2 = {
preencode(state, m) {
c.string.preencode(state, m.id)
c.string.preencode(state, m.chart)
c.string.preencode(state, m.dimension)
state.end++ // max flag is 16 so always one byte
if (m.warn) c.float64.preencode(state, m.warn)
if (m.crit) c.float64.preencode(state, m.crit)
if (m.comparator) c.string.preencode(state, m.comparator)
if (m.info) c.string.preencode(state, m.info)
c.uint.preencode(state, m.updatedAt)
},
encode(state, m) {
const flags =
(m.warn ? 1 : 0) |
(m.crit ? 2 : 0) |
(m.comparator ? 4 : 0) |
(m.enabled ? 8 : 0) |
(m.info ? 16 : 0)
c.string.encode(state, m.id)
c.string.encode(state, m.chart)
c.string.encode(state, m.dimension)
c.uint.encode(state, flags)
if (m.warn) c.float64.encode(state, m.warn)
if (m.crit) c.float64.encode(state, m.crit)
if (m.comparator) c.string.encode(state, m.comparator)
if (m.info) c.string.encode(state, m.info)
c.uint.encode(state, m.updatedAt)
},
decode(state) {
const r0 = c.string.decode(state)
const r1 = c.string.decode(state)
const r2 = c.string.decode(state)
const flags = c.uint.decode(state)
return {
id: r0,
chart: r1,
dimension: r2,
warn: (flags & 1) !== 0 ? c.float64.decode(state) : 0,
crit: (flags & 2) !== 0 ? c.float64.decode(state) : 0,
comparator: (flags & 4) !== 0 ? c.string.decode(state) : null,
enabled: (flags & 8) !== 0,
info: (flags & 16) !== 0 ? c.string.decode(state) : null,
updatedAt: c.uint.decode(state)
}
}
}
// @peardata/alert-event
const encoding3 = {
preencode(state, m) {
c.string.preencode(state, m.id)
c.uint.preencode(state, m.ts)
c.string.preencode(state, m.chart)
state.end++ // max flag is 16 so always one byte
if (m.dimension) c.string.preencode(state, m.dimension)
c.string.preencode(state, m.severity)
if (m.value) c.float64.preencode(state, m.value)
if (m.threshold) c.float64.preencode(state, m.threshold)
if (m.message) c.string.preencode(state, m.message)
},
encode(state, m) {
const flags =
(m.dimension ? 1 : 0) |
(m.value ? 2 : 0) |
(m.threshold ? 4 : 0) |
(m.message ? 8 : 0) |
(m.cleared ? 16 : 0)
c.string.encode(state, m.id)
c.uint.encode(state, m.ts)
c.string.encode(state, m.chart)
c.uint.encode(state, flags)
if (m.dimension) c.string.encode(state, m.dimension)
c.string.encode(state, m.severity)
if (m.value) c.float64.encode(state, m.value)
if (m.threshold) c.float64.encode(state, m.threshold)
if (m.message) c.string.encode(state, m.message)
},
decode(state) {
const r0 = c.string.decode(state)
const r1 = c.uint.decode(state)
const r2 = c.string.decode(state)
const flags = c.uint.decode(state)
return {
id: r0,
ts: r1,
chart: r2,
dimension: (flags & 1) !== 0 ? c.string.decode(state) : null,
severity: c.string.decode(state),
value: (flags & 2) !== 0 ? c.float64.decode(state) : 0,
threshold: (flags & 4) !== 0 ? c.float64.decode(state) : 0,
message: (flags & 8) !== 0 ? c.string.decode(state) : null,
cleared: (flags & 16) !== 0
}
}
}
// @peardata/metric-point
const encoding4 = {
preencode(state, m) {
c.string.preencode(state, m.chart)
c.uint.preencode(state, m.ts)
c.string.preencode(state, m.context)
c.uint.preencode(state, m.tier)
c.string.preencode(state, m.valuesJson)
},
encode(state, m) {
c.string.encode(state, m.chart)
c.uint.encode(state, m.ts)
c.string.encode(state, m.context)
c.uint.encode(state, m.tier)
c.string.encode(state, m.valuesJson)
},
decode(state) {
const r0 = c.string.decode(state)
const r1 = c.uint.decode(state)
const r2 = c.string.decode(state)
const r3 = c.uint.decode(state)
const r4 = c.string.decode(state)
return {
chart: r0,
ts: r1,
context: r2,
tier: r3,
valuesJson: r4
}
}
}
// @peardata/job
const encoding5 = {
preencode(state, m) {
c.string.preencode(state, m.id)
c.string.preencode(state, m.name)
c.string.preencode(state, m.status)
state.end++ // max flag is 8 so always one byte
if (m.startedAt) c.uint.preencode(state, m.startedAt)
if (m.finishedAt) c.uint.preencode(state, m.finishedAt)
if (m.resultJson) c.string.preencode(state, m.resultJson)
if (m.error) c.string.preencode(state, m.error)
},
encode(state, m) {
const flags =
(m.startedAt ? 1 : 0) | (m.finishedAt ? 2 : 0) | (m.resultJson ? 4 : 0) | (m.error ? 8 : 0)
c.string.encode(state, m.id)
c.string.encode(state, m.name)
c.string.encode(state, m.status)
c.uint.encode(state, flags)
if (m.startedAt) c.uint.encode(state, m.startedAt)
if (m.finishedAt) c.uint.encode(state, m.finishedAt)
if (m.resultJson) c.string.encode(state, m.resultJson)
if (m.error) c.string.encode(state, m.error)
},
decode(state) {
const r0 = c.string.decode(state)
const r1 = c.string.decode(state)
const r2 = c.string.decode(state)
const flags = c.uint.decode(state)
return {
id: r0,
name: r1,
status: r2,
startedAt: (flags & 1) !== 0 ? c.uint.decode(state) : 0,
finishedAt: (flags & 2) !== 0 ? c.uint.decode(state) : 0,
resultJson: (flags & 4) !== 0 ? c.string.decode(state) : null,
error: (flags & 8) !== 0 ? c.string.decode(state) : null
}
}
}
// @peardata/node/hyperdb#0
const encoding6 = {
preencode(state, m) {
c.string.preencode(state, m.hostname)
state.end++ // max flag is 64 so always one byte
if (m.publicKeyHex) c.string.preencode(state, m.publicKeyHex)
if (m.platform) c.string.preencode(state, m.platform)
if (m.arch) c.string.preencode(state, m.arch)
if (m.cpus) c.uint.preencode(state, m.cpus)
if (m.totalMemMiB) c.uint.preencode(state, m.totalMemMiB)
if (m.agentVersion) c.string.preencode(state, m.agentVersion)
if (m.labelsJson) c.string.preencode(state, m.labelsJson)
c.uint.preencode(state, m.updatedAt)
},
encode(state, m) {
const flags =
(m.publicKeyHex ? 1 : 0) |
(m.platform ? 2 : 0) |
(m.arch ? 4 : 0) |
(m.cpus ? 8 : 0) |
(m.totalMemMiB ? 16 : 0) |
(m.agentVersion ? 32 : 0) |
(m.labelsJson ? 64 : 0)
c.string.encode(state, m.hostname)
c.uint.encode(state, flags)
if (m.publicKeyHex) c.string.encode(state, m.publicKeyHex)
if (m.platform) c.string.encode(state, m.platform)
if (m.arch) c.string.encode(state, m.arch)
if (m.cpus) c.uint.encode(state, m.cpus)
if (m.totalMemMiB) c.uint.encode(state, m.totalMemMiB)
if (m.agentVersion) c.string.encode(state, m.agentVersion)
if (m.labelsJson) c.string.encode(state, m.labelsJson)
c.uint.encode(state, m.updatedAt)
},
decode(state) {
const r1 = c.string.decode(state)
const flags = c.uint.decode(state)
return {
nodeId: null,
hostname: r1,
publicKeyHex: (flags & 1) !== 0 ? c.string.decode(state) : null,
platform: (flags & 2) !== 0 ? c.string.decode(state) : null,
arch: (flags & 4) !== 0 ? c.string.decode(state) : null,
cpus: (flags & 8) !== 0 ? c.uint.decode(state) : 0,
totalMemMiB: (flags & 16) !== 0 ? c.uint.decode(state) : 0,
agentVersion: (flags & 32) !== 0 ? c.string.decode(state) : null,
labelsJson: (flags & 64) !== 0 ? c.string.decode(state) : null,
updatedAt: c.uint.decode(state)
}
}
}
// @peardata/peer-link/hyperdb#1
const encoding7 = {
preencode(state, m) {
state.end++ // max flag is 16 so always one byte
if (m.role) c.string.preencode(state, m.role)
if (m.alias) c.string.preencode(state, m.alias)
if (m.dbKeyHex) c.string.preencode(state, m.dbKeyHex)
if (m.syncMode) c.string.preencode(state, m.syncMode)
c.uint.preencode(state, m.linkedAt)
if (m.lastSeen) c.uint.preencode(state, m.lastSeen)
},
encode(state, m) {
const flags =
(m.role ? 1 : 0) |
(m.alias ? 2 : 0) |
(m.dbKeyHex ? 4 : 0) |
(m.syncMode ? 8 : 0) |
(m.lastSeen ? 16 : 0)
c.uint.encode(state, flags)
if (m.role) c.string.encode(state, m.role)
if (m.alias) c.string.encode(state, m.alias)
if (m.dbKeyHex) c.string.encode(state, m.dbKeyHex)
if (m.syncMode) c.string.encode(state, m.syncMode)
c.uint.encode(state, m.linkedAt)
if (m.lastSeen) c.uint.encode(state, m.lastSeen)
},
decode(state) {
const flags = c.uint.decode(state)
return {
localNodeId: null,
remotePublicKey: null,
role: (flags & 1) !== 0 ? c.string.decode(state) : null,
alias: (flags & 2) !== 0 ? c.string.decode(state) : null,
dbKeyHex: (flags & 4) !== 0 ? c.string.decode(state) : null,
syncMode: (flags & 8) !== 0 ? c.string.decode(state) : null,
linkedAt: c.uint.decode(state),
lastSeen: (flags & 16) !== 0 ? c.uint.decode(state) : 0
}
}
}
// @peardata/alert-config/hyperdb#2
const encoding8 = {
preencode(state, m) {
c.string.preencode(state, m.chart)
c.string.preencode(state, m.dimension)
state.end++ // max flag is 16 so always one byte
if (m.warn) c.float64.preencode(state, m.warn)
if (m.crit) c.float64.preencode(state, m.crit)
if (m.comparator) c.string.preencode(state, m.comparator)
if (m.info) c.string.preencode(state, m.info)
c.uint.preencode(state, m.updatedAt)
},
encode(state, m) {
const flags =
(m.warn ? 1 : 0) |
(m.crit ? 2 : 0) |
(m.comparator ? 4 : 0) |
(m.enabled ? 8 : 0) |
(m.info ? 16 : 0)
c.string.encode(state, m.chart)
c.string.encode(state, m.dimension)
c.uint.encode(state, flags)
if (m.warn) c.float64.encode(state, m.warn)
if (m.crit) c.float64.encode(state, m.crit)
if (m.comparator) c.string.encode(state, m.comparator)
if (m.info) c.string.encode(state, m.info)
c.uint.encode(state, m.updatedAt)
},
decode(state) {
const r1 = c.string.decode(state)
const r2 = c.string.decode(state)
const flags = c.uint.decode(state)
return {
id: null,
chart: r1,
dimension: r2,
warn: (flags & 1) !== 0 ? c.float64.decode(state) : 0,
crit: (flags & 2) !== 0 ? c.float64.decode(state) : 0,
comparator: (flags & 4) !== 0 ? c.string.decode(state) : null,
enabled: (flags & 8) !== 0,
info: (flags & 16) !== 0 ? c.string.decode(state) : null,
updatedAt: c.uint.decode(state)
}
}
}
// @peardata/alert-event/hyperdb#3
const encoding9 = {
preencode(state, m) {
c.string.preencode(state, m.chart)
state.end++ // max flag is 16 so always one byte
if (m.dimension) c.string.preencode(state, m.dimension)
c.string.preencode(state, m.severity)
if (m.value) c.float64.preencode(state, m.value)
if (m.threshold) c.float64.preencode(state, m.threshold)
if (m.message) c.string.preencode(state, m.message)
},
encode(state, m) {
const flags =
(m.dimension ? 1 : 0) |
(m.value ? 2 : 0) |
(m.threshold ? 4 : 0) |
(m.message ? 8 : 0) |
(m.cleared ? 16 : 0)
c.string.encode(state, m.chart)
c.uint.encode(state, flags)
if (m.dimension) c.string.encode(state, m.dimension)
c.string.encode(state, m.severity)
if (m.value) c.float64.encode(state, m.value)
if (m.threshold) c.float64.encode(state, m.threshold)
if (m.message) c.string.encode(state, m.message)
},
decode(state) {
const r2 = c.string.decode(state)
const flags = c.uint.decode(state)
return {
id: null,
ts: 0,
chart: r2,
dimension: (flags & 1) !== 0 ? c.string.decode(state) : null,
severity: c.string.decode(state),
value: (flags & 2) !== 0 ? c.float64.decode(state) : 0,
threshold: (flags & 4) !== 0 ? c.float64.decode(state) : 0,
message: (flags & 8) !== 0 ? c.string.decode(state) : null,
cleared: (flags & 16) !== 0
}
}
}
// @peardata/metric-point/hyperdb#4
const encoding10 = {
preencode(state, m) {
c.string.preencode(state, m.context)
c.uint.preencode(state, m.tier)
c.string.preencode(state, m.valuesJson)
},
encode(state, m) {
c.string.encode(state, m.context)
c.uint.encode(state, m.tier)
c.string.encode(state, m.valuesJson)
},
decode(state) {
const r2 = c.string.decode(state)
const r3 = c.uint.decode(state)
const r4 = c.string.decode(state)
return {
chart: null,
ts: 0,
context: r2,
tier: r3,
valuesJson: r4
}
}
}
// @peardata/job/hyperdb#5
const encoding11 = {
preencode(state, m) {
c.string.preencode(state, m.name)
c.string.preencode(state, m.status)
state.end++ // max flag is 8 so always one byte
if (m.startedAt) c.uint.preencode(state, m.startedAt)
if (m.finishedAt) c.uint.preencode(state, m.finishedAt)
if (m.resultJson) c.string.preencode(state, m.resultJson)
if (m.error) c.string.preencode(state, m.error)
},
encode(state, m) {
const flags =
(m.startedAt ? 1 : 0) | (m.finishedAt ? 2 : 0) | (m.resultJson ? 4 : 0) | (m.error ? 8 : 0)
c.string.encode(state, m.name)
c.string.encode(state, m.status)
c.uint.encode(state, flags)
if (m.startedAt) c.uint.encode(state, m.startedAt)
if (m.finishedAt) c.uint.encode(state, m.finishedAt)
if (m.resultJson) c.string.encode(state, m.resultJson)
if (m.error) c.string.encode(state, m.error)
},
decode(state) {
const r1 = c.string.decode(state)
const r2 = c.string.decode(state)
const flags = c.uint.decode(state)
return {
id: null,
name: r1,
status: r2,
startedAt: (flags & 1) !== 0 ? c.uint.decode(state) : 0,
finishedAt: (flags & 2) !== 0 ? c.uint.decode(state) : 0,
resultJson: (flags & 4) !== 0 ? c.string.decode(state) : null,
error: (flags & 8) !== 0 ? c.string.decode(state) : null
}
}
}
function setVersion(v) {
version = v
}
function encode(name, value, v = VERSION) {
version = v
return c.encode(getEncoding(name), value)
}
function decode(name, buffer, v = VERSION) {
version = v
return c.decode(getEncoding(name), buffer)
}
function getEnum(name) {
switch (name) {
default:
throw new Error('Enum not found ' + name)
}
}
function getEncoding(name) {
switch (name) {
case '@peardata/node':
return encoding0
case '@peardata/peer-link':
return encoding1
case '@peardata/alert-config':
return encoding2
case '@peardata/alert-event':
return encoding3
case '@peardata/metric-point':
return encoding4
case '@peardata/job':
return encoding5
case '@peardata/node/hyperdb#0':
return encoding6
case '@peardata/peer-link/hyperdb#1':
return encoding7
case '@peardata/alert-config/hyperdb#2':
return encoding8
case '@peardata/alert-event/hyperdb#3':
return encoding9
case '@peardata/metric-point/hyperdb#4':
return encoding10
case '@peardata/job/hyperdb#5':
return encoding11
default:
throw new Error('Encoder not found ' + name)
}
}
function getStruct(name, v = VERSION) {
const enc = getEncoding(name)
return {
preencode(state, m) {
version = v
enc.preencode(state, m)
},
encode(state, m) {
version = v
enc.encode(state, m)
},
decode(state) {
version = v
return enc.decode(state)
}
}
}
const resolveStruct = getStruct // compat
export { resolveStruct, getStruct, getEnum, getEncoding, encode, decode, setVersion, version }
+368
View File
@@ -0,0 +1,368 @@
// This file is autogenerated by the hyperschema compiler
// Schema Version: 1
/* eslint-disable camelcase */
/* eslint-disable quotes */
/* eslint-disable space-before-function-paren */
import { c } from 'hyperschema/runtime'
const VERSION = 1
// eslint-disable-next-line no-unused-vars
let version = VERSION
// @peardata/node
const encoding0 = {
preencode(state, m) {
c.string.preencode(state, m.nodeId)
c.string.preencode(state, m.hostname)
state.end++ // max flag is 64 so always one byte
if (m.publicKeyHex) c.string.preencode(state, m.publicKeyHex)
if (m.platform) c.string.preencode(state, m.platform)
if (m.arch) c.string.preencode(state, m.arch)
if (m.cpus) c.uint.preencode(state, m.cpus)
if (m.totalMemMiB) c.uint.preencode(state, m.totalMemMiB)
if (m.agentVersion) c.string.preencode(state, m.agentVersion)
if (m.labelsJson) c.string.preencode(state, m.labelsJson)
c.uint.preencode(state, m.updatedAt)
},
encode(state, m) {
const flags =
(m.publicKeyHex ? 1 : 0) |
(m.platform ? 2 : 0) |
(m.arch ? 4 : 0) |
(m.cpus ? 8 : 0) |
(m.totalMemMiB ? 16 : 0) |
(m.agentVersion ? 32 : 0) |
(m.labelsJson ? 64 : 0)
c.string.encode(state, m.nodeId)
c.string.encode(state, m.hostname)
c.uint.encode(state, flags)
if (m.publicKeyHex) c.string.encode(state, m.publicKeyHex)
if (m.platform) c.string.encode(state, m.platform)
if (m.arch) c.string.encode(state, m.arch)
if (m.cpus) c.uint.encode(state, m.cpus)
if (m.totalMemMiB) c.uint.encode(state, m.totalMemMiB)
if (m.agentVersion) c.string.encode(state, m.agentVersion)
if (m.labelsJson) c.string.encode(state, m.labelsJson)
c.uint.encode(state, m.updatedAt)
},
decode(state) {
const r0 = c.string.decode(state)
const r1 = c.string.decode(state)
const flags = c.uint.decode(state)
return {
nodeId: r0,
hostname: r1,
publicKeyHex: (flags & 1) !== 0 ? c.string.decode(state) : null,
platform: (flags & 2) !== 0 ? c.string.decode(state) : null,
arch: (flags & 4) !== 0 ? c.string.decode(state) : null,
cpus: (flags & 8) !== 0 ? c.uint.decode(state) : 0,
totalMemMiB: (flags & 16) !== 0 ? c.uint.decode(state) : 0,
agentVersion: (flags & 32) !== 0 ? c.string.decode(state) : null,
labelsJson: (flags & 64) !== 0 ? c.string.decode(state) : null,
updatedAt: c.uint.decode(state)
}
}
}
// @peardata/peer-link
const encoding1 = {
preencode(state, m) {
c.string.preencode(state, m.localNodeId)
c.string.preencode(state, m.remotePublicKey)
state.end++ // max flag is 16 so always one byte
if (m.role) c.string.preencode(state, m.role)
if (m.alias) c.string.preencode(state, m.alias)
if (m.dbKeyHex) c.string.preencode(state, m.dbKeyHex)
if (m.syncMode) c.string.preencode(state, m.syncMode)
c.uint.preencode(state, m.linkedAt)
if (m.lastSeen) c.uint.preencode(state, m.lastSeen)
},
encode(state, m) {
const flags =
(m.role ? 1 : 0) |
(m.alias ? 2 : 0) |
(m.dbKeyHex ? 4 : 0) |
(m.syncMode ? 8 : 0) |
(m.lastSeen ? 16 : 0)
c.string.encode(state, m.localNodeId)
c.string.encode(state, m.remotePublicKey)
c.uint.encode(state, flags)
if (m.role) c.string.encode(state, m.role)
if (m.alias) c.string.encode(state, m.alias)
if (m.dbKeyHex) c.string.encode(state, m.dbKeyHex)
if (m.syncMode) c.string.encode(state, m.syncMode)
c.uint.encode(state, m.linkedAt)
if (m.lastSeen) c.uint.encode(state, m.lastSeen)
},
decode(state) {
const r0 = c.string.decode(state)
const r1 = c.string.decode(state)
const flags = c.uint.decode(state)
return {
localNodeId: r0,
remotePublicKey: r1,
role: (flags & 1) !== 0 ? c.string.decode(state) : null,
alias: (flags & 2) !== 0 ? c.string.decode(state) : null,
dbKeyHex: (flags & 4) !== 0 ? c.string.decode(state) : null,
syncMode: (flags & 8) !== 0 ? c.string.decode(state) : null,
linkedAt: c.uint.decode(state),
lastSeen: (flags & 16) !== 0 ? c.uint.decode(state) : 0
}
}
}
// @peardata/alert-config
const encoding2 = {
preencode(state, m) {
c.string.preencode(state, m.id)
c.string.preencode(state, m.chart)
c.string.preencode(state, m.dimension)
state.end++ // max flag is 16 so always one byte
if (m.warn) c.float64.preencode(state, m.warn)
if (m.crit) c.float64.preencode(state, m.crit)
if (m.comparator) c.string.preencode(state, m.comparator)
if (m.info) c.string.preencode(state, m.info)
c.uint.preencode(state, m.updatedAt)
},
encode(state, m) {
const flags =
(m.warn ? 1 : 0) |
(m.crit ? 2 : 0) |
(m.comparator ? 4 : 0) |
(m.enabled ? 8 : 0) |
(m.info ? 16 : 0)
c.string.encode(state, m.id)
c.string.encode(state, m.chart)
c.string.encode(state, m.dimension)
c.uint.encode(state, flags)
if (m.warn) c.float64.encode(state, m.warn)
if (m.crit) c.float64.encode(state, m.crit)
if (m.comparator) c.string.encode(state, m.comparator)
if (m.info) c.string.encode(state, m.info)
c.uint.encode(state, m.updatedAt)
},
decode(state) {
const r0 = c.string.decode(state)
const r1 = c.string.decode(state)
const r2 = c.string.decode(state)
const flags = c.uint.decode(state)
return {
id: r0,
chart: r1,
dimension: r2,
warn: (flags & 1) !== 0 ? c.float64.decode(state) : 0,
crit: (flags & 2) !== 0 ? c.float64.decode(state) : 0,
comparator: (flags & 4) !== 0 ? c.string.decode(state) : null,
enabled: (flags & 8) !== 0,
info: (flags & 16) !== 0 ? c.string.decode(state) : null,
updatedAt: c.uint.decode(state)
}
}
}
// @peardata/alert-event
const encoding3 = {
preencode(state, m) {
c.string.preencode(state, m.id)
c.uint.preencode(state, m.ts)
c.string.preencode(state, m.chart)
state.end++ // max flag is 16 so always one byte
if (m.dimension) c.string.preencode(state, m.dimension)
c.string.preencode(state, m.severity)
if (m.value) c.float64.preencode(state, m.value)
if (m.threshold) c.float64.preencode(state, m.threshold)
if (m.message) c.string.preencode(state, m.message)
},
encode(state, m) {
const flags =
(m.dimension ? 1 : 0) |
(m.value ? 2 : 0) |
(m.threshold ? 4 : 0) |
(m.message ? 8 : 0) |
(m.cleared ? 16 : 0)
c.string.encode(state, m.id)
c.uint.encode(state, m.ts)
c.string.encode(state, m.chart)
c.uint.encode(state, flags)
if (m.dimension) c.string.encode(state, m.dimension)
c.string.encode(state, m.severity)
if (m.value) c.float64.encode(state, m.value)
if (m.threshold) c.float64.encode(state, m.threshold)
if (m.message) c.string.encode(state, m.message)
},
decode(state) {
const r0 = c.string.decode(state)
const r1 = c.uint.decode(state)
const r2 = c.string.decode(state)
const flags = c.uint.decode(state)
return {
id: r0,
ts: r1,
chart: r2,
dimension: (flags & 1) !== 0 ? c.string.decode(state) : null,
severity: c.string.decode(state),
value: (flags & 2) !== 0 ? c.float64.decode(state) : 0,
threshold: (flags & 4) !== 0 ? c.float64.decode(state) : 0,
message: (flags & 8) !== 0 ? c.string.decode(state) : null,
cleared: (flags & 16) !== 0
}
}
}
// @peardata/metric-point
const encoding4 = {
preencode(state, m) {
c.string.preencode(state, m.chart)
c.uint.preencode(state, m.ts)
c.string.preencode(state, m.context)
c.uint.preencode(state, m.tier)
c.string.preencode(state, m.valuesJson)
},
encode(state, m) {
c.string.encode(state, m.chart)
c.uint.encode(state, m.ts)
c.string.encode(state, m.context)
c.uint.encode(state, m.tier)
c.string.encode(state, m.valuesJson)
},
decode(state) {
const r0 = c.string.decode(state)
const r1 = c.uint.decode(state)
const r2 = c.string.decode(state)
const r3 = c.uint.decode(state)
const r4 = c.string.decode(state)
return {
chart: r0,
ts: r1,
context: r2,
tier: r3,
valuesJson: r4
}
}
}
// @peardata/job
const encoding5 = {
preencode(state, m) {
c.string.preencode(state, m.id)
c.string.preencode(state, m.name)
c.string.preencode(state, m.status)
state.end++ // max flag is 8 so always one byte
if (m.startedAt) c.uint.preencode(state, m.startedAt)
if (m.finishedAt) c.uint.preencode(state, m.finishedAt)
if (m.resultJson) c.string.preencode(state, m.resultJson)
if (m.error) c.string.preencode(state, m.error)
},
encode(state, m) {
const flags =
(m.startedAt ? 1 : 0) | (m.finishedAt ? 2 : 0) | (m.resultJson ? 4 : 0) | (m.error ? 8 : 0)
c.string.encode(state, m.id)
c.string.encode(state, m.name)
c.string.encode(state, m.status)
c.uint.encode(state, flags)
if (m.startedAt) c.uint.encode(state, m.startedAt)
if (m.finishedAt) c.uint.encode(state, m.finishedAt)
if (m.resultJson) c.string.encode(state, m.resultJson)
if (m.error) c.string.encode(state, m.error)
},
decode(state) {
const r0 = c.string.decode(state)
const r1 = c.string.decode(state)
const r2 = c.string.decode(state)
const flags = c.uint.decode(state)
return {
id: r0,
name: r1,
status: r2,
startedAt: (flags & 1) !== 0 ? c.uint.decode(state) : 0,
finishedAt: (flags & 2) !== 0 ? c.uint.decode(state) : 0,
resultJson: (flags & 4) !== 0 ? c.string.decode(state) : null,
error: (flags & 8) !== 0 ? c.string.decode(state) : null
}
}
}
function setVersion(v) {
version = v
}
function encode(name, value, v = VERSION) {
version = v
return c.encode(getEncoding(name), value)
}
function decode(name, buffer, v = VERSION) {
version = v
return c.decode(getEncoding(name), buffer)
}
function getEnum(name) {
switch (name) {
default:
throw new Error('Enum not found ' + name)
}
}
function getEncoding(name) {
switch (name) {
case '@peardata/node':
return encoding0
case '@peardata/peer-link':
return encoding1
case '@peardata/alert-config':
return encoding2
case '@peardata/alert-event':
return encoding3
case '@peardata/metric-point':
return encoding4
case '@peardata/job':
return encoding5
default:
throw new Error('Encoder not found ' + name)
}
}
function getStruct(name, v = VERSION) {
const enc = getEncoding(name)
return {
preencode(state, m) {
version = v
enc.preencode(state, m)
},
encode(state, m) {
version = v
enc.encode(state, m)
},
decode(state) {
version = v
return enc.decode(state)
}
}
}
const resolveStruct = getStruct // compat
export { resolveStruct, getStruct, getEnum, getEncoding, encode, decode, setVersion, version }
+341
View File
@@ -0,0 +1,341 @@
{
"version": 1,
"schema": [
{
"name": "node",
"namespace": "peardata",
"compact": false,
"flagsPosition": 2,
"fields": [
{
"name": "nodeId",
"required": true,
"type": "string",
"version": 1
},
{
"name": "hostname",
"required": true,
"type": "string",
"version": 1
},
{
"name": "publicKeyHex",
"required": false,
"type": "string",
"version": 1
},
{
"name": "platform",
"required": false,
"type": "string",
"version": 1
},
{
"name": "arch",
"required": false,
"type": "string",
"version": 1
},
{
"name": "cpus",
"required": false,
"type": "uint",
"version": 1
},
{
"name": "totalMemMiB",
"required": false,
"type": "uint",
"version": 1
},
{
"name": "agentVersion",
"required": false,
"type": "string",
"version": 1
},
{
"name": "labelsJson",
"required": false,
"type": "string",
"version": 1
},
{
"name": "updatedAt",
"required": true,
"type": "uint",
"version": 1
}
]
},
{
"name": "peer-link",
"namespace": "peardata",
"compact": false,
"flagsPosition": 2,
"fields": [
{
"name": "localNodeId",
"required": true,
"type": "string",
"version": 1
},
{
"name": "remotePublicKey",
"required": true,
"type": "string",
"version": 1
},
{
"name": "role",
"required": false,
"type": "string",
"version": 1
},
{
"name": "alias",
"required": false,
"type": "string",
"version": 1
},
{
"name": "dbKeyHex",
"required": false,
"type": "string",
"version": 1
},
{
"name": "syncMode",
"required": false,
"type": "string",
"version": 1
},
{
"name": "linkedAt",
"required": true,
"type": "uint",
"version": 1
},
{
"name": "lastSeen",
"required": false,
"type": "uint",
"version": 1
}
]
},
{
"name": "alert-config",
"namespace": "peardata",
"compact": false,
"flagsPosition": 3,
"fields": [
{
"name": "id",
"required": true,
"type": "string",
"version": 1
},
{
"name": "chart",
"required": true,
"type": "string",
"version": 1
},
{
"name": "dimension",
"required": true,
"type": "string",
"version": 1
},
{
"name": "warn",
"required": false,
"type": "float64",
"version": 1
},
{
"name": "crit",
"required": false,
"type": "float64",
"version": 1
},
{
"name": "comparator",
"required": false,
"type": "string",
"version": 1
},
{
"name": "enabled",
"required": false,
"type": "bool",
"version": 1
},
{
"name": "info",
"required": false,
"type": "string",
"version": 1
},
{
"name": "updatedAt",
"required": true,
"type": "uint",
"version": 1
}
]
},
{
"name": "alert-event",
"namespace": "peardata",
"compact": false,
"flagsPosition": 3,
"fields": [
{
"name": "id",
"required": true,
"type": "string",
"version": 1
},
{
"name": "ts",
"required": true,
"type": "uint",
"version": 1
},
{
"name": "chart",
"required": true,
"type": "string",
"version": 1
},
{
"name": "dimension",
"required": false,
"type": "string",
"version": 1
},
{
"name": "severity",
"required": true,
"type": "string",
"version": 1
},
{
"name": "value",
"required": false,
"type": "float64",
"version": 1
},
{
"name": "threshold",
"required": false,
"type": "float64",
"version": 1
},
{
"name": "message",
"required": false,
"type": "string",
"version": 1
},
{
"name": "cleared",
"required": false,
"type": "bool",
"version": 1
}
]
},
{
"name": "metric-point",
"namespace": "peardata",
"compact": false,
"flagsPosition": -1,
"fields": [
{
"name": "chart",
"required": true,
"type": "string",
"version": 1
},
{
"name": "ts",
"required": true,
"type": "uint",
"version": 1
},
{
"name": "context",
"required": true,
"type": "string",
"version": 1
},
{
"name": "tier",
"required": true,
"type": "uint",
"version": 1
},
{
"name": "valuesJson",
"required": true,
"type": "string",
"version": 1
}
]
},
{
"name": "job",
"namespace": "peardata",
"compact": false,
"flagsPosition": 3,
"fields": [
{
"name": "id",
"required": true,
"type": "string",
"version": 1
},
{
"name": "name",
"required": true,
"type": "string",
"version": 1
},
{
"name": "status",
"required": true,
"type": "string",
"version": 1
},
{
"name": "startedAt",
"required": false,
"type": "uint",
"version": 1
},
{
"name": "finishedAt",
"required": false,
"type": "uint",
"version": 1
},
{
"name": "resultJson",
"required": false,
"type": "string",
"version": 1
},
{
"name": "error",
"required": false,
"type": "string",
"version": 1
}
]
}
]
}
+32
View File
@@ -0,0 +1,32 @@
import test from 'brittle'
import { MetricsCollector } from '../server/services/collector.js'
import { CHART_BY_ID, getAllChartDefs } from '../shared/metrics.js'
test('collector emits core system charts', async (t) => {
const c = new MetricsCollector({ intervalMs: 50 })
/** @type {any[]} */
let batch = []
c.once('samples', (b) => {
batch = b
})
c.start()
await new Promise((r) => setTimeout(r, 80))
c.stop()
t.ok(batch.length > 5)
const charts = new Set(batch.map((s) => s.chart))
t.ok(charts.has('system.cpu'))
t.ok(charts.has('system.ram') || charts.has('mem.available'))
t.ok(charts.has('system.load'))
t.ok(charts.has('system.uptime'))
const cpu = batch.find((s) => s.chart === 'system.cpu')
t.ok(cpu)
for (const dim of ['user', 'system', 'idle', 'iowait', 'steal']) {
t.ok(dim in cpu.values, dim)
}
// runtime instance charts should be registered after first tick
t.ok(getAllChartDefs().length >= 30)
t.ok(CHART_BY_ID.has('system.cpu'))
})
+105
View File
@@ -0,0 +1,105 @@
import test from 'brittle'
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import { PearDataModel } from '../server/db/model.js'
import Corestore from 'corestore'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const TMP = path.join(__dirname, '..', 'tmp-hyperdb-test')
async function withModel(fn) {
fs.rmSync(TMP, { recursive: true, force: true })
const store = new Corestore(path.join(TMP, 'corestore'))
await store.ready()
const core = store.get({ name: 'peardata-meta' })
const model = new PearDataModel(core, { autoUpdate: true })
await model.ready()
try {
await fn(model)
} finally {
await model.close().catch(() => {})
await store.close().catch(() => {})
fs.rmSync(TMP, { recursive: true, force: true })
}
}
test('hyperdb put/get node', async (t) => {
await withModel(async (db) => {
await db.putNode({
nodeId: 'node-a',
hostname: 'lab',
publicKeyHex: 'ab'.repeat(32),
cpus: 4,
updatedAt: Date.now(),
})
const row = await db.getNode('node-a')
t.is(row.hostname, 'lab')
t.is(row.cpus, 4)
})
})
test('hyperdb peer links', async (t) => {
await withModel(async (db) => {
const remote = 'cd'.repeat(32)
await db.putPeerLink({
localNodeId: 'local',
remotePublicKey: remote,
syncMode: 'pull',
alias: 'nas',
})
const links = await db.listPeerLinks('local')
t.is(links.length, 1)
t.is(links[0].alias, 'nas')
await db.deletePeerLink('local', remote)
t.is((await db.listPeerLinks('local')).length, 0)
})
})
test('hyperdb warm metric points', async (t) => {
await withModel(async (db) => {
const ts = Date.now()
await db.putMetricPoints([
{
chart: 'system.cpu',
context: 'system.cpu',
ts: ts - 1000,
values: { user: 10, idle: 90 },
tier: 1,
},
{
chart: 'system.cpu',
context: 'system.cpu',
ts,
values: { user: 20, idle: 80 },
tier: 1,
},
])
const rows = await db.queryMetricPoints({
chart: 'system.cpu',
afterMs: ts - 5000,
beforeMs: ts + 1000,
})
t.is(rows.length, 2)
t.is(rows[1].values.user, 20)
})
})
test('hyperdb alert event', async (t) => {
await withModel(async (db) => {
const ts = Date.now()
await db.putAlertEvent({
id: 'cpu_user_high',
ts,
chart: 'system.cpu',
dimension: 'user',
severity: 'warning',
value: 90,
threshold: 80,
message: 'high',
})
const evs = await db.listAlertEvents({ chart: 'system.cpu', limit: 10 })
t.ok(evs.length >= 1)
t.is(evs[0].severity, 'warning')
})
})
+23 -3
View File
@@ -9,7 +9,7 @@ import {
Pushes,
} from '../shared/protocol.js'
import { validateMethodArgs, SCHEMA_VERSION } from '../shared/schema.js'
import { CHART_DEFS, CONTEXT_IDS } from '../shared/metrics.js'
import { CHART_DEFS, CONTEXT_IDS, getAllChartDefs, getContextIds } from '../shared/metrics.js'
test('protocol constants', (t) => {
t.is(PROTOCOL, 'peardata/rpc')
@@ -34,6 +34,9 @@ test('method roles cover monitoring surface', (t) => {
'listCharts',
'mintInvite',
'runJob',
'getDbInfo',
'linkPeer',
'unlinkPeer',
]) {
t.ok(MethodRoles[m], m)
t.is(Methods[m], m)
@@ -45,8 +48,25 @@ test('pushes include metrics + anomaly', (t) => {
t.ok(Pushes.anomaly.startsWith('push:'))
})
test('metrics catalog non-empty', (t) => {
t.ok(CHART_DEFS.length >= 5)
test('metrics catalog covers system stats', (t) => {
t.ok(CHART_DEFS.length >= 30)
t.ok(getAllChartDefs().length >= CHART_DEFS.length)
const contexts = getContextIds()
for (const id of [
'system.cpu',
'system.ram',
'system.load',
'system.io',
'system.net',
'system.ip',
'mem.available',
'mem.swap',
'ip.tcpsock',
'ipv4.packets',
'system.cpu_some_pressure',
]) {
t.ok(contexts.includes(id), id)
}
t.ok(CONTEXT_IDS.includes('system.cpu'))
})
+16 -10
View File
@@ -12,14 +12,14 @@ initAuthKeys({
publicKeyHex: b4a.toString(crypto.keyPair(seed).publicKey, 'hex'),
})
test('REST /api/v3/info', (t) => {
const res = handleRest('/api/v3/info', new URLSearchParams())
test('REST /api/v3/info', async (t) => {
const res = await handleRest('/api/v3/info', new URLSearchParams())
t.is(res.status, 200)
t.ok(res.body.hostname)
t.ok(res.body.peardata)
})
test('REST /api/v1/charts after ingest', (t) => {
test('REST /api/v1/charts after ingest', async (t) => {
getStore().ingest([
{
chart: 'system.load',
@@ -28,12 +28,12 @@ test('REST /api/v1/charts after ingest', (t) => {
values: { load1: 0.5, load5: 0.4, load15: 0.3 },
},
])
const res = handleRest('/api/v1/charts', new URLSearchParams())
const res = await handleRest('/api/v1/charts', new URLSearchParams())
t.is(res.status, 200)
t.ok(res.body.charts['system.load'] || res.body.charts['system.cpu'])
})
test('REST /api/v3/data', (t) => {
test('REST /api/v3/data', async (t) => {
const now = Date.now()
for (let i = 0; i < 10; i++) {
getStore().ingest([
@@ -53,7 +53,7 @@ test('REST /api/v3/data', (t) => {
},
])
}
const res = handleRest(
const res = await handleRest(
'/api/v3/data',
new URLSearchParams({ chart: 'system.cpu', after: '-30', points: '10' })
)
@@ -61,13 +61,19 @@ test('REST /api/v3/data', (t) => {
t.ok(Array.isArray(res.body.data))
})
test('REST /api/v3/contexts', (t) => {
const res = handleRest('/api/v3/contexts', new URLSearchParams())
test('REST /api/v3/contexts', async (t) => {
const res = await handleRest('/api/v3/contexts', new URLSearchParams())
t.is(res.status, 200)
t.ok(res.body.contexts['system.cpu'])
})
test('REST 404', (t) => {
const res = handleRest('/api/v9/nope', new URLSearchParams())
test('REST /api/v3/db', async (t) => {
const res = await handleRest('/api/v3/db', new URLSearchParams())
t.is(res.status, 200)
t.ok('enabled' in res.body)
})
test('REST 404', async (t) => {
const res = await handleRest('/api/v9/nope', new URLSearchParams())
t.is(res.status, 404)
})
+5 -4
View File
@@ -1,7 +1,7 @@
import test from 'brittle'
import { MetricStore } from '../server/services/store.js'
test('store ingest + query', (t) => {
test('store ingest + query', async (t) => {
const store = new MetricStore()
const now = Date.now()
for (let i = 0; i < 30; i++) {
@@ -27,15 +27,16 @@ test('store ingest + query', (t) => {
t.ok(meta)
t.is(meta.context, 'system.cpu')
const q = store.query({ chart: 'system.cpu', after: -60, before: 0, points: 15 })
const q = await store.query({ chart: 'system.cpu', after: -60, before: 0, points: 15 })
t.absent(q.error)
t.ok(q.data.length > 0)
t.ok(q.labels.includes('user'))
t.ok(q.labels.includes('time'))
t.ok(q.source)
})
test('store unknown chart', (t) => {
test('store unknown chart', async (t) => {
const store = new MetricStore()
const q = store.query({ chart: 'nope.chart', points: 10 })
const q = await store.query({ chart: 'nope.chart', points: 10 })
t.ok(q.error)
})