rtb_telemetry/event.rs
1//! The [`Event`] emitted for each telemetry record.
2
3use std::collections::HashMap;
4
5use serde::Serialize;
6
7/// A single telemetry event. `#[non_exhaustive]` so new fields can be
8/// added in a minor bump without breaking downstream `match` arms.
9#[derive(Debug, Clone, Serialize)]
10#[non_exhaustive]
11pub struct Event {
12 /// The event name, e.g. `command.invoke`, `tool.start`.
13 pub name: String,
14 /// The owning tool's name.
15 pub tool: String,
16 /// The owning tool's version string.
17 pub tool_version: String,
18 /// Salted SHA-256 of the host's machine ID — hex-encoded.
19 pub machine_id: String,
20 /// RFC 3339 / ISO 8601 UTC timestamp.
21 pub timestamp_utc: String,
22 /// Raw command-line args, when the caller chose to record them.
23 /// Redacted automatically by out-of-process sinks via
24 /// [`rtb_redact::string`]; see [`Event::redacted`].
25 #[serde(skip_serializing_if = "Option::is_none")]
26 pub args: Option<String>,
27 /// Error / panic message, when the caller chose to record it.
28 /// Redacted automatically by out-of-process sinks via
29 /// [`rtb_redact::string`]; see [`Event::redacted`].
30 #[serde(skip_serializing_if = "Option::is_none")]
31 pub err_msg: Option<String>,
32 /// Freeform string attributes.
33 ///
34 /// # Privacy — callers own redaction for `attrs`
35 ///
36 /// [`Event::args`] and [`Event::err_msg`] flow through the
37 /// framework redactor before leaving the process (see
38 /// [`Event::redacted`], applied by every built-in
39 /// out-of-process sink). Values placed in `attrs` are **not**
40 /// auto-redacted: callers must either use stable enumerated
41 /// values or run [`rtb_redact::string`] themselves.
42 ///
43 /// Prefer stable enumerated values for `attrs`: the command
44 /// name, an outcome (`ok`/`error`/`cancelled`), a duration
45 /// bucket, a framework-supplied version. Free-form strings
46 /// belong in `args` or `err_msg` so they pick up the automatic
47 /// redaction.
48 pub attrs: HashMap<String, String>,
49}
50
51impl Event {
52 /// Construct a fresh event with a caller-supplied timestamp
53 /// already formatted as RFC 3339 UTC (see [`Event::now`] for
54 /// the auto-timestamp path).
55 #[must_use]
56 pub fn with_timestamp(
57 name: impl Into<String>,
58 tool: impl Into<String>,
59 tool_version: impl Into<String>,
60 machine_id: impl Into<String>,
61 timestamp_utc: impl Into<String>,
62 ) -> Self {
63 Self {
64 name: name.into(),
65 tool: tool.into(),
66 tool_version: tool_version.into(),
67 machine_id: machine_id.into(),
68 timestamp_utc: timestamp_utc.into(),
69 args: None,
70 err_msg: None,
71 attrs: HashMap::new(),
72 }
73 }
74
75 /// Construct an event stamped with the current UTC time.
76 #[must_use]
77 pub fn now(
78 name: impl Into<String>,
79 tool: impl Into<String>,
80 tool_version: impl Into<String>,
81 machine_id: impl Into<String>,
82 ) -> Self {
83 let ts = time::OffsetDateTime::now_utc()
84 .format(&time::format_description::well_known::Rfc3339)
85 .unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string());
86 Self::with_timestamp(name, tool, tool_version, machine_id, ts)
87 }
88
89 /// Fluent setter for a single attribute. Overwrites existing
90 /// entries with the same key.
91 #[must_use]
92 pub fn with_attr(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
93 self.attrs.insert(key.into(), value.into());
94 self
95 }
96
97 /// Attach raw command-line args. Redacted by outbound sinks via
98 /// [`Event::redacted`].
99 #[must_use]
100 pub fn with_args(mut self, args: impl Into<String>) -> Self {
101 self.args = Some(args.into());
102 self
103 }
104
105 /// Attach an error / panic message. Redacted by outbound sinks
106 /// via [`Event::redacted`].
107 #[must_use]
108 pub fn with_err_msg(mut self, msg: impl Into<String>) -> Self {
109 self.err_msg = Some(msg.into());
110 self
111 }
112
113 /// Return a clone with [`Event::args`] and [`Event::err_msg`]
114 /// passed through [`rtb_redact::string`]. Every built-in
115 /// out-of-process sink calls this before serialisation.
116 #[must_use]
117 pub fn redacted(&self) -> Self {
118 let mut clone = self.clone();
119 if let Some(raw) = &clone.args {
120 clone.args = Some(rtb_redact::string(raw).into_owned());
121 }
122 if let Some(raw) = &clone.err_msg {
123 clone.err_msg = Some(rtb_redact::string(raw).into_owned());
124 }
125 clone
126 }
127}
128
129/// Stringified severity discriminant.
130///
131/// `"ERROR"` when [`Event::err_msg`] is populated, `"INFO"`
132/// otherwise. Shared between `HttpSink` (JSON body) and
133/// `OtlpSink` (`OTel` `Severity` mapping, both behind the
134/// `remote-sinks` feature) so both sinks
135/// agree on what a given event is.
136#[must_use]
137pub const fn severity_of(event: &Event) -> &'static str {
138 if event.err_msg.is_some() {
139 "ERROR"
140 } else {
141 "INFO"
142 }
143}