Skip to content

rtb-telemetry

Opt-in anonymous usage telemetry for CLI tools. Ships TelemetryContext — the handle tool code records events through — plus the TelemetrySink trait, three always-available sinks (NoopSink, MemorySink, FileSink), and two remote sinks (HttpSink, OtlpSink) behind the remote-sinks Cargo feature.

Part of the phpboyscout Rust toolkit; extracted from — and battle-tested by — rust-tool-base.

  • Author compile-in. A tool enables telemetry support by depending on this crate. No dependency, no telemetry code in the binary.
  • User runtime-enable. Collection happens only when the TelemetryContext is built with CollectionPolicy::Enabled. The default is Disabled — no events, no machine-ID derivation, no sink calls. A Disabled context's record() returns Ok(()) without even building an Event.

The user's decision is persisted to <config_dir>/<tool>/consent.toml:

version = 1
state = "enabled"   # or "disabled" or "unset"
decided_at = "2026-05-08T12:34:56Z"
use rtb_telemetry::consent::{self, Consent};
use rtb_telemetry::CollectionPolicy;

// Read on startup. Missing file → Ok(None) → opt-in default.
let policy = match consent::read(&path)? {
    Some(c) => c.state.into(),             // ConsentState → CollectionPolicy
    None    => CollectionPolicy::Disabled,
};

// Write on `telemetry enable` / `disable`; wipe on `telemetry reset`.
consent::write(&path, &Consent::enabled_now())?;
consent::reset(&path)?;   // idempotent

Consent carries an explicit schema version (currently 1) so a future format change is non-breaking — read rejects unknown versions. Decisions are timestamped in RFC 3339 (UTC).

Machine identity

MachineId::derive(salt) returns sha256(salt || machine_uid) hex-encoded — the raw machine ID never leaves this crate. When the OS exposes no machine ID (sandboxed container, WASI), it falls back to a random UUID. Salt uniqueness per tool is the author's responsibility; the recommended pattern is

.salt(concat!(env!("CARGO_PKG_NAME"), ".telemetry.v1"))

Rotating .v1.v2 invalidates every previously-recorded machine identity — the intended reset flow.

Events

Each Event carries the event name (e.g. command.invoke), the tool's name + version, the salted machine ID, an RFC-3339 UTC timestamp, optional args / err_msg strings, and a caller-supplied HashMap<String, String> of attrs.

Sinks

Sink Feature Backing Use case
NoopSink always Disabled-policy default; no allocation, no I/O.
MemorySink always Vec<Event> in memory Test fixtures; .snapshot(), .len(), .is_empty().
FileSink always Newline-delimited JSON on disk Local audit trail; creates parent dirs; serialises concurrent writes so JSONL lines never interleave.
HttpSink remote-sinks reqwest JSON POST Ship events to an HTTPS collection endpoint (optional bearer token, insecure endpoints refused by default).
OtlpSink remote-sinks OTLP/gRPC (tonic) or OTLP/HTTP Export events as OpenTelemetry log records.

Custom sinks implement the async TelemetrySink trait (emit(&Event) + optional flush()).

Redaction wiring

Every built-in sink calls Event::redacted() before serialisation, which runs rtb-redact automatically over Event::args and Event::err_msg — URL userinfo, credential query parameters, provider key prefixes, and long opaque tokens are stripped before an event leaves the process.

Callers own attr redaction

Event::attrs values are not auto-redacted — anything in the map ships verbatim to the sink. Tool authors MUST NOT pass raw command-line arguments, home-directory paths, user-sourced error messages, secrets, or free-form user strings as attrs. Safe attrs: command name, enumerated outcome (ok/error/cancelled), duration bucket, framework-supplied version string. Route anything free-form through rtb_redact::string yourself, or put it in args / err_msg where redaction is automatic.

Usage

use rtb_telemetry::{CollectionPolicy, FileSink, TelemetryContext};
use std::sync::Arc;

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(CollectionPolicy::Enabled)
    .build();

telemetry.record("command.invoke").await?;

Full API reference: docs.rs/rtb-telemetry.

Design record

The authoritative contracts are the crate's specs, retained in the rust-tool-base spec series:

Related engineering-standards rules: §1.4 filesystem concurrency (the FileSink write-serialisation rule) and §4.6 (the safe-attribute set for telemetry events).