Write events to a file¶
FileSink appends each event as one line of JSON. It needs no feature
flag and no network.
Wire it up¶
use std::sync::Arc;
use rtb_telemetry::{CollectionPolicy, FileSink, TelemetryContext};
let sink = Arc::new(FileSink::new(data_dir.join("mytool/telemetry.jsonl")));
let telemetry = TelemetryContext::builder()
.tool(env!("CARGO_PKG_NAME"))
.tool_version(env!("CARGO_PKG_VERSION"))
.salt(concat!(env!("CARGO_PKG_NAME"), ".telemetry.v1"))
.sink(sink)
.policy(policy)
.build();
The file is not created until the first event is recorded, and parent directories are created at that point. An existing file is appended to, never truncated.
Read what it wrote¶
One JSON object per line:
{"name":"tool.start","tool":"mytool","tool_version":"0.1.0","machine_id":"0052e913…","timestamp_utc":"2026-08-02T20:31:52.918046826Z","attrs":{}}
{"name":"command.invoke","tool":"mytool","tool_version":"0.1.0","machine_id":"0052e913…","timestamp_utc":"2026-08-02T20:31:52.918459561Z","attrs":{"outcome":"ok","command":"greet"}}
jq works on it directly:
Two things to watch when you parse it. Timestamps carry nanoseconds, so a
whole-seconds RFC 3339 parser will reject them. And args and err_msg
are absent rather than null when unset. The full shape is in
Event.
Confirm the redaction happened¶
Anything in args or err_msg is scrubbed before it reaches the disk:
let event = Event::with_timestamp("cmd", "mytool", "1.0.0", "abc", "2026-04-24T00:00:00Z")
.with_args("deploy --token ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
sink.emit(&event).await?;
// the line on disk contains "[redacted]", not the token
Attributes are not scrubbed. If you put a token in attrs, it lands on
disk verbatim — see
Where redaction applies.
Keep the file from growing forever¶
FileSink does not rotate, cap or prune anything. Options, roughly in
order of effort:
- Point it at a path your platform's log rotation already manages.
- Truncate or delete the file yourself at a point where you know nothing is mid-write — at startup, before the context is built, is the easy one.
- Wrap it in a custom sink that rotates on size.
Do not point two processes at one file¶
Within a process, concurrent writes are serialised and cannot interleave. Across processes that guarantee does not hold, and a pair of events over 4 KiB can splice into one malformed line. Give each process its own file and aggregate afterwards — the reasoning is in Why FileSink holds a lock.
Handle the errors¶
emit returns TelemetryError::Io when the directory cannot be created or
the file cannot be opened or written — a read-only home directory, a full
disk — and TelemetryError::Serde if the event cannot be serialised.
Failing the user's command because a telemetry line could not be written is
almost never right; log it and carry on.