Skip to main content

zenkey_fleet/report/
admin.rs

1//! The admin plane (RFC 09 §5.1): routers, storages, declared entities,
2//! state coverage and the mesh topology — everything read out of Zenoh's own
3//! `@/**` adminspace rather than off the keyspace.
4
5use serde::Serialize;
6
7#[derive(Debug, Clone, Serialize)]
8pub struct StorageList {
9    pub storages: Vec<crate::StorageInfo>,
10    pub coverage: Vec<crate::CoverageRow>,
11}
12
13/// The routers the admin space answered for (#236).
14///
15/// Same argument as [`ScoutReport`](super::scout::ScoutReport): `[]` cannot distinguish a peer-only mesh
16/// from an admin space that is disabled, and those are different facts about
17/// the deployment. The selector that was actually asked rides with the answer.
18#[derive(Debug, Clone, Serialize)]
19pub struct RouterList {
20    /// The admin selector put to the bus.
21    pub asked: String,
22    pub routers: Vec<crate::report::RouterInfo>,
23}
24
25/// A router (or peer) as the admin space reports it.
26#[derive(Debug, Clone, serde::Serialize)]
27pub struct RouterInfo {
28    pub zid: String,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub version: Option<String>,
31    #[serde(skip_serializing_if = "Vec::is_empty")]
32    pub locators: Vec<String>,
33    /// The full admin document, untrimmed — layouts vary by version.
34    pub raw: serde_json::Value,
35}
36
37/// One configured storage, as the admin space reports it.
38#[derive(Debug, Clone, serde::Serialize)]
39pub struct StorageInfo {
40    pub zid: String,
41    pub name: String,
42    /// The key expression the storage captures, when the layout exposes it.
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub key_expr: Option<String>,
45    /// The literal prefix stripped before the volume sees a key (RFC 09 §2 —
46    /// zenoh requires a wildcard-free prefix here). Absent when the layout
47    /// does not say, which is not the same as "none configured".
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub strip_prefix: Option<String>,
50    /// The backing volume's id — `memory` is volatile and loses late-joiner
51    /// seeds on a router restart, `fs`/`rocksdb` are the durable LWW stores
52    /// (RFC 09 §2). Spelled either as a bare string or as `{ id: "fs", … }`
53    /// depending on version; both are absorbed here.
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub volume: Option<String>,
56    /// The full admin document, untrimmed — layouts vary by version.
57    pub raw: serde_json::Value,
58}
59
60/// How a declared state family relates to the configured storages.
61///
62/// `rename_all` is not decoration: without it this enum inherited Rust's
63/// variant spelling and serialized `"Covered"` while every other vocabulary in
64/// the report surface — `TopicVerdict`, `DoctorSeverity`, `CutoverVerdict`,
65/// `ExpectVerdict` — was snake_case (#232). A consumer could not learn the
66/// file's conventions from one document and apply them to the next.
67#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
68#[serde(tag = "coverage", content = "storage", rename_all = "snake_case")]
69pub enum Coverage {
70    /// Some storage's key expression includes every key of the family.
71    Covered(String),
72    /// A storage overlaps the family but does not include all of it.
73    Partial(String),
74    /// No storage touches the family. For volatile (ttl'd) state this can be
75    /// legitimate — advanced-pub/sub cache seeding (RFC 04 §3.5); storage is
76    /// authoritative for durable data.
77    Uncovered,
78}
79
80#[derive(Debug, Clone, serde::Serialize)]
81pub struct CoverageRow {
82    pub producer: String,
83    pub path: String,
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub ttl_s: Option<i64>,
86    #[serde(flatten)]
87    pub coverage: Coverage,
88}
89
90/// What kind of declared entity an admin reply describes.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
92#[serde(rename_all = "snake_case")]
93pub enum EntityKind {
94    Subscriber,
95    Publisher,
96    Queryable,
97    Querier,
98    Token,
99}
100
101/// One declared entity, as the admin space reports it: the reply key is
102/// `@/<zid>/<whatami>/<kind>/<declared-keyexpr...>`, so the keyexpr every
103/// session declared is readable **without subscribing to any data** — the
104/// payload-free discovery leg of issue #84.
105#[derive(Debug, Clone, serde::Serialize)]
106pub struct DeclaredEntity {
107    pub kind: EntityKind,
108    /// The declared key expression, verbatim.
109    pub keyexpr: String,
110    /// The node whose admin space answered.
111    pub node_zid: String,
112    /// The raw payload (`Sources { routers, peers, clients }`-shaped in
113    /// zenoh 1.9) — kept as-is; layouts vary by version.
114    pub sources: serde_json::Value,
115}
116
117/// The declared-entity sweep result.
118#[derive(Debug, Clone, Default, serde::Serialize)]
119pub struct DeclaredEntities {
120    pub entities: Vec<DeclaredEntity>,
121}
122
123/// One link of the mesh, seen as undirected.
124#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
125pub struct MeshLink {
126    /// The lower zid of the pair — the ordering is arbitrary but stable, so a
127    /// renderer can group without re-sorting.
128    pub a: String,
129    pub b: String,
130    /// Both ends reported this link. Reciprocal reports are corroboration,
131    /// not duplication, and the distinction is worth keeping: a link only one
132    /// end mentions is weaker evidence than one both do.
133    pub corroborated: bool,
134    /// Endpoints as the **first** reporter described them. A second report's
135    /// links are not merged: the two ends name the same link from opposite
136    /// sides, and concatenating them would read as twice the links.
137    pub links: Vec<String>,
138}
139
140/// One liveliness origin attached to the session that declared its token —
141/// the #131 join, evidence-first: an attachment is made only from what the
142/// admin space actually said, never guessed (a guessed attachment would be
143/// the O4 failure on a picture).
144#[derive(Debug, Clone, serde::Serialize)]
145pub struct OriginAttachment {
146    /// The origin the token names (`h-…` or `@service`).
147    pub origin: String,
148    /// The declaring session's zid, when the token's admin `sources` names
149    /// exactly one. `None` = the sources were absent or ambiguous — the
150    /// origin is then only *reported by* the answering admin space, and a
151    /// renderer says so instead of drawing a line it cannot back.
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub session_zid: Option<String>,
154    /// The admin space that reported the token: the origin's own session in
155    /// a peer mesh serving its admin space, a router in a routed one.
156    pub reporter_zid: String,
157    /// The token key the evidence rode — the audit trail.
158    pub token_key: String,
159}
160
161/// One node of the mesh, as the topology join sees it (#118).
162#[derive(Debug, Clone, serde::Serialize)]
163pub struct TopologyNode {
164    pub zid: String,
165    /// `router` | `peer` | `client`, as the admin key (or a neighbour's
166    /// session list) spells it.
167    pub whatami: String,
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub version: Option<String>,
170    /// Locators as the node's own root doc declares them. Since zenoh
171    /// 1.10.0 the root doc filters loopback endpoints out of this list
172    /// (upstream eclipse-zenoh/zenoh#2671, the loopback scouting fix:
173    /// `get_locators()` → `get_locators_noloopback()`) — deliberate, so a
174    /// loopback-only node honestly declares `[]` here.
175    #[serde(skip_serializing_if = "Vec::is_empty")]
176    pub locators: Vec<String>,
177    /// Endpoints corroborated from session links when the root doc
178    /// declares no locators: addresses a live link actually used on this
179    /// node's side (#155). Evidence of reachability, **not** a
180    /// listen-endpoint claim — renderers label the provenance ("via
181    /// session link") rather than folding these into `locators`.
182    #[serde(skip_serializing_if = "Vec::is_empty")]
183    pub locators_via_links: Vec<String>,
184    /// `true` = this node's own admin space answered; `false` = only heard
185    /// of via a neighbour's session list — "heard of, not queryable",
186    /// rendered as such rather than omitted (the issue's honesty rule).
187    pub answered: bool,
188}
189
190/// One reported link. Kept per-reporter — a renderer that wants an
191/// undirected mesh dedups by unordered zid pair, and reciprocal reports
192/// are corroboration, not duplication.
193#[derive(Debug, Clone, serde::Serialize)]
194pub struct TopologyEdge {
195    /// The zid whose admin doc reported this session.
196    pub reporter: String,
197    /// The far end's zid.
198    pub peer: String,
199    /// The far end's whatami, as the reporter says it.
200    pub whatami: String,
201    /// The session's region, verbatim as the reporter's admin doc states
202    /// it (zenoh 1.10 session entries carry one, `"unknown"` included —
203    /// the regions rework that landed over 1.9 "Longwang"). Absent on
204    /// older fleets whose docs have no such field.
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub region: Option<String>,
207    /// Link endpoints, `src -> dst`, protocol included.
208    #[serde(skip_serializing_if = "Vec::is_empty")]
209    pub links: Vec<String>,
210}
211
212/// The mesh as the admin space answered it, joined with nothing invented
213/// (#118): what answered, what was only mentioned, and who we are.
214#[derive(Debug, Clone, serde::Serialize)]
215pub struct TopologyReport {
216    pub nodes: Vec<TopologyNode>,
217    pub edges: Vec<TopologyEdge>,
218    /// The selector the sweep asked.
219    pub asked: String,
220    /// Root docs that answered. Zero is "the admin space did not answer" —
221    /// a reading about reachability, never an empty mesh.
222    pub answered: usize,
223    /// This session's own zid — the "you are here" marker.
224    pub self_zid: String,
225}