Skip to content

v2.3.0

Choose a tag to compare

@github-actions github-actions released this 26 Aug 08:28
· 12 commits to main since this release
fb03881

The per-interval time series is now something an external plotter can consume
directly, so reporting stays outside the binary. Nothing about what zrk
measures changes: pacing, coordinated-omission correction and every number in
the report mean exactly what they did in 2.2.2.

Piping the series into a plotter

--timeseries - streams the NDJSON rows to stdout instead of a file, so they
go straight into a streaming plotter such as jplot,
which reads newline-delimited JSON and addresses fields by dotted path:

zrk -c50 -R1000 -d5m --timeseries - http://127.0.0.1:8080/ \
  | jplot achieved_rate+target_rate \
          latency_us.p50+latency_us.p90+latency_us.p99 \
          error_rate

There is no jaggr stage in that pipeline, unlike vegeta's. Aggregating raw
per-request samples into per-interval percentiles is what zrk already does
in-process, with real HdrHistograms — the rows arrive pre-aggregated and
lossless.

stdout then belongs to the row stream, so the dashboard is suppressed and the
final report goes to --output if set, else stderr. That last part is not
cosmetic: without it the plain append-only lines interleave with the rows and
the reader dies on the first one it meets — [ 0.5s] 100 req/s decodes as
a JSON array, then fails on the s.

Pair it with -o when the report is machine-read. --format json --timeseries - puts the JSON summary on stderr alongside the human notices,
so a parser reading stderr whole can trip over a trailing SLO-breach line;
--format json -o result.json --timeseries - keeps all three streams apart.

What the rows carry now

{"t":1.006,"target_rate":480.0,"achieved_rate":476.2,"requests":476,"errors":3,"error_rate":0.006263,"errors_by_kind":{"connect":0,"read":0,"write":0,"timeout":1,"deadline":2,"non_2xx_3xx":0},"bytes":58852,"bytes_per_sec":58501.0,"max_schedule_lag_us":18524,"latency_us":{"p50":245,"p90":669,"p99":1745,"p99_9":2401,"max":2401}}

errors_by_kind splits the errors scalar the way the final summary's
errors object does. A window whose tail broke into deadline misses and one
that broke into connect failures are different findings, and the single number
cannot tell them apart — stacked, they say how a run failed, not just that
it did.

error_rate is that window's failure fraction, computed exactly like the
summary's top-level error_rate and so directly comparable to it and to the
--max-error-rate gate. Plot this rather than the raw errors count: it sits
on a fixed 0..1 axis whatever --interval is set to, where the count silently
rescales with the window. The two share their arithmetic, so they cannot drift.

The gauge that is not a delta

Every count in a row is that interval's delta, so the rows sum to the run.
max_schedule_lag_us is deliberately the exception: it is a running peak
aggregated by max, not a tally, so there is nothing to difference. The row
carries the cumulative high-water mark as of that interval — the same gauge
the summary reports.

An interval-local peak would mean the connections resetting the gauge on the
row cadence, which would cost the final report its true peak. Read the series
as a staircase instead: each riser dates the window in which the client fell
further behind its schedule than it ever had before, which is what puts the
onset of a backlog on the same time axis as the latency it explains.

Redirected output no longer clobbers itself

Every writer onto a shared stream is now streaming rather than positional.
.init pwrites from byte 0 on each fresh Writer — right for a file just
created, wrong for stdout or stderr, where earlier writes have already advanced
the offset. The same hazard was fixed for writeAll in 2.2.0; these are the
remaining paths.

One of them predates this release: zrk --format json >>file wrote the
summary from byte 0 rather than appending, silently overwriting whatever was
already in the file. --timeseries - >>file had the same flaw, and with stderr
redirected to a file an SLO-breach notice could land in the middle of the
report it followed — corrupting the JSON summary outright. All three are fixed
by picking the writer mode from whether --output actually opened a file.

Compatibility

Existing consumers are unaffected. errors stays a scalar count and every
previously emitted field keeps its name, type and meaning; the new fields are
additions. --timeseries <file> behaves exactly as before — only the literal
argument - is newly special.