Skip to main content

zenkey_fleet/
skeleton.rs

1//! The declared keyspace, built without subscribing to anything (issue #84).
2//!
3//! A Zenoh subscription cannot be "metadata only" — once declared, payloads
4//! flow. So a lazy explorer's tree starts from what costs (almost) nothing:
5//!
6//! - **registry slices** → the *declared* subject families per producer,
7//!   with `{var}` positions kept symbolic;
8//! - **the liveliness roster** → which origins run which producers
9//!   (zero-payload by construction, RFC 04 §5);
10//! - **admin-space declared entities** ([`crate::declared_entities`]) →
11//!   keyexprs sessions actually declared, when an admin space answers at all
12//!   (`adminspace.enabled` defaults to false — absence is "not available",
13//!   never "nothing declared").
14//!
15//! [`merge`] then folds the skeleton with the *observed* tree
16//! ([`crate::KeyTreeSnapshot`]) and the active watch set into one tree with a
17//! typed per-node [`NodeStatus`] — the acceptance criterion of RFC 09 §5.1
18//! O4/O5 applied to a tree: "declared, never seen" and "watched, quiet" are
19//! different facts and must be different *types*, not rendering conventions.
20
21use std::collections::BTreeMap;
22
23use zenoh::key_expr::keyexpr;
24
25use crate::registry::SliceSet;
26use crate::tree::{KeyTreeSnapshot, TreeNode};
27
28/// One skeleton chunk: concrete, or a declared variable kept symbolic.
29///
30/// The display form of a variable is `{name}` — `{` sorts after the
31/// alphanumerics, so variables naturally list after their literal siblings in
32/// a `BTreeMap<String, _>`.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum SkeletonChunk {
35    Literal(String),
36    /// `{var}` — exactly one chunk, member set unknown.
37    Var(String),
38    /// `{var...}` — the whole remaining tail (RFC 03 §1.4's rest-variable).
39    Rest(String),
40}
41
42impl SkeletonChunk {
43    fn parse(chunk: &str) -> SkeletonChunk {
44        if let Some(inner) = chunk.strip_prefix('{').and_then(|c| c.strip_suffix("...}")) {
45            SkeletonChunk::Rest(inner.to_string())
46        } else if let Some(inner) = chunk.strip_prefix('{').and_then(|c| c.strip_suffix('}')) {
47            SkeletonChunk::Var(inner.to_string())
48        } else {
49            SkeletonChunk::Literal(chunk.to_string())
50        }
51    }
52
53    /// The display key (and `BTreeMap` key) for this chunk.
54    pub fn display(&self) -> String {
55        match self {
56            SkeletonChunk::Literal(s) => s.clone(),
57            SkeletonChunk::Var(v) => format!("{{{v}}}"),
58            SkeletonChunk::Rest(v) => format!("{{{v}...}}"),
59        }
60    }
61
62    /// The selector chunk this position contributes when testing watch
63    /// coverage: a variable is any-one-chunk, a rest is any-tail.
64    fn selector_chunk(&self) -> &str {
65        match self {
66            SkeletonChunk::Literal(s) => s,
67            SkeletonChunk::Var(_) => "*",
68            SkeletonChunk::Rest(_) => "**",
69        }
70    }
71}
72
73/// Why we believe a node exists. At least one flag is set on every skeleton
74/// node (an all-false node would be a node nobody claimed).
75#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
76pub struct Evidence {
77    /// A registry-slice subject template covers it.
78    pub declared: bool,
79    /// The liveliness roster places this origin/producer here.
80    pub alive: bool,
81    /// An admin-space declared entity's keyexpr names it.
82    pub admin: bool,
83}
84
85impl Evidence {
86    fn merge(self, other: Evidence) -> Evidence {
87        Evidence {
88            declared: self.declared || other.declared,
89            alive: self.alive || other.alive,
90            admin: self.admin || other.admin,
91        }
92    }
93}
94
95/// The declaring registry entry behind a skeleton leaf.
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct DeclRef {
98    pub producer: String,
99    /// The subject pattern as declared (`disk/{mount}/used`).
100    pub path: String,
101    pub type_name: String,
102}
103
104#[derive(Debug, Clone, Default)]
105pub struct SkeletonNode {
106    pub chunk: Option<SkeletonChunk>,
107    pub children: BTreeMap<String, SkeletonNode>,
108    pub evidence: Evidence,
109    /// Set on template leaves.
110    pub decl: Option<DeclRef>,
111}
112
113impl SkeletonNode {
114    fn insert(&mut self, chunks: &[SkeletonChunk], evidence: Evidence, decl: Option<DeclRef>) {
115        self.evidence = self.evidence.merge(evidence);
116        let Some((first, rest)) = chunks.split_first() else {
117            if decl.is_some() {
118                self.decl = decl;
119            }
120            return;
121        };
122        let child = self
123            .children
124            .entry(first.display())
125            .or_insert_with(|| SkeletonNode {
126                chunk: Some(first.clone()),
127                ..SkeletonNode::default()
128            });
129        child.insert(rest, evidence, decl);
130    }
131}
132
133/// What fed the skeleton — the O5 coverage statement, typed.
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135pub struct SkeletonCoverage {
136    pub slices: usize,
137    pub roster_origins: usize,
138    /// `None` = the admin space was not asked or did not answer — which is
139    /// *not* an observed zero (RFC 09 §5.1 O4).
140    pub admin_entities: Option<usize>,
141}
142
143#[derive(Debug, Clone)]
144pub struct Skeleton {
145    pub root: SkeletonNode,
146    pub coverage: SkeletonCoverage,
147}
148
149impl Skeleton {
150    /// Build the declared keyspace. Pure — every input was gathered by the
151    /// caller, this only assembles.
152    ///
153    /// Expansion rules:
154    /// - a **host** slice × every roster origin running that producer →
155    ///   `<base>/v1/<origin>/<class>/<producer>/<pattern…>` (evidence:
156    ///   declared+alive);
157    /// - a host slice with **no** live origin → the same under a symbolic
158    ///   `{origin}` variable (declared, not placed — still visible, O4);
159    /// - a **service** slice → `<base>/v1/<origin>/<class>/<pattern…>` with
160    ///   no producer chunk (RFC 03 §1.5);
161    /// - admin entities with concrete (wildcard-free) keyexprs insert as
162    ///   literal paths; wildcard-bearing ones are not expanded into fake
163    ///   concrete nodes — they only contribute to the coverage count.
164    pub fn build(
165        base: &str,
166        slices: &SliceSet,
167        roster: &BTreeMap<String, Vec<String>>,
168        admin: Option<&crate::DeclaredEntities>,
169    ) -> Skeleton {
170        let mut root = SkeletonNode::default();
171        let base_chunks: Vec<SkeletonChunk> = if base.is_empty() {
172            Vec::new()
173        } else {
174            base.split('/')
175                .map(|c| SkeletonChunk::Literal(c.to_string()))
176                .collect()
177        };
178
179        for slice in slices.slices() {
180            for subject in &slice.subjects {
181                let decl = DeclRef {
182                    producer: slice.name.clone(),
183                    path: subject.path.clone(),
184                    type_name: subject.type_name.clone(),
185                };
186                let tail: Vec<SkeletonChunk> =
187                    subject.path.split('/').map(SkeletonChunk::parse).collect();
188
189                if let Some(origin) = &slice.service_origin {
190                    // Service origin: no producer chunk (RFC 03 §1.5).
191                    let mut path = base_chunks.clone();
192                    path.push(SkeletonChunk::Literal("v1".into()));
193                    path.push(SkeletonChunk::Literal(origin.clone()));
194                    path.push(SkeletonChunk::Literal(subject.class.clone()));
195                    path.extend(tail.clone());
196                    root.insert(
197                        &path,
198                        Evidence {
199                            declared: true,
200                            ..Evidence::default()
201                        },
202                        Some(decl.clone()),
203                    );
204                    continue;
205                }
206
207                // Host producer: place under every live origin running it…
208                let live: Vec<&String> = roster
209                    .iter()
210                    .filter(|(_, producers)| {
211                        producers
212                            .iter()
213                            .any(|p| p == &slice.name || instance_base(p) == slice.name)
214                    })
215                    .map(|(origin, _)| origin)
216                    .collect();
217                if live.is_empty() {
218                    // …or, unplaced, under a symbolic {origin}: declared but
219                    // not currently served anywhere we can see.
220                    let mut path = base_chunks.clone();
221                    path.push(SkeletonChunk::Literal("v1".into()));
222                    path.push(SkeletonChunk::Var("origin".into()));
223                    path.push(SkeletonChunk::Literal(subject.class.clone()));
224                    path.push(SkeletonChunk::Literal(slice.name.clone()));
225                    path.extend(tail.clone());
226                    root.insert(
227                        &path,
228                        Evidence {
229                            declared: true,
230                            ..Evidence::default()
231                        },
232                        Some(decl.clone()),
233                    );
234                } else {
235                    for origin in live {
236                        let mut path = base_chunks.clone();
237                        path.push(SkeletonChunk::Literal("v1".into()));
238                        path.push(SkeletonChunk::Literal(origin.clone()));
239                        path.push(SkeletonChunk::Literal(subject.class.clone()));
240                        path.push(SkeletonChunk::Literal(slice.name.clone()));
241                        path.extend(tail.clone());
242                        root.insert(
243                            &path,
244                            Evidence {
245                                declared: true,
246                                alive: true,
247                                ..Evidence::default()
248                            },
249                            Some(decl.clone()),
250                        );
251                    }
252                }
253            }
254        }
255
256        let mut admin_count = None;
257        if let Some(entities) = admin {
258            admin_count = Some(entities.entities.len());
259            for e in &entities.entities {
260                if e.keyexpr.contains('*') {
261                    continue; // never invent concrete nodes from wildcards
262                }
263                let path: Vec<SkeletonChunk> = e
264                    .keyexpr
265                    .split('/')
266                    .map(|c| SkeletonChunk::Literal(c.to_string()))
267                    .collect();
268                root.insert(
269                    &path,
270                    Evidence {
271                        admin: true,
272                        ..Evidence::default()
273                    },
274                    None,
275                );
276            }
277        }
278
279        Skeleton {
280            root,
281            coverage: SkeletonCoverage {
282                slices: slices.slices().len(),
283                roster_origins: roster.len(),
284                admin_entities: admin_count,
285            },
286        }
287    }
288}
289
290/// `snmp-2` → `snmp` (the instance suffix, RFC 03 §1.5).
291fn instance_base(producer: &str) -> &str {
292    match producer.rsplit_once('-') {
293        Some((name, suffix))
294            if !suffix.is_empty() && suffix.bytes().all(|b| b.is_ascii_digit()) =>
295        {
296            name
297        }
298        _ => producer,
299    }
300}
301
302/// The typed declared/observed state of one merged node.
303///
304/// Derived purely from (has traffic stats) × (a watch covers it):
305///
306/// |                | covered by a watch | not covered            |
307/// |----------------|--------------------|------------------------|
308/// | **has stats**  | `Observed`         | `Unwatched` (leftover) |
309/// | **no stats**   | `WatchedQuiet`     | `DeclaredOnly`         |
310///
311/// `Unwatched` is transient by construction —
312/// [`Monitor::unwatch`](crate::Monitor::unwatch) retires uncovered stats
313/// immediately — but the
314/// state exists so the interval between release and retirement never renders
315/// as live observation.
316#[derive(Debug, Clone, Copy, PartialEq, Eq)]
317pub enum NodeStatus {
318    /// Skeleton evidence only — never seen a payload here.
319    DeclaredOnly(Evidence),
320    /// A watch covers it; no traffic has arrived. "Quiet" is an observation.
321    WatchedQuiet(Evidence),
322    /// Traffic observed under an active watch.
323    Observed(Evidence),
324    /// Stats linger but no watch covers them any more.
325    Unwatched(Evidence),
326}
327
328impl NodeStatus {
329    pub fn evidence(self) -> Evidence {
330        match self {
331            NodeStatus::DeclaredOnly(e)
332            | NodeStatus::WatchedQuiet(e)
333            | NodeStatus::Observed(e)
334            | NodeStatus::Unwatched(e) => e,
335        }
336    }
337}
338
339/// Owned copy of a [`TreeNode`]'s numbers (the merged tree must not borrow
340/// the snapshot).
341#[derive(Debug, Clone, Copy, PartialEq)]
342pub struct NodeStats {
343    pub count: u64,
344    pub bytes: u64,
345    pub rate_hz: f64,
346    pub subtree_count: u64,
347    pub subtree_bytes: u64,
348    pub subtree_rate_hz: f64,
349    pub subtree_keys: usize,
350    /// Newest sample anywhere in the subtree — the per-node freshness signal
351    /// (issue #65; computed by the snapshot since the beginning, now carried
352    /// through the merge instead of dropped).
353    pub subtree_last_seen: Option<std::time::Instant>,
354}
355
356impl NodeStats {
357    fn from_tree(node: &TreeNode) -> NodeStats {
358        NodeStats {
359            count: node.count,
360            bytes: node.bytes,
361            rate_hz: node.rate_hz,
362            subtree_count: node.subtree_count,
363            subtree_bytes: node.subtree_bytes,
364            subtree_rate_hz: node.subtree_rate_hz,
365            subtree_keys: node.subtree_keys,
366            subtree_last_seen: node.subtree_last_seen,
367        }
368    }
369}
370
371/// One node of the merged declared∪observed tree.
372#[derive(Debug, Clone)]
373pub struct MergedNode {
374    pub children: BTreeMap<String, MergedNode>,
375    pub status: NodeStatus,
376    pub stats: Option<NodeStats>,
377    pub decl: Option<DeclRef>,
378}
379
380/// Fold the skeleton, the observed snapshot, and the active watch set into
381/// one tree. Runs at tick cadence — the same order of work as a flatten.
382pub fn merge(skeleton: &Skeleton, observed: &KeyTreeSnapshot, watched: &[String]) -> MergedNode {
383    // Borrowed, not owned: `keyexpr::new(&str)` validates without allocating,
384    // where `KeyExpr::new(String)` builds an `OwnedKeyExpr` (an `Arc<str>`
385    // copy) per selector per tick (`docs/zero-copy.md`).
386    let watched: Vec<&keyexpr> = watched
387        .iter()
388        .filter_map(|w| keyexpr::new(w.as_str()).ok())
389        .collect();
390    // One reusable buffer for the descent's prefixes, instead of a fresh
391    // `String` per node per tick.
392    let mut path = String::new();
393    merge_nodes(
394        Some(&skeleton.root),
395        Some(&observed.root),
396        &watched,
397        &mut path,
398    )
399}
400
401fn merge_nodes(
402    skel: Option<&SkeletonNode>,
403    obs: Option<&TreeNode>,
404    watched: &[&keyexpr],
405    path: &mut String,
406) -> MergedNode {
407    let evidence = skel.map(|s| s.evidence).unwrap_or_default();
408    let stats = obs.map(NodeStats::from_tree);
409    let covered = is_covered(path, watched);
410    let status = match (stats.is_some(), covered) {
411        (true, true) => NodeStatus::Observed(evidence),
412        (true, false) => NodeStatus::Unwatched(evidence),
413        (false, true) => NodeStatus::WatchedQuiet(evidence),
414        (false, false) => NodeStatus::DeclaredOnly(evidence),
415    };
416
417    let mut names: Vec<&String> = Vec::new();
418    if let Some(s) = skel {
419        names.extend(s.children.keys());
420    }
421    if let Some(o) = obs {
422        names.extend(o.children.keys());
423    }
424    names.sort();
425    names.dedup();
426
427    let mut children = BTreeMap::new();
428    for name in names {
429        let skel_child = skel.and_then(|s| s.children.get(name));
430        let obs_child = obs.and_then(|o| o.children.get(name));
431        // Coverage tests run on *selector* form: symbolic chunks widen.
432        // `selector_chunk()` already returns `&str`, so nothing is owned here.
433        let sel_chunk = skel_child
434            .and_then(|c| c.chunk.as_ref())
435            .map(|c| c.selector_chunk())
436            .unwrap_or(name.as_str());
437        let mark = path.len();
438        if !path.is_empty() {
439            path.push('/');
440        }
441        path.push_str(sel_chunk);
442        let child = merge_nodes(skel_child, obs_child, watched, path);
443        path.truncate(mark);
444        children.insert(name.clone(), child);
445    }
446
447    MergedNode {
448        children,
449        status,
450        stats,
451        decl: skel.and_then(|s| s.decl.clone()),
452    }
453}
454
455/// Does any active watch reach into this subtree?
456///
457/// Deliberately generous: a node counts as covered when a watch *intersects*
458/// its subtree (`prefix/**`), so an ancestor of a watched subtree reads
459/// "watched" rather than "declared only" — the honest reading of "some watch
460/// reaches below here". The root is covered iff anything is watched.
461fn is_covered(prefix: &str, watched: &[&keyexpr]) -> bool {
462    if watched.is_empty() {
463        return false;
464    }
465    if prefix.is_empty() {
466        return true;
467    }
468    // A thread-local scratch buffer: this runs once per node per tick, and a
469    // fresh `String` plus an `OwnedKeyExpr` each time was the heaviest thing
470    // on the render path (`docs/zero-copy.md`).
471    thread_local! {
472        static SUBTREE: std::cell::RefCell<String> = const { std::cell::RefCell::new(String::new()) };
473    }
474    SUBTREE.with(|buf| {
475        let mut buf = buf.borrow_mut();
476        buf.clear();
477        buf.push_str(prefix);
478        buf.push_str("/**");
479        let Ok(node) = keyexpr::new(buf.as_str()) else {
480            return false;
481        };
482        watched.iter().any(|w| w.intersects(node))
483    })
484}
485
486#[cfg(test)]
487mod tests {
488    use super::*;
489    use crate::stats::StatsTable;
490    use std::time::Instant;
491    use zenkey::slice::{RegistrySlice, SubjectDecl};
492
493    fn subject(path: &str, class: &str) -> SubjectDecl {
494        SubjectDecl {
495            path: path.into(),
496            class: class.into(),
497            type_name: "T".into(),
498            common: None,
499            since: None,
500            description: None,
501            qos: None,
502            ttl_s: None,
503            unit: None,
504            rate: None,
505            cardinality: None,
506            encoding: None,
507        }
508    }
509
510    fn host_slice(name: &str, subjects: Vec<SubjectDecl>) -> RegistrySlice {
511        RegistrySlice {
512            version: "1.0".into(),
513            app: "test".into(),
514            convention: 1,
515            name: name.into(),
516            service_origin: None,
517            description: None,
518            subjects,
519            procedures: vec![],
520            blob: vec![],
521            media: vec![],
522            deprecated: vec![],
523        }
524    }
525
526    #[test]
527    fn skeleton_places_declared_subjects_under_live_origins() {
528        let slices = SliceSet::from_slices(vec![host_slice(
529            "sysinfo",
530            vec![subject("disk/{mount}/used", "telemetry")],
531        )]);
532        let mut roster = BTreeMap::new();
533        roster.insert("h-3fa9c2d41b7e".to_string(), vec!["sysinfo".to_string()]);
534        let skel = Skeleton::build("", &slices, &roster, None);
535
536        let node = &skel.root.children["v1"].children["h-3fa9c2d41b7e"].children["telemetry"]
537            .children["sysinfo"]
538            .children["disk"]
539            .children["{mount}"]
540            .children["used"];
541        assert!(node.evidence.declared && node.evidence.alive);
542        assert_eq!(node.decl.as_ref().unwrap().type_name, "T");
543        assert_eq!(
544            skel.coverage.admin_entities, None,
545            "admin not asked is None, not zero (O4)"
546        );
547    }
548
549    /// A declared producer with no live origin is still visible — under a
550    /// symbolic {origin}, never invented as a concrete host.
551    #[test]
552    fn skeleton_keeps_unplaced_producers_symbolic() {
553        let slices = SliceSet::from_slices(vec![host_slice(
554            "sysinfo",
555            vec![subject("health", "state")],
556        )]);
557        let skel = Skeleton::build("", &slices, &BTreeMap::new(), None);
558        let origin = &skel.root.children["v1"].children["{origin}"];
559        assert!(origin.evidence.declared && !origin.evidence.alive);
560        assert!(
561            origin.children["state"].children["sysinfo"].children["health"]
562                .evidence
563                .declared
564        );
565    }
566
567    /// A service slice omits the producer chunk (RFC 03 §1.5).
568    #[test]
569    fn skeleton_places_service_origins_without_a_producer_chunk() {
570        let mut slice = host_slice("catalog", vec![subject("entity/{id}", "state")]);
571        slice.service_origin = Some("@catalog".into());
572        let slices = SliceSet::from_slices(vec![slice]);
573        let skel = Skeleton::build("", &slices, &BTreeMap::new(), None);
574        let state = &skel.root.children["v1"].children["@catalog"].children["state"];
575        assert!(state.children["entity"].children["{id}"].evidence.declared);
576    }
577
578    /// Admin evidence inserts concrete keyexprs and never expands wildcards
579    /// into fake concrete nodes.
580    #[test]
581    fn skeleton_admin_evidence_is_concrete_only() {
582        let entities = crate::DeclaredEntities {
583            entities: vec![
584                crate::DeclaredEntity {
585                    kind: crate::EntityKind::Publisher,
586                    keyexpr: "v1/h-aabbccddeeff/telemetry/x/m".into(),
587                    node_zid: "z".into(),
588                    sources: serde_json::Value::Null,
589                },
590                crate::DeclaredEntity {
591                    kind: crate::EntityKind::Subscriber,
592                    keyexpr: "v1/*/state/**".into(),
593                    node_zid: "z".into(),
594                    sources: serde_json::Value::Null,
595                },
596            ],
597        };
598        let skel = Skeleton::build("", &SliceSet::default(), &BTreeMap::new(), Some(&entities));
599        assert_eq!(skel.coverage.admin_entities, Some(2));
600        let concrete = &skel.root.children["v1"].children["h-aabbccddeeff"];
601        assert!(concrete.evidence.admin);
602        // The wildcard subscriber created no children under v1 beyond the
603        // concrete one.
604        assert_eq!(skel.root.children["v1"].children.len(), 1);
605    }
606
607    /// The four NodeStatus values are types, produced by the documented
608    /// (stats × coverage) table.
609    #[test]
610    fn merge_produces_all_four_statuses() {
611        // Skeleton declares two leaves under one live origin.
612        let slices = SliceSet::from_slices(vec![host_slice(
613            "sysinfo",
614            vec![subject("cpu", "telemetry"), subject("mem", "telemetry")],
615        )]);
616        let mut roster = BTreeMap::new();
617        roster.insert("h-3fa9c2d41b7e".to_string(), vec!["sysinfo".to_string()]);
618        let skel = Skeleton::build("", &slices, &roster, None);
619
620        // Observed traffic: cpu (watched) and a foreign key (not watched).
621        let mut stats = StatsTable::new();
622        let now = Instant::now();
623        stats.record(
624            "v1/h-3fa9c2d41b7e/telemetry/sysinfo/cpu",
625            4,
626            None,
627            now,
628            None,
629        );
630        stats.record("demo/foreign", 4, None, now, None);
631        let observed = KeyTreeSnapshot::build(&stats);
632
633        let watched = vec!["v1/h-3fa9c2d41b7e/telemetry/**".to_string()];
634        let merged = merge(&skel, &observed, &watched);
635
636        let sysinfo = &merged.children["v1"].children["h-3fa9c2d41b7e"].children["telemetry"]
637            .children["sysinfo"];
638        assert!(matches!(
639            sysinfo.children["cpu"].status,
640            NodeStatus::Observed(_)
641        ));
642        assert!(
643            matches!(sysinfo.children["mem"].status, NodeStatus::WatchedQuiet(_)),
644            "declared, covered, no traffic — quiet is an observation"
645        );
646        assert!(matches!(
647            merged.children["demo"].children["foreign"].status,
648            NodeStatus::Unwatched(_)
649        ));
650
651        // No watches at all: everything declared reads DeclaredOnly.
652        let merged = merge(&skel, &KeyTreeSnapshot::default(), &[]);
653        assert!(matches!(
654            merged.children["v1"].status,
655            NodeStatus::DeclaredOnly(_)
656        ));
657    }
658
659    #[test]
660    fn instance_suffixes_place_under_their_base_producer() {
661        let slices = SliceSet::from_slices(vec![host_slice(
662            "snmp",
663            vec![subject("if/{iface}/in", "telemetry")],
664        )]);
665        let mut roster = BTreeMap::new();
666        roster.insert("h-aabbccddeeff".to_string(), vec!["snmp-2".to_string()]);
667        let skel = Skeleton::build("", &slices, &roster, None);
668        assert!(
669            skel.root.children["v1"].children["h-aabbccddeeff"].children["telemetry"].children
670                ["snmp"]
671                .evidence
672                .alive
673        );
674    }
675}