LogWisp: from a tail-and-grep habit to a log transport
LogWisp began as a way to stop doing the same thing every morning: SSH into
each server, tail -F a file, grep it, repeat on the next one. The platforms
that solve this are sized for fleets; my servers are a handful, their traffic
is low, and I wanted one static binary that streams, filters and forwards logs
and asks for nothing else. This is how that binary found its shape.
The first cut
The prototype watched a directory and pushed every new line to a browser as
Server-Sent Events. SSE won over WebSockets because it is plain HTTP: one
GET, a stream of events, and the browser reconnects on its own. A raw TCP
listener came next for machines on the same network, where TLS was somebody
else’s job.
It worked, and it was a dead end! A filter, a second destination, a cap on a noisy producer: each meant surgery in the one loop that did everything.
The pipeline
The rewrite made the pipeline the unit, and everything since has been a part that plugs into one:
- Sources
- Files through rotation, stdin, or another LogWisp node over TCP or HTTP.
- Flow
- A rate limit with a token bucket and a size cap, include and exclude patterns in order, then a formatter: raw, text or JSON.
- Sinks
- The console, rotating files, a live SSE or TCP stream, or the next node.
Every queue between them is bounded. A full one drops its own entries and counts them, so a slow reader costs only itself and never stalls a producer. Pipelines are independent, and a changed file rebuilds them in place; a candidate that fails validation leaves the running ones alone.
Transport
Chaining is what turned a tail into a transport. A chain sink hands the next
node the structured entry, time, level, message, fields and origin, not a
formatted line, so a relay can filter and reformat as if the entry were local
and every record keeps the node it came from. An edge tails its files and
dials an aggregator; the aggregator listens for its edges and serves one
stream or writes one archive. Two presets, edge and aggregator, build that
from a few keys; pipe, tail and serve cover the single-machine cases.
Security, once there was a network
TLS 1.2 and 1.3 everywhere, mutual TLS with identity-based authorization, and Argon2id for stored passwords. For links that stay plaintext there is Argon2id-SCRAM: a challenge-response login that doesn’t puts the password (or its hash) on the wire. It does not defeat an attacker in the middle; it keeps unauthorized clients from opening a connection at all, which is the threat on an internal network. Every listener can also refuse addresses before the handshake, cap a client’s connections, and read the real address from a load balancer’s PROXY header. Unknown configuration keys are refused rather than ignored.
Where it is now
lw composes its own configuration: lw --tui on a terminal, and the
Configure tab on this site, both running the same
engine, the second compiled to WebAssembly. The pipeline behind the
ViF lobby’s live log is the same binary on this machine.
The code and the documentation are on
GitHub.