Skip to main content

zenkey_fleet/report/
trace.rs

1//! The RPC trace window (#215): one call, then everything observed on the
2//! called origin for a window after it — each sample tagged by how far the
3//! registry can *relate* it to the procedure, never by what caused it.
4//!
5//! RFC 05 §3's long-running idiom is a declared causal chain — `GET
6//! @rpc/<p>/artifact/request` → `state/<p>/artifact/<kind>` →
7//! `events/<p>/artifact/<ulid>` → `@blob` — and nothing followed it: an
8//! operator called a write procedure and then hunted three panes for what it
9//! did. This suite makes the call, so it owns the request instant; the
10//! grammar fixes the origin's position, so "this origin's keyspace" is one
11//! selector; the registry declares which subjects the producer owns. Those
12//! three facts are what a trace has. What it does **not** have is causality:
13//! the wording throughout is *observed after the call*, never *caused*, and
14//! there is no edge, no arrow, and no trace-id attachment — the ratified
15//! posture forecloses one.
16//!
17//! [`TraceReport::subscribed_before_call`] is always `true` and is pinned
18//! anyway: the order of operations — subscribe, then call, then hold — is
19//! normative (RFC 09 §5.1 O4). A window opened *after* the call converts
20//! "not asked" into "no", and a refactor that inverts the order must change
21//! the document to do it.
22
23use serde::Serialize;
24
25use crate::report::{CallReport, RowKind};
26
27/// What the two Δ columns are measured against, and what excluded the
28/// `@blob` bytes — spelled once each, so the report and the renderer cite
29/// the same sentence.
30pub const TRACE_CHAIN_RULE: &str =
31    "first-chunk naming heuristic (RFC 05 §3 idiom); a naming coincidence is tagged the same way";
32
33/// The planes a `**` window cannot reach (RFC 03 §4 D2), as the report
34/// states it. The artifact bytes on `@blob` are outside the trace by
35/// construction — excluded, not empty — and the window is deliberately not
36/// widened to reach them: the idiom's own step 4 is a pull, not a sample.
37pub const TRACE_EXCLUDED: &str = "the verbatim planes (`@rpc`, `@blob`, `@media`, `@adv`, \
38     `@catalog`): `**` never crosses an `@`-chunk (RFC 03 §4 D2), so the artifact bytes on \
39     `@blob` are outside this window — excluded, not empty";
40
41/// How a same-origin sample relates to the procedure that was called.
42///
43/// Three states, and the third is the one a `bool` cannot spell: with no
44/// registry loaded the chain is *unjudgeable*, which is not "undeclared"
45/// (RFC 09 §5.1 O4 — [`crate::Registration::Unknown`] kept distinct from
46/// [`crate::Registration::Unregistered`], one layer up).
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
48#[serde(rename_all = "snake_case")]
49pub enum TraceRelation {
50    /// The registry refines the subject under the called producer **and**
51    /// its first chunk is the procedure's first chunk (`artifact/request`
52    /// ↔ `state/<p>/artifact/<kind>`). A naming heuristic, stated as one in
53    /// [`TRACE_CHAIN_RULE`]: a coincidence of names is tagged the same way.
54    DeclaredChain,
55    /// Same origin; the registry does not put it in the chain — a different
56    /// producer, a different first chunk, or an unregistered subject.
57    SameOriginUndeclared,
58    /// Same origin, and no registry was loaded, so the chain could not be
59    /// judged either way.
60    SameOriginRegistryNotLoaded,
61}
62
63/// What the HLC Δ column is measured against.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
65#[serde(rename_all = "snake_case")]
66pub enum HlcReference {
67    /// The reply sample carried an HLC; every stamped effect's `hlc_delta_ms`
68    /// is measured from it.
69    Reply,
70    /// The reply carried none — a deployment's timestamping stamps
71    /// publications, not replies, and the caller's own session mints no HLC
72    /// (see [`crate::model::timeline`]) — so no `hlc_delta_ms` is computed
73    /// at all, and the column is absent rather than defaulted to arrival. A
74    /// responder that stamps its reply ([`crate::Responder::reply_stamped`])
75    /// is what makes the reference exist.
76    None,
77}
78
79/// One sample observed on the called origin after the call.
80#[derive(Debug, Clone, PartialEq, Serialize)]
81pub struct TraceRow {
82    pub key: String,
83    pub relation: TraceRelation,
84    /// Arrival on this observer's monotonic clock, ms since `t0` — the
85    /// instant after the watches were declared and before the GET left.
86    /// Always present.
87    pub arrival_delta_ms: f64,
88    /// The sample's HLC, `<ntp64>/<stamper>`, when it carried one.
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub hlc: Option<String>,
91    /// Sample HLC minus the reply's HLC, ms — present only when both exist.
92    /// Signed: a negative value is a sample stamped *before* the reply on
93    /// the stamper's clock, which is a fact to show, not to clamp.
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub hlc_delta_ms: Option<i64>,
96    /// Who stamped `hlc`: `self`, `foreign:<id>` or `unattributable:<id>`
97    /// (RFC 09 §5.1 O7). Present exactly when `hlc` is.
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub stamped_by: Option<String>,
100    pub kind: RowKind,
101    pub payload_bytes: usize,
102    /// Samples this observer missed while behind, between the previous row
103    /// of this lane and this one (O6). A break rides the row that follows
104    /// it in *every* lane, because the broadcast does not know whose
105    /// samples it lost.
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub break_before: Option<u64>,
108}
109
110/// Samples from **other** origins during the window: a count and a few
111/// keys, never rows. They are stated so that the attributed lane cannot be
112/// read as "the only thing that happened" — and they are attributed to
113/// nothing, because nothing in hand can.
114#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
115pub struct ConcurrentLane {
116    pub samples: u64,
117    /// Distinct keys among them.
118    pub keys: u64,
119    /// The first few keys, capped at [`crate::EXPANSION_CAP`].
120    pub examples: Vec<String>,
121    /// Samples the fleet-wide watch dropped while behind — its own counter,
122    /// so a busy fleet's lag never reads as a break in the origin's lanes.
123    pub dropped: u64,
124}
125
126/// The call, then what was observed after it.
127#[derive(Debug, Clone, Serialize)]
128pub struct TraceReport {
129    pub call: CallReport,
130    /// The selectors actually watched (O5): the origin's subtree, then the
131    /// fleet-wide window the concurrent lane is counted from.
132    pub scopes: Vec<String>,
133    /// [`TRACE_EXCLUDED`].
134    pub excluded: &'static str,
135    pub window_s: f64,
136    /// Always `true`; pinned so that inverting the order changes the document.
137    pub subscribed_before_call: bool,
138    /// `t0` on the wall clock, seconds since the Unix epoch — the one wall
139    /// reading in the report, so a script can place the window beside logs.
140    pub t0_unix_s: f64,
141    /// When the GET returned, ms after `t0`: the reply arrived at or before
142    /// this instant (the fan-in waits for the query to finalize, which is not
143    /// the reply's own arrival — that instant the query layer does not hand
144    /// out).
145    pub call_returned_ms: f64,
146    pub hlc_reference: HlcReference,
147    /// The reply's HLC, `<ntp64>/<stamper>`, when `hlc_reference` is `reply`.
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub reply_hlc: Option<String>,
150    /// [`TRACE_CHAIN_RULE`].
151    pub chain_rule: &'static str,
152    /// Whether a registry was loaded. `false` means every same-origin row is
153    /// [`TraceRelation::SameOriginRegistryNotLoaded`] — and, when the window
154    /// was empty, that the chain could not have been judged at all.
155    pub registry_loaded: bool,
156    /// The procedure's declared `kind` token — `long-running`, `write`,
157    /// `read` — or `undeclared` when no loaded slice declares it.
158    pub idiom: String,
159    /// Same origin, in the declared chain — in arrival order.
160    pub attributed: Vec<TraceRow>,
161    /// Same origin, not in the chain (or not judgeable) — in arrival order.
162    pub same_origin: Vec<TraceRow>,
163    pub concurrent: ConcurrentLane,
164    /// Samples the origin's watch dropped while behind (O6); each is also a
165    /// `break_before` on the next row of every lane.
166    pub dropped: u64,
167    /// Keys the origin watch's statistics table retired during the window.
168    pub keys_evicted: u64,
169}
170
171impl TraceReport {
172    /// The call's own exit code, unchanged: a trace is an act's observation,
173    /// not a judgement, and nothing seen in the window moves it
174    /// ([`CallReport::exit_code`]).
175    pub fn exit_code(&self) -> i32 {
176        self.call.exit_code()
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    /// The exit code is the call's: an empty window after a clean reply is
185    /// still 0, and a refused call is 1 however busy the origin was.
186    #[test]
187    fn a_trace_exits_as_its_call_does() {
188        let call = |answers| CallReport {
189            key: "k".into(),
190            timeout_s: 5.0,
191            answers,
192        };
193        let report = |call| TraceReport {
194            call,
195            scopes: vec![],
196            excluded: TRACE_EXCLUDED,
197            window_s: 10.0,
198            subscribed_before_call: true,
199            t0_unix_s: 0.0,
200            call_returned_ms: 0.0,
201            hlc_reference: HlcReference::None,
202            reply_hlc: None,
203            chain_rule: TRACE_CHAIN_RULE,
204            registry_loaded: false,
205            idiom: "undeclared".into(),
206            attributed: vec![],
207            same_origin: vec![],
208            concurrent: ConcurrentLane {
209                samples: 0,
210                keys: 0,
211                examples: vec![],
212                dropped: 0,
213            },
214            dropped: 0,
215            keys_evicted: 0,
216        };
217        assert_eq!(report(call(vec![])).exit_code(), 2);
218        assert_eq!(
219            report(call(vec![crate::report::CallAnswer {
220                origin: "h-1".into(),
221                outcome: crate::report::CallOutcome::Ok {
222                    value: None,
223                    text: None,
224                },
225                attachment: None,
226                attachment_bytes: None,
227            }]))
228            .exit_code(),
229            0
230        );
231    }
232
233    /// Every optional on a row is absent, never null: the arrival Δ is the
234    /// one column every sample has.
235    #[test]
236    fn an_unstamped_row_carries_only_its_arrival() {
237        let row = TraceRow {
238            key: "v1/h-3fa9c2d41b7e/telemetry/other/noise".into(),
239            relation: TraceRelation::SameOriginUndeclared,
240            arrival_delta_ms: 1.5,
241            hlc: None,
242            hlc_delta_ms: None,
243            stamped_by: None,
244            kind: RowKind::Put,
245            payload_bytes: 3,
246            break_before: None,
247        };
248        assert_eq!(
249            serde_json::to_value(&row).unwrap(),
250            serde_json::json!({
251                "key": "v1/h-3fa9c2d41b7e/telemetry/other/noise",
252                "relation": "same_origin_undeclared",
253                "arrival_delta_ms": 1.5,
254                "kind": "put",
255                "payload_bytes": 3
256            })
257        );
258    }
259}