Skip to main content

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}