Three transports, same payload.

Each line is identical regardless of sink. -o FILE and --data-socket SPEC can both run at once and broadcast every record to every active sink.

TransportConfigured byUse
File (append) -o /var/log/sloth.jsonl log forwarder pulls / tail -f pipelines
UNIX-domain --data-socket unix:/var/run/sloth.sock local consumer on the same host
TCP --data-socket tcp:HOST:PORT remote consumer over a trusted transport (Tailscale, private VPN, localhost only)

Backpressure. The socket writer is non-blocking. A slow client that fills its kernel send buffer loses lines for the duration of the stall (it does not get queued). A broken pipe closes the client fd; reconnect to resume.

Seven record types.

Every line is a JSON object with at least type and ts (Unix seconds). Fields are append-only; consumers ignore unknown type values and unknown keys gracefully.

json dns
{"type":"dns","ts":1700000000,
 "src":"192.168.1.5",
 "qname":"example.com",
 "qtype":"A",
 "answer":"93.184.216.34",
 "is_resp":1}
json tls
{"type":"tls","ts":1700000001,
 "src":"10.0.0.5","dst":"93.184.216.34",
 "host":"example.com",
 "ver":"TLS 1.3",
 "ja3":"deadbeefcafef00d…"}
json quic
{"type":"quic","ts":1700000002,
 "src":"10.0.0.5","dst":"1.1.1.1",
 "host":"cloudflare.com",
 "ver":"v1"}
json http
{"type":"http","ts":1700000003,
 "src":"10.0.0.5",
 "host":"example.com",
 "method":"GET",
 "path":"/index.html"}
json ntp
{"type":"ntp","ts":1700000004,
 "src":"10.0.0.1","dst":"192.168.1.5",
 "mode":"server",
 "version":4,"stratum":1,"ref":"GPS"}
json icmp
{"type":"icmp","ts":1700000005,
 "src":"192.168.1.5","dst":"8.8.8.8",
 "desc":"Echo Req",
 "ty":8,"code":0,"seq":42,"v6":0}
json alert
{"type":"alert","ts":1700000006,
 "title":"THREAT_DOMAIN",
 "detail":"192.168.1.5 queried …",
 "key":"threat-d:malware.testing.com",
 "sev":2,"ty":3,"count":1}

sev is the severity tier: 0 = LOW (yellow), 1 = WARN (orange), 2 = CRIT (red). The enum is stable. ts on alerts is the last_seen time of the dedup key, not the first observation.

Reference consumer.

examples/consumer/sloth-stream.py is the textbook consumer loop — connect, stream, parse, filter, reconnect. Stdlib only. Read it as a template for porting to Go, Node, or Swift's Network.framework; the structure ports directly.

sh Tail every record, ANSI colour
python3 examples/consumer/sloth-stream.py \
    unix:/tmp/sloth.sock
sh Only alerts, over Tailscale
python3 examples/consumer/sloth-stream.py \
    tcp:100.64.0.5:8765 --type alert
sh Raw JSON into jq
python3 examples/consumer/sloth-stream.py \
    unix:/tmp/sloth.sock --raw | jq .
sh Type tally every 5s
python3 examples/consumer/sloth-stream.py \
    unix:/tmp/sloth.sock --count

Reference SIEM forwarder.

examples/forwarder/sloth-forward.py reads from the data socket, batches, and pushes to a downstream SIEM. Three sinks ship; the sink interface is two members (.name, .send(batch)) so adding Loki, Datadog, or your in-house collector is ~30 lines.

sh Splunk HEC
python3 examples/forwarder/sloth-forward.py \
    unix:/tmp/sloth.sock \
    --sink hec \
    --hec-url       https://splunk.example.com:8088/services/collector \
    --hec-token-env SLOTH_HEC_TOKEN
sh Elasticsearch (daily rolled)
python3 examples/forwarder/sloth-forward.py \
    unix:/tmp/sloth.sock \
    --sink elastic \
    --es-url       https://elastic.example.com:9200 \
    --es-index     'sloth-events-%Y.%m.%d' \
    --es-api-key-env SLOTH_ES_API_KEY
sh RFC 5424 syslog (UDP)
python3 examples/forwarder/sloth-forward.py \
    unix:/tmp/sloth.sock \
    --sink syslog \
    --syslog-host siem.example.com \
    --syslog-port 514
sh Alerts-only feed, big batches
python3 examples/forwarder/sloth-forward.py \
    unix:/tmp/sloth.sock \
    --type alert \
    --batch-size 500 --batch-ms 500 \
    --sink hec --hec-url … --hec-token-env …
DELIVERY

Non-durable by design.

The data socket is non-durable, and the forwarder doesn't pretend otherwise. When the downstream sink fails after --max-retries, the batch is dropped and a count is logged to stderr.

If your deployment needs durability, also pass -o /var/log/sloth.jsonl to sloth and ship the file with a log-shipping agent (filebeat, vector, fluent-bit) on a separate path. That pipeline gives you durability; this one gives you low-latency triage.

The stats line (every --stats-interval seconds, default 30s) is the operator's ground truth: received ≥ forwarded + dropped. A non-zero dropped over a long window means the sink can't keep up.