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}