Skip to content

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:

jq -r '[.timestamp_utc, .name, (.attrs.outcome // "-")] | @tsv' telemetry.jsonl

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.