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.
Consent model: opt-in at two levels¶
- 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
TelemetryContextis built withCollectionPolicy::Enabled. The default isDisabled— no events, no machine-ID derivation, no sink calls. ADisabledcontext'srecord()returnsOk(())without even building anEvent.
Persisted consent (consent module)¶
The user's decision is persisted to <config_dir>/<tool>/consent.toml:
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
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).