Skip to content

WebSocket Stream

The dashboard’s live tail is powered by a single WebSocket endpoint:

ws://127.0.0.1:8080/api/v1/ws

The wire format is JSON. Each frame is a self-describing message with a type discriminator.

const ws = new WebSocket('ws://127.0.0.1:8080/api/v1/ws');
ws.onmessage = (frame) => {
const msg = JSON.parse(frame.data);
switch (msg.type) {
case 'event': /* ... */ break;
case 'alert': /* ... */ break;
case 'status': /* ... */ break;
}
};

The server pushes; the client does not need to subscribe — every connected client receives every event and alert. Filtering happens client-side in the widgets.

Emitted whenever a new event is committed to storage.

{
"type": "event",
"event": {
"id": "01HZX...",
"observed_ts": "2026-05-13T15:30:00.123Z",
"event_ts": "2026-05-13T15:29:59.998Z",
"source": "syslog",
"severity": 13,
"message": "Failed password for root from 10.0.1.42",
"template_id": "drain_0042",
"template": "Failed password for <*> from <*>",
"entities": [
{ "type": "user", "value": "root", "uuid": "..." },
{ "type": "ipv4", "value": "10.0.1.42", "uuid": "..." }
],
"anomaly_score": 0.87,
"anomaly_threshold": 0.63,
"attack": { "tactic": "credential-access", "technique": "T1110.001" }
}
}

Emitted when an alert fires — Sigma match, blended-score breach, kill-chain detection, UEBA, threat-intel match, or risk-accumulation threshold.

{
"type": "alert",
"alert": {
"id": "alr_01HZX...",
"ts": "2026-05-13T15:30:00.456Z",
"rule_id": "ssh_brute_force",
"rule_kind": "sigma",
"severity": "high",
"title": "SSH brute force",
"entities": [
{ "type": "ipv4", "value": "10.0.1.42", "uuid": "..." }
],
"attack": { "tactic": "credential-access", "technique": "T1110.001" },
"dedup_key": "sigma:ssh_brute_force:10.0.1.42"
}
}

Periodic pipeline heartbeat — surfaces throughput, queue depth, and degraded subsystems. The dashboard uses it to drive the disconnected banner and stats card.

{
"type": "status",
"ts": "2026-05-13T15:30:01.000Z",
"throughput_eps": 4287,
"queue_depth": 23,
"queue_max": 10000,
"receivers": { "syslog": "ok", "otlp_grpc": "ok", "file": "ok" },
"detectors": { "hst": "ok", "holt_winters": "ok", "cusum": "ok", "markov": "ok" }
}

The bundled dashboard reconnects with exponential backoff (250 ms → 8 s, capped) and shows the red Disconnected banner until the next message arrives. Build the same pattern into any custom client — there is no message replay, so reconnecting clients only receive frames sent after they are subscribed.

When a widget first mounts it issues a normal REST call (GET /api/v1/alerts?since=…, GET /api/v1/events?since=…) to populate history, then transitions to the WebSocket for live tail. See the REST API for the backfill endpoints.