loadtest --live-json emits newline-delimited JSON (NDJSON) on stdout —
one self-describing event per line — instead of the human progress table. It lets
an orchestrator consume live/structured telemetry (per-interval samples, error
classification, run outcome) rather than scraping a human-readable line, which is
brittle and loses TTFT + failure detail.
Default behavior is unchanged: without --live-json, the human table prints
exactly as before.
- stdout carries ONLY NDJSON. Every line is one complete JSON object; each line parses independently. The human report + file-path lines move to stderr.
- stderr carries diagnostics only (banner, FD warnings, the final text report).
- No secrets, ever. No event contains an
Authorizationheader, bearer token, virtual key, request body, or prompt content. Events carry metrics + labels only. - Exit code:
0= the load tool ran to completion (even if the target failed every request — that is reported in the summary, not the exit code); non-zero = the load tool itself could not run (bad config, unreachable from the start).
loadtest --config <profile> --live-json \
[--run-id <id>] [--target-name <name>] [--target-mode <mode>] \
[--target <url>] [--model <m>] [--vk <vk>] [--stages '@2000:300s']
--run-id / --target-name / --target-mode are labels only, stamped on
every event so a consumer can attribute a multiplexed stream. They do not affect
the load run. (--vk still sets the bearer token; it is never echoed in output.)
To emit per-interval sample events the profile must set a positive
live_interval (e.g. "1s") and the stage must last at least two intervals;
otherwise samples are suppressed (as with the human progress line).
Every event carries:
| field | type | meaning |
|---|---|---|
schema |
string | always "nexus-loadtest/live/v1" |
type |
string | run_started | sample | error_summary | run_finished | run_failed |
run_id |
string | the --run-id label (may be empty) |
ts |
string | RFC3339Nano UTC emit time |
target |
object | { name, base_url, mode } — descriptive labels |
{ "schema":"nexus-loadtest/live/v1", "type":"run_started", "run_id":"...", "ts":"...",
"target":{"name":"nexus-hooks-on","base_url":"http://...","mode":"hooks-on"},
"config":{ "rps":2000, "duration_s":300, "concurrency":20, "streaming":false, "stages":1 } }config echoes the requested shape (peak offered rate, total duration, peak
concurrency, whether any scenario streams, stage count).
{ "type":"sample", "stage":1, "target":{...}, "phase":"measured",
"metrics":{ "rps":1998.4, "lat_p50_ms":8.5, "lat_p99_ms":68.0, "ttft_p95_ms":22.0,
"ok_pct":100.0, "error_rate":0.0 } }The rolling-window observation for the last interval. ok_pct = 100 − err_pct;
error_rate is a fraction.
phase is "warmup" or "measured", and is omitted when the window cannot
be assigned to one. It sits at the top level, beside metrics, not inside it —
it describes when the window was taken, not a measurement of the system under
test, and consumers read it off the event.
The label applies to the whole window, [elapsed − live_interval, elapsed]:
| window position | phase |
|---|---|
entirely within warmup |
"warmup" |
entirely after warmup |
"measured" |
| straddling the boundary | omitted |
warmup is 0s |
"measured" (no warm window exists) |
A straddling window contains both warm-up and steady-state requests and is
honestly neither, so it carries no label rather than a confident wrong one — the
field exists so a consumer can EXCLUDE warm-up, and a mislabelled window defeats
that. Treat an absent phase as unknown; do not default it to "measured" and do
not infer it from a neighbouring sample.
Note this marker is for interval-level analysis only. Stage summaries already
exclude warm-up from steady-state figures, so run_finished percentiles are
unaffected by it.
{ "type":"error_summary", "target":{...},
"errors":{ "total":250, "http_401":250, "http_403":0, "http_404":0, "http_429":0,
"http_5xx":0, "timeouts":0, "connection_errors":0,
"generator_port_exhaustion":0, "generator_fd_exhaustion":0, "other":0 },
"classification_hint":"auth", "generator_side":false }Every count is observed (status codes + error classes). classification_hint
is the dominant bucket, one of: auth (401/403), route (404), upstream
(5xx), capacity (429 or generator exhaustion), timeout, connection,
unknown. It is a hint from client-side evidence, not a root cause — the tool
never asserts a cause it cannot see. A clean stage emits no error_summary.
Honesty boundary —
generator_side."cannot assign requested address"(ephemeral-port exhaustion) and"too many open files"are this load tool's limits, not the gateway's. When such a bucket dominates,generator_side:trueandclassification_hint:"capacity". A consumer MUST mapgenerator_side:trueto a generator/rig limit (e.g.loadgen_limited) — never to a gateway failure likefailed_capacity. Blaming the gateway for the generator's ceiling is the exact false-signal this flag exists to prevent.
{ "type":"run_finished", "status":"completed", "target":{...},
"summary":{ "rps":2000.0, "lat_p50_ms":8.0, "lat_p99_ms":69.0, "ttft_p95_ms":24.0,
"ok_pct":100.0, "error_rate":0.0, "requests_total":600000, "requests_ok":600000 } }status is "completed" (natural end) or "interrupted" (SIGINT/SIGTERM — a
clean early stop). Both mean the tool worked; how the target did is in the
summary (ok_pct / error_rate). A 100%-failing target still yields
status:"completed" with ok_pct:0.
{ "type":"run_failed", "status":"failed", "target":{...},
"failure":{ "class":"configuration", "reason":"load config: ...",
"generator_side":false, "http_status":null },
"last_sample":{ ... }|null }Emitted only for a runner-level fatal (bad profile, target unreachable from
the start), distinct from a target that merely returned errors. class follows
the same evidence rules (configuration for a setup/wiring failure the tool hit).
The process also exits non-zero.
schema is nexus-loadtest/live/v1. Additive fields (new metrics, new error
buckets) do not bump the version — consumers must ignore unknown fields. A
breaking change (renamed/removed field, changed semantics) bumps to v2.
- Parse line-by-line; ignore blank lines and unknown
types / fields. - Treat
run_finished status:completed ok_pct:0as the target failed, not as a tool failure — readerror_summary/classification_hintfor the why. - Honor
generator_side:true— it is a rig limit, not a gateway verdict. - Never expect secrets; never log the raw stream expecting to find auth material.