ferro-lumberjack
Rust primitives for the Logstash Lumberjack v2 wire protocol — the framing protocol Filebeat, Heartbeat, and other Elastic Beats agents use to ship events to Logstash (and to other Beats-protocol-compatible ingestion endpoints).
Beta (
v0.1.0). Both client and server primitives are available. The public API is stable for the0.1.xseries — minor releases may deprecate but not remove APIs. See Status.
Part of the Ferro ecosystem. Extracted from production use in
ferro-beat (Filebeat-compatible log shipper) and
ferro-heartbeat (Heartbeat-compatible monitor).
What this crate provides
- Frame codec (
framemodule) — encoders for Window (W), JSON-payload (J), Compressed (C), and Ack (A) frames; a streaming decoder that consumes bytes incrementally and emits typed frames. Pure data, usable from any runtime. - Async client (
clientmodule, default featureclient) — builds a connection to a Logstash endpoint, sends batches as Lumberjack v2 windows, parses the ACK response, validates the acknowledged sequence number, and surfaces partial-ACK / sequence-mismatch errors. Supports load-balanced multi-host failover and a persistent monotonic sequence counter that handlesu32::MAXwrap-around correctly. - Async server (
servermodule, default featureserver) — binds a TCP listener, accepts inbound connections, and exposesServerConnection::read_windowfor pulling decoded windows of events. The caller controls when to ACK, allowing strict ack-after-fsync, partial ACKs, or fire-and-forget. Compressed windows are decoded transparently; legacyDframes are surfaced as "consumed slots" without a payload. - TLS (default feature
tls, both directions) —tokio-rustlsbased. Client side: custom CA bundles viarustls-pemfileorwebpki-rootsfallback, plus an explicitly opt-indangerous_disable_verificationmode. Server side: cert chain + private-key PEM loading viaServerTlsConfig.
What this crate does not provide (yet)
- Field-by-field
D(data) frames. Modern Beats use the JSONJframe exclusively; the legacyD(key-value) frame is not encoded here. Decoding is supported (skipped frames are surfaced asFrame::Unknown) so a server-side path can choose to handle them. - A runtime-agnostic API. This crate is Tokio-only on purpose;
if you need a different runtime, the
framecodec is runtime-free and you can drive your own I/O. - Built-in event de-duplication or persistence. The server surfaces decoded events; durability is the caller's concern.
Specification compliance
The protocol is most clearly described in the
elastic/go-lumber reference
implementation; there is no formal RFC. The frame layout we implement:
| Frame | Bytes | Meaning |
|---|---|---|
2 W <u32 count> |
6 | Window — number of J/D data frames the sender will transmit before expecting an ACK. |
2 J <u32 seq> <u32 len> <payload> |
10 + len | JSON-encoded event with monotonic sequence number. |
2 C <u32 len> <zlib bytes> |
6 + len | Compressed batch — payload is a zlib stream containing concatenated J/D frames. |
2 A <u32 seq> |
6 | ACK from receiver — seq is the highest sequence number successfully processed. |
Sequence numbers are u32 and wrap modulo 2^32. Compare with
wrapping subtraction (acked.wrapping_sub(expected) == 0) — see
Sequence::is_acked_by. This is the only correct way to handle
long-lived connections that send more than u32::MAX events.
Status
| Aspect | Status |
|---|---|
| API stability | beta (v0.1.x — semver applies) |
| Client | working, used in production by ferro-beat / ferro-heartbeat |
| Server | working, exercised by 6 client↔server end-to-end tests |
| TLS | rustls 0.23 + tokio-rustls 0.26; no openssl, both directions |
| MSRV | rustc 1.88 |
| Fuzz harness | parse_frame (decoder) — covered nightly |
| Coverage target | 80%+ line; current measured in CI |
| Async runtime | Tokio (no other runtime supported) |
Quick start (client)
use ClientBuilder;
# async
With TLS:
use ClientBuilder;
use TlsConfig;
# async
Quick start (server)
use Server;
# async
There is also a runnable echo server in examples/echo_server.rs:
Frame codec (no I/O — usable from any runtime)
use ;
let mut decoder = new;
decoder.feed;
decoder.feed;
decoder.feed;
while let Some = decoder.next_frame?
# Ok::
Used in production by
- FerroBeat — Filebeat-compatible
Rust log shipper. (Source crate; will switch to
ferro-lumberjackonce published.) - FerroHeartbeat — Heartbeat-compatible monitor. (Source crate; will switch once published.)
Triage policy
See the workspace CONTRIBUTING.md. In
short: security 48h, bugs (with a reproducer) 14 days best-effort,
features collected for the next minor.
Trademarks
Logstash® and Elastic® are registered trademarks of Elasticsearch B.V. This crate implements a wire protocol that is compatible with those products; it is not endorsed by, or affiliated with, Elastic.
License
Apache-2.0. See LICENSE.