rtb-telemetry 0.7.2

Opt-in anonymous usage telemetry with pluggable sinks. Part of the phpboyscout Rust toolkit.
Documentation
---
title: rtb-telemetry
description: Opt-in anonymous usage telemetry with pluggable sinks, salted machine identity, and persisted consent.
---

# `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](https://rust.phpboyscout.uk);
extracted from — and battle-tested by —
[rust-tool-base](https://gitlab.com/phpboyscout/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
  `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`.

### Persisted consent (`consent` module)

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

```toml
version = 1
state = "enabled"   # or "disabled" or "unset"
decided_at = "2026-05-08T12:34:56Z"
```

```rust
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

```rust
.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`](https://redact.rust.phpboyscout.uk)
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.

!!! warning "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

```rust
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](https://docs.rs/rtb-telemetry).

## Design record

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

- [v0.1 — opt-in events, salted machine ID, three sinks]https://gitlab.com/phpboyscout/rust-tool-base/-/blob/main/docs/development/specs/2026-04-22-rtb-telemetry-v0.1.md
- [HTTP + OTLP sinks (`remote-sinks`)]https://gitlab.com/phpboyscout/rust-tool-base/-/blob/main/docs/development/specs/2026-04-24-rtb-telemetry-http-otlp-sinks.md

Related engineering-standards rules:
[§1.4 filesystem concurrency](https://gitlab.com/phpboyscout/rust-tool-base/-/blob/main/docs/development/engineering-standards.md)
(the FileSink write-serialisation rule) and §4.6 (the safe-attribute
set for telemetry events).