---
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
| `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`:
| `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`.