Skip to content

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

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:

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:

{"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.