1use crate::model::facts::{KeyDescription, KeyShape, Registration};
19use crate::model::timeline::{HlcStamp, TimelineRow};
20use crate::report::{Provenance, TraceRelation, TraceRow};
21use zenkey::slice::ProcedureDecl;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct TraceTarget {
27 pub origin: String,
29 pub producer: Option<String>,
32 pub chain_chunk: String,
35 pub registry_loaded: bool,
38}
39
40impl TraceTarget {
41 pub fn relation_of(&self, desc: &KeyDescription) -> Option<TraceRelation> {
45 let KeyShape::V1(facts) = &desc.facts.shape else {
46 return None;
47 };
48 if facts.origin != self.origin {
49 return None;
50 }
51 if !self.registry_loaded {
52 return Some(TraceRelation::SameOriginRegistryNotLoaded);
53 }
54 let in_chain = matches!(desc.facts.registration, Registration::Registered(_))
55 && facts.producer == self.producer
56 && facts.subject.first().map(String::as_str) == Some(self.chain_chunk.as_str());
57 Some(if in_chain {
58 TraceRelation::DeclaredChain
59 } else {
60 TraceRelation::SameOriginUndeclared
61 })
62 }
63}
64
65pub fn idiom_of(decl: Option<&ProcedureDecl>) -> String {
73 decl.and_then(|d| d.kind.as_ref())
74 .map(|k| k.token().to_string())
75 .unwrap_or_else(|| "undeclared".to_string())
76}
77
78pub fn hlc_delta_ms(sample_ntp64: u64, reference_ntp64: u64) -> i64 {
81 let diff = i128::from(sample_ntp64) - i128::from(reference_ntp64);
82 i64::try_from(diff * 1000 / (1i128 << 32)).unwrap_or(if diff < 0 { i64::MIN } else { i64::MAX })
86}
87
88pub fn stamped_by(stamp: &HlcStamp) -> String {
91 match stamp.provenance {
92 Provenance::SelfStamped => "self".to_string(),
93 Provenance::Foreign => format!("foreign:{}", stamp.stamper),
94 Provenance::Unattributable => format!("unattributable:{}", stamp.stamper),
95 }
96}
97
98pub fn trace_row(
102 row: &TimelineRow,
103 relation: TraceRelation,
104 reply_ntp64: Option<u64>,
105 break_before: Option<u64>,
106) -> TraceRow {
107 TraceRow {
108 key: row.key.clone(),
109 relation,
110 arrival_delta_ms: row.t_us as f64 / 1_000.0,
111 hlc: row.hlc.as_ref().map(HlcStamp::to_wire),
112 hlc_delta_ms: match (&row.hlc, reply_ntp64) {
113 (Some(h), Some(r)) => Some(hlc_delta_ms(h.ntp64, r)),
114 _ => None,
115 },
116 stamped_by: row.hlc.as_ref().map(stamped_by),
117 kind: row.kind,
118 payload_bytes: row.payload_bytes,
119 break_before,
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use crate::model::facts::describe_key;
127 use crate::model::registry::SliceSet;
128 use crate::report::{LaneId, RowKind};
129 use zenkey::slice::{RegistrySlice, SubjectDecl};
130
131 const ORIGIN: &str = "h-3fa9c2d41b7e";
132
133 fn slices() -> SliceSet {
134 let mut slice = RegistrySlice::new("1.0", "t", "demo");
135 slice.subjects = vec![
136 SubjectDecl::new("artifact/{kind}", zenkey::Class::State),
137 SubjectDecl::new("artifact/{ulid}", zenkey::Class::Events),
138 ];
139 let mut proc_decl = ProcedureDecl::new("artifact/request");
140 proc_decl.kind = Some(zenkey::Declared::Other("long-running".into()));
141 slice.procedures = vec![proc_decl];
142 let mut other = RegistrySlice::new("1.0", "t", "other");
143 other.subjects = vec![SubjectDecl::new("artifact/copy", zenkey::Class::State)];
144 SliceSet::from_slices(vec![slice, other])
145 }
146
147 fn target(registry_loaded: bool) -> TraceTarget {
148 TraceTarget {
149 origin: ORIGIN.into(),
150 producer: Some("demo".into()),
151 chain_chunk: "artifact".into(),
152 registry_loaded,
153 }
154 }
155
156 #[test]
159 fn the_chain_is_the_registered_subject_sharing_the_first_chunk() {
160 let s = slices();
161 let rel = |key: &str| target(true).relation_of(&describe_key("", key, Some(&s)));
162 assert_eq!(
163 rel(&format!("v1/{ORIGIN}/state/demo/artifact/pcap")),
164 Some(TraceRelation::DeclaredChain)
165 );
166 assert_eq!(
167 rel(&format!("v1/{ORIGIN}/events/demo/artifact/01H")),
168 Some(TraceRelation::DeclaredChain)
169 );
170 assert_eq!(
173 rel(&format!("v1/{ORIGIN}/state/other/artifact/copy")),
174 Some(TraceRelation::SameOriginUndeclared)
175 );
176 assert_eq!(
178 rel(&format!("v1/{ORIGIN}/telemetry/demo/noise")),
179 Some(TraceRelation::SameOriginUndeclared)
180 );
181 assert_eq!(rel("v1/h-bbbbbbbbbbbb/state/demo/artifact/pcap"), None);
183 assert_eq!(rel("plain/key"), None);
185 }
186
187 #[test]
190 fn without_a_registry_the_chain_is_unjudgeable_not_undeclared() {
191 let rel = |key: &str| target(false).relation_of(&describe_key("", key, None));
192 assert_eq!(
193 rel(&format!("v1/{ORIGIN}/state/demo/artifact/pcap")),
194 Some(TraceRelation::SameOriginRegistryNotLoaded)
195 );
196 assert_eq!(rel("v1/h-bbbbbbbbbbbb/state/demo/artifact/pcap"), None);
197 }
198
199 #[test]
202 fn a_service_target_matches_keys_with_no_producer_chunk() {
203 let mut slice = RegistrySlice::new("1.0", "t", "catalog");
204 slice.service_origin = Some(zenkey::Declared::Known(
205 zenkey::origin::ServiceOrigin::new("@catalog").unwrap(),
206 ));
207 slice.subjects = vec![SubjectDecl::new("entity/{id}", zenkey::Class::State)];
208 let s = SliceSet::from_slices(vec![slice]);
209 let t = TraceTarget {
210 origin: "@catalog".into(),
211 producer: None,
212 chain_chunk: "entity".into(),
213 registry_loaded: true,
214 };
215 assert_eq!(
216 t.relation_of(&describe_key("", "v1/@catalog/state/entity/x", Some(&s))),
217 Some(TraceRelation::DeclaredChain)
218 );
219 }
220
221 #[test]
222 fn the_idiom_is_the_declared_token_or_undeclared() {
223 let s = slices();
224 let decl = s.get("demo").unwrap().procedures.first();
225 assert_eq!(idiom_of(decl), "long-running");
226 assert_eq!(idiom_of(None), "undeclared");
227 let mut write = ProcedureDecl::new("set");
228 write.kind = Some(zenkey::Declared::Known(zenkey::ProcedureKind::Write));
229 assert_eq!(idiom_of(Some(&write)), "write");
230 }
231
232 #[test]
235 fn hlc_delta_is_signed_milliseconds() {
236 let one_s = 1u64 << 32;
237 assert_eq!(hlc_delta_ms(one_s, 0), 1000);
238 assert_eq!(hlc_delta_ms(0, one_s), -1000);
239 let point_four_ms = one_s * 4 / 10_000;
241 assert_eq!(hlc_delta_ms(point_four_ms, 0), 0);
242 assert_eq!(hlc_delta_ms(0, point_four_ms), 0);
243 assert_eq!(hlc_delta_ms(one_s + one_s / 2, one_s), 500);
244 }
245
246 #[test]
249 fn a_trace_row_takes_its_clocks_from_the_timeline_row() {
250 let stamped = TimelineRow {
251 key: format!("v1/{ORIGIN}/state/demo/artifact/pcap"),
252 lane: LaneId::Origin {
253 origin: ORIGIN.into(),
254 producer: Some("demo".into()),
255 },
256 t_us: 12_345,
257 hlc: Some(HlcStamp {
258 ntp64: (1u64 << 32) * 2,
259 stamper: "33".into(),
260 provenance: Provenance::Foreign,
261 }),
262 source: None,
263 sn: None,
264 kind: RowKind::Put,
265 payload_bytes: 7,
266 };
267 let row = trace_row(
268 &stamped,
269 TraceRelation::DeclaredChain,
270 Some(1u64 << 32),
271 Some(3),
272 );
273 assert_eq!(row.arrival_delta_ms, 12.345);
274 assert_eq!(row.hlc.as_deref(), Some("8589934592/33"));
275 assert_eq!(row.hlc_delta_ms, Some(1000));
276 assert_eq!(row.stamped_by.as_deref(), Some("foreign:33"));
277 assert_eq!(row.break_before, Some(3));
278
279 let row = trace_row(&stamped, TraceRelation::DeclaredChain, None, None);
281 assert!(row.hlc.is_some());
282 assert_eq!(row.hlc_delta_ms, None);
283
284 let unstamped = TimelineRow {
285 hlc: None,
286 lane: LaneId::Unstamped,
287 ..stamped
288 };
289 let row = trace_row(
290 &unstamped,
291 TraceRelation::SameOriginUndeclared,
292 Some(1u64 << 32),
293 None,
294 );
295 assert_eq!(
296 (row.hlc, row.hlc_delta_ms, row.stamped_by),
297 (None, None, None)
298 );
299 }
300}