rtb-telemetry 0.7.4

Opt-in anonymous usage telemetry with pluggable sinks. Part of the phpboyscout Rust toolkit.
Documentation
---
title: Event
description: Event fields, the JSON emitted on the wire, timestamp format and severity mapping.
---

# `Event` and its JSON shape

An `Event` is one telemetry record. `TelemetryContext::record` builds one
per call; sinks serialise it.

## Fields

| Field | Type | Set by | Notes |
|---|---|---|---|
| `name` | `String` | caller | The event name, e.g. `command.invoke`, `tool.start`. |
| `tool` | `String` | context | The owning tool's name. |
| `tool_version` | `String` | context | The owning tool's version string. |
| `machine_id` | `String` | context | 64-char hex, salted SHA-256. Empty string on a `Disabled` context. |
| `timestamp_utc` | `String` | `Event::now` | RFC 3339 UTC, with sub-second precision. |
| `args` | `Option<String>` | caller | Raw command-line args. Redacted by out-of-process sinks. |
| `err_msg` | `Option<String>` | caller | Error or panic message. Redacted by out-of-process sinks. |
| `attrs` | `HashMap<String, String>` | caller | Free-form attributes. **Never** redacted. |

`Event` is `#[non_exhaustive]`, so it cannot be constructed with a struct
literal from outside the crate, and new fields can appear in a minor
release. Use the constructors.

## Constructors and setters

```rust
Event::now(name, tool, tool_version, machine_id)                      // stamps the current UTC time
Event::with_timestamp(name, tool, tool_version, machine_id, ts)       // caller supplies the timestamp
```

Both take `impl Into<String>` for every argument. `with_timestamp` expects
a string already formatted as RFC 3339 UTC — it does not parse or validate
what you pass, so a malformed timestamp reaches the sink unchanged. It is
the constructor tests use for reproducible output.

The fluent setters each return `Self`:

```rust
event.with_attr(key, value)   // inserts, overwriting an existing key
event.with_args(args)         // sets Some(args)
event.with_err_msg(msg)       // sets Some(msg)
event.redacted()              // clone with args and err_msg passed through rtb-redact
```

## The JSON a sink writes

`Event` derives `Serialize`; field names on the wire are the Rust field
names. `args` and `err_msg` are omitted entirely when `None`; `attrs` is
always present, as `{}` when empty. This is a real line written by
`FileSink`:

```json
{"name":"command.invoke","tool":"tutorial-check","tool_version":"0.1.0","machine_id":"0052e913c636c14e8ddd57119588170cb500a875cd971efe40d45c9d0b2f09a5","timestamp_utc":"2026-08-02T20:31:52.918459561Z","attrs":{"outcome":"ok","command":"greet"}}
```

Two consequences for anything consuming that output:

- **`attrs` key order is not stable.** It is a `HashMap`, and the order
  changes between runs of the same program. Do not diff raw lines or use
  them as a cache key without normalising first.
- **The timestamp carries nanoseconds.** `Event::now` formats
  `time::OffsetDateTime::now_utc()` as RFC 3339, which keeps the sub-second
  component: `2026-08-02T20:31:52.918459561Z`, not `2026-08-02T20:31:52Z`.
  A parser that only accepts whole seconds will reject it.

If the system clock cannot be formatted as RFC 3339 — which in practice
means a clock far outside the representable range — `Event::now` falls back
to `1970-01-01T00:00:00Z` rather than failing the call.

## Severity

`rtb_telemetry::event::severity_of(&event)` returns a `&'static str`:

| Condition | Value |
|---|---|
| `err_msg` is `Some(_)` | `"ERROR"` |
| `err_msg` is `None` | `"INFO"` |

That is the whole rule. Nothing else — not the event name, not an attribute
called `outcome` — affects severity, so an event recording a failure needs
`err_msg` set for it to be classified as one.

`HttpSink` adds the result as a top-level `severity` field alongside the
event fields in the POST body. `OtlpSink` maps `"ERROR"` to
`Severity::Error` and everything else to `Severity::Info` on the log
record. `FileSink` and `MemorySink` do not record severity at all; a
consumer of the JSONL derives it from the presence of `err_msg`.

`severity_of` is not re-exported at the crate root — import it from
`rtb_telemetry::event`.