Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
Added
- Project website at https://zoosky.github.io/driller, built with
Accent CMS from the repository's own markdown
(
docs/,SYNTAX.md,example/README.md,FORK.md,CONTRIBUTING.md,CHANGELOG.md) plus a landing page and a getting-started guide undersite/. The newPagesworkflow builds it on every push tomain(and checks links on pull requests) and publishes the output to GitHub Pages. The mounted markdown files gained a short frontmatter block that sets each page's title, summary, and position in the docs navigation.site/is excluded from the crates.io package.
Changed
- A failed request now prints a concise, classified line instead of a raw
reqwest::ErrorDebugdump. The line matches the success line format (step name, URL, then a redERR <cause>marker where the status would be) -- for exampleERR request timed out,ERR connection refused,ERR DNS resolution failed, orERR TLS error. The cause is classified from reqwest's own predicates plus a walk of the error'ssource()chain (no new dependency). Under--verbosethe full underlyingsource()chain is appended on a dimmedcause:line, so the detail from the old dump is still available on demand. The suppression gate (--quiet), the synthetic520recorded for--stats, and the status-code breakdown are all unchanged. - Internal: the library error type (
driller::Error) now derives itsDisplay/Errorimplementations withthiserrorinstead of hand-writtenimplblocks. User-facing messages and thesource()chain are byte-for-byte unchanged, and the public error API is identical (non-breaking).thiserrorwas already present transitively (viareqwest's hickory-dns feature), so this adds no new crate to the build.
Security
- Bump
h20.4.14 -> 0.4.19 to clear RUSTSEC-2026-0258, in which the HTTP/2 implementation accepted and queued empty DATA frames without limit -- unbounded memory growth on a stream that is not drained, or a panic from an integer overflow. Pulled in transitively viareqwest, and reachable on any run whose target negotiates HTTP/2, so a hostile or malfunctioning target server could exhaust the load generator. Lockfile-only change; no driller source is affected.
0.13.0 - 2026-07-03
Added
--stats-format <text|json>selects the statistics output format (defaulttext, unchanged).--stats-format jsonemits the same global and per-step statistics as a single JSON document to stdout -- and nothing else on stdout: the run banner and any warnings are routed to stderr and per-request progress is suppressed (as with--quiet), sodriller run ... --stats-format json | jq .always sees valid JSON. It implies--stats. The document carries a top-level integerschemaversion; each bucket reports totals, astatus_countsmap (exact code to count, including the synthetic520), aclass_rollup(2xx/3xx/4xx/5xxplus a separateconnection_errorsfor520), andlatency_mspercentiles as raw milliseconds (--nanosecdoes not affect the JSON).--reportis unaffected;--stats-format jsoncannot be combined with--compare(both write to stdout), the same restriction--statsalready has.driller run -reads the ad-hoc target URL from standard input, so a single-endpoint load test composes in a shell pipeline -- for exampleecho http://localhost:9000/health | driller run - --duration 10s --stats. The URL is the first non-empty line of stdin (trimmed of surrounding whitespace and a leading UTF-8 BOM) and runs through the same synthetic-GET path asdriller run <URL>. Because-is an ad-hoc source it cannot be combined with--benchmark(that pairing exits1with a clear message); empty stdin prints the standarderror: either a URL or --benchmark is required; and unreadable or non-UTF-8 stdin exits1witherror: couldn't read URL from stdin: ...rather than a misleading missing-URL message.
Changed
- The ad-hoc runner now rejects a target URL without a scheme (for example
example.com) up front witherror: URL must include a scheme, e.g. http://example.comand exit1, instead of building a malformed base URL that fails every request. Applies to a positionaldriller run <URL>, a URL piped viadriller run -, and a--base-urloverride; driller drives HTTP(S) only. - Library API (source-breaking):
RunOptions(re-exported from the crate root) gains a publicmachine_readablefield, so downstream code that constructs it with a struct literal must set the new field. This is why 0.13.0 is a minor rather than a patch: the library surface is not source-compatible with 0.12.x, even though every CLI flag and plan file is.
0.12.0 - 2026-07-01
Added
- A reusable library target (
src/lib.rs). driller was a binary-only crate; the load-testing engine is now exposed as a library with a single public entry point,driller::run(&RunOptions) -> Result<BenchmarkResult, driller::Error>, alongside theactions,benchmark,checker,config,expandable,reader, andtagsmodules. Integration tests, benchmarks, and sibling crates can drive the engine directly instead of shelling out to the binary. The CLI (src/main.rs) keeps its behavior and now drives the engine through this public API.
Changed
- The library no longer calls
std::process::exiton bad input. The engine (driller::runand thereader/config/tags/checkerhelpers) now returns a typeddriller::Errorfor an unreadable or malformed plan, an invalid configuration, an empty plan, an empty/malformed--comparebaseline, and an empty tag listing, leaving the exit decision to the binary. Embedding driller as a crate no longer risks having the whole host process terminated from deep inside the engine. Fatal-input messages printed by the CLI are now uniformly prefixed witherror:(for example a missing--benchmarkfile still printserror: couldn't open <path>: ...and exits1, with no Rust panic backtrace).
Fixed
driller --versionnow embeds the correct commit hash for installs from crates.io. The hash was derived only fromgit rev-parse, which returns nothing when building the published tarball (it carries no.git), socargo install drillerreportedunknown(and once printed a bare0.11.0 ()). The publish step now writes the release commit's short hash into the packaged tarball andbuild.rsprefers it, so an installed binary is traceable back to its source. The hash is still resolved viagit rev-parsefirst in a normal checkout, so developer builds are unaffected.
Security
- Bump
quinn-proto0.11.14 -> 0.11.15 to address RUSTSEC-2026-0185, a high-severity remote memory exhaustion from unbounded out-of-order QUIC stream reassembly. Pulled in transitively viareqwest's optional QUIC support; driller is not directly affected, but the bump keeps--lockedbuilds and the security audit clean. - Bump
anyhow1.0.102 -> 1.0.103 to clear RUSTSEC-2026-0190, an unsoundness inError::downcast_mut(). Present only transitively through build-timewit-bindgen/getrandomtooling; driller does not useanyhowdirectly.
0.11.1 - 2026-06-01
Changed
--reportnow runs the full benchmark and writes every request (all iterations, in completion order) to the report file, honoringconcurrency/iterations/durationlike any other run. Previously report mode executed a single hard-coded iteration and ignored those properties, so the report captured only one request per plan step (fcsonline/drill#87).--reportcomposes with--stats, which now reports over the full run.--comparenow averages both the baseline and the current run per requestnameand compares each name's mean duration, instead of comparing by position in the file. This keeps the verdict stable regardless of iteration count or the order concurrent iterations finished in (withconcurrency > 1positions are not reproducible). A request with no matching baseline name is skipped, records missing aname/durationare skipped rather than panicking, and an empty or malformed baseline file now exits with a clean error instead of silently reporting success.
Fixed
- A failed
assertno longer aborts the run with a Rust panic and backtrace hint. Instead, driller prints a singleFAIL: <key> -- expected <x>, got <y>line, continues the run so any remaining checks still report, and finishes with a non-zero exit code so CI can detect the failure. A passing run still exits0. Strict-equality semantics are unchanged. --stats --reporttogether no longer printsNaNrequests-per-second and an all-zero stats block. Report mode now produces real timing data, and the requests-per-second divide is guarded against a zero-duration run (fcsonline/drill#87).--reportno longer silently writes an empty file when a run completes no requests (e.g. a plan with norequestitems, or a--durationshorter than a single request); it prints a warning and skips the write instead.{{ index }}now resolves in a plain request (one with nowith_items/with_items_range/with_items_from_csv/with_items_from_file). Previously it only existed inside those expansions, so a plain plan that referenced{{ index }}panicked in the default strict mode ("Unknown 'index' variable") or interpolated to an empty string under--relaxed-interpolations(fcsonline/drill#186). In a plain requestindexis the iteration counter; inside an items expansion it remains the item's position in the list.
Added
- Documented the built-in interpolation variables (
base,index,iteration,item) inSYNTAX.md.
0.11.0 - 2026-05-31
Changed
- Request latency now measures time-to-last-byte. driller reads the full
response body before stopping its timer, matching
wrk,k6,vegetaand other load-testing tools. Previously the timer stopped as soon as the response headers arrived, and the body was only read when a request usedassign, so endpoints serving non-trivial bodies (files, large JSON) were reported as far faster than they really were (fcsonline/drill#74). Reported latencies for body-heavy endpoints will increase to reflect true end-to-end time.- Because the body read is now part of the timed request, a response whose
body does not finish transferring within
--timeoutis reported as a connection error (synthetic status520/ theconntotal) rather than its HTTP status. Previously such a request reported its status (e.g.200) because the body was never read. Loosen--timeoutif body transfer for a slow or large-bodied endpoint legitimately needs more time. assignbodies are decoded using the response'sContent-Typecharset (defaulting to UTF-8), preserving the previous charset-aware behaviour now that driller drains the body itself instead of calling reqwest'stext().- The body is streamed and discarded chunk by chunk; it is only buffered in
memory when a request uses
assign. Peak memory therefore stays bounded per in-flight request rather than scaling with the full response size, so testing large-body endpoints at high concurrency does not balloon memory.
- Because the body read is now part of the timed request, a response whose
body does not finish transferring within
- In
--durationmode, iterations that complete before the deadline are now counted even when the deadline falls mid-batch; previously the entire in-flight batch was discarded when the duration elapsed. Only requests still in flight at the deadline are dropped. - In
--verbosemode, connection and body-read failures now also print the<<<response marker (with no body), so failed requests are visible in the request/response log instead of only the inline error line. cargo-denynow bansnative-tls,openssl, andopenssl-sys, so CI fails if OpenSSL is ever pulled back into the dependency tree. TLS stays on rustls; this guards against the prebuilt-musl OpenSSL segfault class (fcsonline/drill#168, #190).
0.10.3 - 2026-05-30
Added
--statsoutput now includes a per-status-code breakdown: each HTTP status mapped to its request count, followed by a2xx/3xx/4xx/5xxclass rollup. The synthetic status520is labelled as a connection error and reported as a separateconntotal (not folded into5xx), so dropped connections are distinguishable from server5xxresponses (e.g.example/benchmark.ymlnow shows its 202 "failures" as 200 expected 404s + 2 flaky 500s). With--verboseeach plan step also prints a compact per-step breakdown.
Changed
- Example server is now a small Rust (axum) binary at
example/server, serving theresponses/fixtures and a few dynamic endpoints; running the examples needs onlycargo. The previous Node/Express example server, itsnpmdependency tree, and its Docker files were removed. - CI: a new
examplesjob builds the example server and runs every standaloneexample/*.ymlplan against it, gating on a clean exit and no connection errors -- turning the example suite into a regression test.
Fixed
--versionno longer prints an empty commit hash in release binaries (e.g.driller 0.10.2 ()).build.rsnow requires a successful, non-emptygit rev-parseand otherwise falls back to$GITHUB_SHA(thenunknown), so CI-built binaries always embed a real commit identifier. ACross.tomlpassesGITHUB_SHAinto the musl container build for the same reason.- Release workflow: build the
x86_64-apple-darwintarget onmacos-latest(Apple-silicon, cross-compiling) instead of the frequently-unavailablemacos-13runner, which had left the Intel macOS asset missing from the 0.10.2 release. example/headers.yml: corrected the base URL port (3000->9000) so the custom-headers example reaches the example server instead of failing with connection-refused.example/benchmark.yml: fixed the CSVquote_char("\'"->"'", which had decoded to a backslash) so the CSV-driven POST step issues requests instead of silently parsing nothing; corrected the matchingquote_charexample inSYNTAX.md.
Documentation
README.md: document the--worker-threads/-wflag and linkdocs/cli-reference.mdfor the full flag list and the runtime workload-tuning guide.example/README.md: document running the examples against the Rust server withdriller run --benchmark … --stats.
0.10.2 - 2026-05-29
Added
driller run --worker-threads N(short-w N): selects the tokio runtime.N = 1(default) uses the current-thread runtime;N >= 2uses the multi-thread runtime withNworker threads.N = 0is rejected at CLI parse time. Seedocs/cli-reference.mdfor the workload-vs-N guidance table.
Changed
- Default tokio runtime is now explicitly
current_thread. This matches the behavior every previous release shipped with -- the prior derivationmin(num_cpus, concurrency)was paired withBuilder::new_current_thread(), which silently ignored the computed worker count. Behavior is therefore identical for users who do not pass--worker-threads; only the manifest is now honest.
Removed
num_cpusdependency. No longer needed now that the worker count is taken directly from the CLI flag.
Changed
actions::request: shrink the connection-poolMutexwindow to cover only theHashMaplookup and a cheapreqwest::Clientclone (the inner state isArc-shared). The per-requestRequestBuilderis now constructed after the lock is released. Originally pursued as a candidate fix for a multi-thread-runtime throughput regression at moderate response sizes; a clean-machine sweep of the patched binary did not show the regression closing, so this lands as a cleanup rather than a perf fix.Cargo.toml: declare tokio'srtandrt-multi-threadfeatures explicitly. The runtime builder requires both, and they were previously available only via reqwest's transitive feature enablement.
Added
Cargo.toml: aprofilingcargo profile that inherits fromreleaseand keeps debug symbols (debug = true, strip = false). Use withcargo build --profile profilingorcargo install --path . --profile profiling --forcefor samply / instruments stack walking. Defaultreleasebuild is unchanged.
0.10.1 - 2026-05-28
Fixed
--thresholdrejects non-numeric values at CLI parse time instead of after running the benchmark, with an error that hints at the bundled-short-flags gotcha (e.g.-statsis parsed as-s -t ats, not as--stats).- File-not-found and YAML/CSV parse errors in
reader.rsare now reported as cleanerror: ...lines on stderr with exit code 1, instead of Rust panics with backtrace hints. Affects--benchmark,--compare, and any benchmark step that reads aniterate/csvsource file.
Changed (release pipeline)
- Release workflow rewritten to use
taiki-e/upload-rust-binary-action, replacing the previous Docker-only action that failed on macOS and Windows runners. The 0.10.0 release shipped without binary assets as a result; 0.10.1 restores cross-platform artifacts forx86_64-unknown-linux-musl,x86_64-apple-darwin,aarch64-apple-darwin, andx86_64-pc-windows-msvc.
0.10.0 - 2026-05-28
Added
driller run <URL>subcommand for ad-hoc HTTP testing without a benchmark file- CLI override flags:
--concurrency,--iterations,--duration,--rampup,--base-url - Duration-based runs (
--duration 30s) that loop the plan for a fixed wall-clock period - Three-layer config precedence: hard-coded defaults < YAML file < CLI flags
docs/cli-reference.mdwith full CLI documentation
Changed
benchmark::execute()accepts aRunOptionsstruct instead of positional parametersTagsstruct owns its data (removed lifetime parameter)- Synthetic plan built programmatically via
Request::simple_getinstead of YAML construction - Duration loop bounded by
tokio::time::timeoutto prevent overshooting the deadline - Terminal output colors changed from purple to cyan
- Concurrency > iterations validation produces a clear error message instead of a panic
checker::compare()acceptsthresholdasf64(parsed at CLI boundary)- Positional URL split into base and path components for correct request targeting
- README quick-start section tightened; example updated to use
runsubcommand - Upgrade
reqwest0.12 to 0.13, bump MSRV to 1.95 - Upgrade
colored2 to 3,rand0.8 to 0.10 - Add
cargo-denyconfiguration for license and advisory auditing
Fixed
- Histogram panic on response durations above 3.6 seconds (upper bound raised to 1 hour)
- Duration mode no longer overshoots by a full batch latency
Added (infrastructure)
- SECURITY.md, CONTRIBUTING.md, issue templates, CODEOWNERS
- Cross-platform release workflow (Linux, macOS, Windows)
- Security audit and cargo-deny CI checks
0.10.0-alpha.2 - 2026-05-25
Changed
- Upgrade
clap2 to 4 (colored help output, better error messages, typed derive API) - Upgrade to Rust edition 2024, set MSRV to 1.85
- Update release workflow toolchain from 1.83.0 to 1.85.0
- Bump all dependencies via
cargo update
Fixed
- Clear all RUSTSEC vulnerabilities:
bytes(integer overflow),rustls-webpki(4 CVEs),time(stack exhaustion DoS),rand(unsound with custom logger) - Remove unmaintained
ansi_termandattytransitive dependencies (were pulled in by clap 2) - Fix clippy
unnecessary_unwraplint in request handling
0.10.0-alpha.1 - 2026-05-22
Friendly fork of fcsonline/drill 0.9.0. See FORK.md for rationale and migration instructions.
Changed
- Renamed crate and binary from
drilltodriller - Updated package metadata (repository, description, authors)
- Trimmed publish payload (exclude
.github/, example server)
Added
FORK.mdexplaining the fork's purpose and relationship to upstream- Local CI script (
local-ci.sh)
Unchanged
- License remains GPL-3.0-or-later
- Benchmark YAML format and CLI flags are fully compatible with drill 0.9.0
- Full upstream git history preserved