Skip to main content

reachability_core/telemetry/processors/
interface_aware_logger.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use super::{InterfaceIdentifier, InterfaceTimeSeriesGrouping};
6
7use fuchsia_inspect::Node as InspectNode;
8use fuchsia_inspect_contrib::nodes::{DedupeLogNode, LruCacheNode};
9use fuchsia_inspect_derive::Unit;
10use std::collections::HashMap;
11use windowed_stats::experimental::inspect::{InspectSender, InspectedTimeMatrix, TimeMatrixClient};
12use windowed_stats::experimental::series::interpolation::ConstantSample;
13use windowed_stats::experimental::series::metadata::BitSetNode;
14use windowed_stats::experimental::series::statistic::Union;
15use windowed_stats::experimental::series::{SamplingProfile, TimeMatrix};
16
17use crate::fetch::{FetchError, fetch_result_short_name};
18use crate::ping::{PingError, ping_result_short_name};
19use crate::{FetchParameters, IpVersions, PingParameters};
20
21// Keep only the 50 most recent events.
22const INSPECT_LOG_WINDOW_SIZE: usize = 50;
23
24#[derive(PartialEq, Eq, Unit)]
25struct PingResult {
26    address: String,
27    interface_name: String,
28    result: String,
29}
30
31#[derive(PartialEq, Eq, Unit)]
32struct FetchResult {
33    host_and_path: String,
34    resolved_address: String,
35    interface_name: String,
36    result: String,
37}
38
39struct PerInterfaceEvents {
40    // Gateway and internet ping results are separated to allow for better deduping.
41    gateway_ping_results: IpVersions<DedupeLogNode<PingResult>>,
42    internet_ping_results: IpVersions<DedupeLogNode<PingResult>>,
43    fetch_results: IpVersions<DedupeLogNode<FetchResult>>,
44    _interface_node: fuchsia_inspect::Node,
45    _v4_node: fuchsia_inspect::Node,
46    _v6_node: fuchsia_inspect::Node,
47}
48
49impl PerInterfaceEvents {
50    pub fn new(parent_node: &fuchsia_inspect::Node, identifier: InterfaceIdentifier) -> Self {
51        let interface_node = parent_node.create_child(format!("{}", identifier));
52        let v4_node = interface_node.create_child("v4");
53        let v6_node = interface_node.create_child("v6");
54
55        Self {
56            gateway_ping_results: IpVersions {
57                ipv4: DedupeLogNode::new(
58                    v4_node.create_child("gateway_ping_results"),
59                    INSPECT_LOG_WINDOW_SIZE,
60                ),
61                ipv6: DedupeLogNode::new(
62                    v6_node.create_child("gateway_ping_results"),
63                    INSPECT_LOG_WINDOW_SIZE,
64                ),
65            },
66            internet_ping_results: IpVersions {
67                ipv4: DedupeLogNode::new(
68                    v4_node.create_child("internet_ping_results"),
69                    INSPECT_LOG_WINDOW_SIZE,
70                ),
71                ipv6: DedupeLogNode::new(
72                    v6_node.create_child("internet_ping_results"),
73                    INSPECT_LOG_WINDOW_SIZE,
74                ),
75            },
76            fetch_results: IpVersions {
77                ipv4: DedupeLogNode::new(
78                    v4_node.create_child("fetch_results"),
79                    INSPECT_LOG_WINDOW_SIZE,
80                ),
81                ipv6: DedupeLogNode::new(
82                    v6_node.create_child("fetch_results"),
83                    INSPECT_LOG_WINDOW_SIZE,
84                ),
85            },
86            _interface_node: interface_node,
87            _v4_node: v4_node,
88            _v6_node: v6_node,
89        }
90    }
91
92    /// Example of gateway ping result logged:
93    /// ```
94    /// gateway_ping_results:
95    ///   0:
96    ///     Created@time = 98528289843
97    ///     LastSeen@time = 487674877812
98    ///     count = 8
99    ///     log:
100    ///       address = 192.168.1.1:0
101    ///       interface_name = wlan
102    ///       result = Success
103    /// ```
104    pub fn log_gateway_ping_result(
105        &mut self,
106        ping_parameters: &PingParameters,
107        result: &Result<(), PingError>,
108    ) {
109        Self::log_ping_result(&mut self.gateway_ping_results, ping_parameters, result);
110    }
111
112    /// Example of internet ping result logged:
113    /// ```
114    /// internet_ping_results:
115    ///   0:
116    ///     Created@time = 98528289843
117    ///     LastSeen@time = 487674877812
118    ///     count = 8
119    ///     log:
120    ///       address = 8.8.8.8:0
121    ///       interface_name = wlan
122    ///       result = Success
123    /// ```
124    pub fn log_internet_ping_result(
125        &mut self,
126        ping_parameters: &PingParameters,
127        result: &Result<(), PingError>,
128    ) {
129        Self::log_ping_result(&mut self.internet_ping_results, ping_parameters, result);
130    }
131
132    fn log_ping_result(
133        ping_results: &mut IpVersions<DedupeLogNode<PingResult>>,
134        ping_parameters: &PingParameters,
135        result: &Result<(), PingError>,
136    ) {
137        let results_node = match ping_parameters.addr.is_ipv4() {
138            true => &mut ping_results.ipv4,
139            false => &mut ping_results.ipv6,
140        };
141        let ping_result = PingResult {
142            address: format!("{}", ping_parameters.addr),
143            interface_name: ping_parameters.interface_name.clone(),
144            result: ping_result_short_name(result),
145        };
146        results_node.insert(ping_result);
147    }
148
149    /// Example of fetch result logged:
150    /// ```
151    /// fetch_results:
152    ///   0:
153    ///     Created@time = 128089098645
154    ///     LastSeen@time = 487719914947
155    ///     count = 7
156    ///     log:
157    ///       host_and_path = www.gstatic.com/generate_204
158    ///       resolved_address = [IP_ADDRESS]
159    ///       interface_name = wlan
160    ///       result = Completed_204
161    /// ```
162    pub fn log_fetch_result(
163        &mut self,
164        fetch_parameters: &FetchParameters,
165        result: &Result<u16, FetchError>,
166    ) {
167        let results_node = match fetch_parameters.ip.is_ipv4() {
168            true => &mut self.fetch_results.ipv4,
169            false => &mut self.fetch_results.ipv6,
170        };
171        let fetch_result = FetchResult {
172            host_and_path: format!("{}{}", fetch_parameters.domain, fetch_parameters.path),
173            resolved_address: format!("{}", fetch_parameters.ip),
174            interface_name: fetch_parameters.interface_name.clone(),
175            result: fetch_result_short_name(result),
176        };
177        results_node.insert(fetch_result);
178    }
179}
180
181fn bitset_constant_sample_time_matrix(
182    client: &TimeMatrixClient,
183    time_series_name: &str,
184    bitset_node: BitSetNode,
185) -> InspectedTimeMatrix<u64> {
186    client.inspect_time_matrix_with_metadata(
187        time_series_name,
188        TimeMatrix::<Union<u64>, ConstantSample>::new(
189            SamplingProfile::highly_granular(),
190            ConstantSample::default(),
191        ),
192        bitset_node,
193    )
194}
195
196struct PerInterfaceTimeSeries {
197    gateway_ping_result_time_matrix: IpVersions<InspectedTimeMatrix<u64>>,
198    internet_ping_result_time_matrix: IpVersions<InspectedTimeMatrix<u64>>,
199    fetch_result_time_matrix: IpVersions<InspectedTimeMatrix<u64>>,
200    _interface_node: InspectNode,
201    _v4_node: InspectNode,
202    _v6_node: InspectNode,
203}
204
205impl PerInterfaceTimeSeries {
206    pub fn new(
207        parent_node: &InspectNode,
208        inspect_metadata_path: &str,
209        identifier: InterfaceIdentifier,
210    ) -> Self {
211        let interface_node = parent_node.create_child(format!("{}", identifier));
212        let v4_node = interface_node.create_child("v4");
213        let v6_node = interface_node.create_child("v6");
214
215        let client_v4 = TimeMatrixClient::new(v4_node.clone_weak());
216        let client_v6 = TimeMatrixClient::new(v6_node.clone_weak());
217
218        let bitset_constant_sample_time_matrices = |name: &str, metadata_node_name: &str| {
219            let bitset_node =
220                BitSetNode::from_path(format!("{}/{}", inspect_metadata_path, metadata_node_name));
221            IpVersions {
222                ipv4: bitset_constant_sample_time_matrix(&client_v4, name, bitset_node.clone()),
223                ipv6: bitset_constant_sample_time_matrix(&client_v6, name, bitset_node),
224            }
225        };
226
227        Self {
228            gateway_ping_result_time_matrix: bitset_constant_sample_time_matrices(
229                "gateway_ping_results",
230                InspectMetadataNode::PING_RESULTS,
231            ),
232            internet_ping_result_time_matrix: bitset_constant_sample_time_matrices(
233                "internet_ping_results",
234                InspectMetadataNode::PING_RESULTS,
235            ),
236            fetch_result_time_matrix: bitset_constant_sample_time_matrices(
237                "fetch_results",
238                InspectMetadataNode::FETCH_RESULTS,
239            ),
240            _interface_node: interface_node,
241            _v4_node: v4_node,
242            _v6_node: v6_node,
243        }
244    }
245
246    fn log_gateway_ping_result(&self, ping_parameters: &PingParameters, result_bitmask: u64) {
247        if ping_parameters.addr.is_ipv4() {
248            self.gateway_ping_result_time_matrix.ipv4.fold_or_log_error(result_bitmask);
249        } else {
250            self.gateway_ping_result_time_matrix.ipv6.fold_or_log_error(result_bitmask);
251        }
252    }
253
254    fn log_internet_ping_result(&self, ping_parameters: &PingParameters, result_bitmask: u64) {
255        if ping_parameters.addr.is_ipv4() {
256            self.internet_ping_result_time_matrix.ipv4.fold_or_log_error(result_bitmask);
257        } else {
258            self.internet_ping_result_time_matrix.ipv6.fold_or_log_error(result_bitmask);
259        }
260    }
261
262    fn log_fetch_result(&self, fetch_parameters: &FetchParameters, result_bitmask: u64) {
263        if fetch_parameters.ip.is_ipv4() {
264            self.fetch_result_time_matrix.ipv4.fold_or_log_error(result_bitmask);
265        } else {
266            self.fetch_result_time_matrix.ipv6.fold_or_log_error(result_bitmask);
267        }
268    }
269}
270
271// The wrapper for time series reporting.
272pub struct InterfaceAwareLogger {
273    events: HashMap<InterfaceIdentifier, PerInterfaceEvents>,
274    // Tracks the provided `InterfaceIdentifier`s against the time series for
275    // that identifier. Entries are only created during initialization.
276    time_series_stats: HashMap<InterfaceIdentifier, PerInterfaceTimeSeries>,
277    inspect_metadata_node: InspectMetadataNode,
278    _events_node: InspectNode,
279    _time_series_node: InspectNode,
280}
281
282impl InterfaceAwareLogger {
283    pub fn new(
284        inspect_metadata_node: &InspectNode,
285        inspect_metadata_path: &str,
286        interface_grouping: InterfaceTimeSeriesGrouping,
287        events_node: InspectNode,
288        time_series_node: InspectNode,
289    ) -> Self {
290        let (events, time_series_stats) = match interface_grouping {
291            InterfaceTimeSeriesGrouping::Type(tys) => {
292                let events = tys
293                    .iter()
294                    .map(|ty| {
295                        let identifier = InterfaceIdentifier::Type(*ty);
296                        (identifier.clone(), PerInterfaceEvents::new(&events_node, identifier))
297                    })
298                    .collect();
299                let time_series_stats = tys
300                    .into_iter()
301                    .map(|ty| {
302                        let identifier = InterfaceIdentifier::Type(ty);
303                        (
304                            identifier.clone(),
305                            PerInterfaceTimeSeries::new(
306                                &time_series_node,
307                                inspect_metadata_path,
308                                identifier,
309                            ),
310                        )
311                    })
312                    .collect();
313                (events, time_series_stats)
314            }
315        };
316
317        Self {
318            events,
319            time_series_stats,
320            inspect_metadata_node: InspectMetadataNode::new(inspect_metadata_node),
321            _events_node: events_node,
322            _time_series_node: time_series_node,
323        }
324    }
325
326    pub fn log_gateway_ping_result(
327        &mut self,
328        interface_identifiers: Vec<InterfaceIdentifier>,
329        ping_parameters: &PingParameters,
330        gateway_ping_result: &Result<(), PingError>,
331    ) {
332        self.log_ping_result(
333            interface_identifiers,
334            ping_parameters,
335            gateway_ping_result,
336            PerInterfaceTimeSeries::log_gateway_ping_result,
337            PerInterfaceEvents::log_gateway_ping_result,
338        );
339    }
340
341    pub fn log_internet_ping_result(
342        &mut self,
343        interface_identifiers: Vec<InterfaceIdentifier>,
344        ping_parameters: &PingParameters,
345        internet_ping_result: &Result<(), PingError>,
346    ) {
347        self.log_ping_result(
348            interface_identifiers,
349            ping_parameters,
350            internet_ping_result,
351            PerInterfaceTimeSeries::log_internet_ping_result,
352            PerInterfaceEvents::log_internet_ping_result,
353        );
354    }
355
356    fn log_ping_result(
357        &mut self,
358        interface_identifiers: Vec<InterfaceIdentifier>,
359        ping_parameters: &PingParameters,
360        ping_result: &Result<(), PingError>,
361        time_series_log_fn: fn(&PerInterfaceTimeSeries, &PingParameters, u64),
362        event_log_fn: fn(&mut PerInterfaceEvents, &PingParameters, &Result<(), PingError>),
363    ) {
364        let result = ping_result_short_name(ping_result);
365        let result_id = self.inspect_metadata_node.ping_result.insert(result);
366        interface_identifiers.iter().for_each(|identifier| {
367            if let Some(events) = self.events.get_mut(identifier) {
368                event_log_fn(events, ping_parameters, ping_result);
369            }
370
371            if let Some(time_series) = self.time_series_stats.get(identifier) {
372                time_series_log_fn(time_series, ping_parameters, 1 << result_id);
373            }
374        });
375    }
376
377    pub fn log_fetch_result(
378        &mut self,
379        interface_identifiers: Vec<InterfaceIdentifier>,
380        fetch_parameters: &FetchParameters,
381        fetch_result: &Result<u16, FetchError>,
382    ) {
383        let result = fetch_result_short_name(fetch_result);
384        let result_id = self.inspect_metadata_node.fetch_result.insert(result);
385        interface_identifiers.iter().for_each(|identifier| {
386            if let Some(events) = self.events.get_mut(identifier) {
387                events.log_fetch_result(fetch_parameters, fetch_result);
388            }
389
390            if let Some(time_series) = self.time_series_stats.get(identifier) {
391                time_series.log_fetch_result(fetch_parameters, 1 << result_id);
392            }
393        });
394    }
395}
396
397const PING_RESULT_METADATA_CACHE_SIZE: usize = 32;
398const FETCH_RESULT_METADATA_CACHE_SIZE: usize = 32;
399
400// Holds the inspect node children for the static metadata that correlates to
401// bits in each of the corresponding structs / enums.
402struct InspectMetadataNode {
403    ping_result: LruCacheNode<String>,
404    fetch_result: LruCacheNode<String>,
405}
406
407impl InspectMetadataNode {
408    const PING_RESULTS: &'static str = "ping_results";
409    const FETCH_RESULTS: &'static str = "fetch_results";
410
411    fn new(inspect_node: &InspectNode) -> Self {
412        let ping_result = LruCacheNode::new(
413            inspect_node.create_child(Self::PING_RESULTS),
414            PING_RESULT_METADATA_CACHE_SIZE,
415        );
416        let fetch_result = LruCacheNode::new(
417            inspect_node.create_child(Self::FETCH_RESULTS),
418            FETCH_RESULT_METADATA_CACHE_SIZE,
419        );
420
421        Self { ping_result, fetch_result }
422    }
423}
424
425#[cfg(test)]
426mod tests {
427    use super::*;
428    use diagnostics_assertions::{
429        AnyBytesProperty, AnyNumericProperty, AnyProperty, assert_data_tree,
430    };
431
432    use crate::telemetry::processors::InterfaceType;
433    use crate::telemetry::testing::setup_test;
434
435    const IPV4_ADDR: std::net::IpAddr = std::net::IpAddr::V4(std::net::Ipv4Addr::new(8, 8, 8, 8));
436    const IPV6_ADDR: std::net::IpAddr =
437        std::net::IpAddr::V6(std::net::Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0));
438
439    #[fuchsia::test]
440    fn test_log_time_series_metadata_to_inspect() {
441        let mut harness = setup_test();
442        let logger_node = harness.inspect_node.create_child("interfaces");
443        let events_node = logger_node.create_child("events");
444        let time_series_node = logger_node.create_child("time_series");
445
446        let _interface_aware_logger = InterfaceAwareLogger::new(
447            &harness.inspect_metadata_node,
448            &harness.inspect_metadata_path,
449            InterfaceTimeSeriesGrouping::Type(vec![InterfaceType::Ethernet]),
450            events_node,
451            time_series_node,
452        );
453
454        let tree = harness.get_inspect_data_tree();
455        assert_data_tree!(
456            @executor harness.exec,
457            tree,
458            root: contains {
459                test_stats: contains {
460                    metadata: {
461                        ping_results: contains {},
462                        fetch_results: contains {},
463                    },
464                    interfaces: contains {
465                        time_series: contains {
466                            TYPE_ethernet: {
467                                v4: {
468                                    gateway_ping_results: {
469                                        "type": "bitset",
470                                        "data": AnyBytesProperty,
471                                        metadata: {
472                                            index_node_path: "root/test_stats/metadata/ping_results",
473                                        }
474                                    },
475                                    internet_ping_results: {
476                                        "type": "bitset",
477                                        "data": AnyBytesProperty,
478                                        metadata: {
479                                            index_node_path: "root/test_stats/metadata/ping_results",
480                                        }
481                                    },
482                                    fetch_results: {
483                                        "type": "bitset",
484                                        "data": AnyBytesProperty,
485                                        metadata: {
486                                            index_node_path: "root/test_stats/metadata/fetch_results",
487                                        }
488                                    }
489                                },
490                                v6: {
491                                    gateway_ping_results: {
492                                        "type": "bitset",
493                                        "data": AnyBytesProperty,
494                                        metadata: {
495                                            index_node_path: "root/test_stats/metadata/ping_results",
496                                        }
497                                    },
498                                    internet_ping_results: {
499                                        "type": "bitset",
500                                        "data": AnyBytesProperty,
501                                        metadata: {
502                                            index_node_path: "root/test_stats/metadata/ping_results",
503                                        }
504                                    },
505                                    fetch_results: {
506                                        "type": "bitset",
507                                        "data": AnyBytesProperty,
508                                        metadata: {
509                                            index_node_path: "root/test_stats/metadata/fetch_results",
510                                        }
511                                    }
512                                }
513                            }
514                        }
515                    }
516                }
517            }
518        )
519    }
520
521    #[fuchsia::test]
522    fn test_log_gateway_ping_result() {
523        let mut harness = setup_test();
524        let logger_node = harness.inspect_node.create_child("interfaces");
525        let events_node = logger_node.create_child("events");
526        let time_series_node = logger_node.create_child("time_series");
527
528        let mut interface_aware_logger = InterfaceAwareLogger::new(
529            &harness.inspect_metadata_node,
530            &harness.inspect_metadata_path,
531            InterfaceTimeSeriesGrouping::Type(vec![InterfaceType::Ethernet]),
532            events_node,
533            time_series_node,
534        );
535
536        let ping_parameters = crate::PingParameters {
537            addr: std::net::SocketAddr::new(IPV4_ADDR.into(), 80),
538            interface_name: "eth0".to_string(),
539        };
540
541        interface_aware_logger.log_gateway_ping_result(
542            vec![InterfaceIdentifier::Type(InterfaceType::Ethernet)],
543            &ping_parameters,
544            &Ok(()),
545        );
546        interface_aware_logger.log_gateway_ping_result(
547            vec![InterfaceIdentifier::Type(InterfaceType::Ethernet)],
548            &ping_parameters,
549            &Err(PingError::NoReply),
550        );
551
552        let tree = harness.get_inspect_data_tree();
553        assert_data_tree!(
554            @executor harness.exec,
555            tree,
556            root: contains {
557                test_stats: contains {
558                    metadata: contains {
559                        ping_results: {
560                            "0": {
561                                "@time": AnyNumericProperty,
562                                data: "Success",
563                            },
564                            "1": {
565                                "@time": AnyNumericProperty,
566                                data: "e_NoReply",
567                            },
568                        },
569                    },
570                    interfaces: contains {
571                        time_series: contains {
572                            TYPE_ethernet: contains {
573                                v4: contains {
574                                    gateway_ping_results: {
575                                        "type": "bitset",
576                                        "data": AnyBytesProperty,
577                                        metadata: {
578                                            index_node_path: "root/test_stats/metadata/ping_results",
579                                        }
580                                    },
581                                }
582                            }
583                        }
584                    }
585                }
586            }
587        )
588    }
589
590    #[fuchsia::test]
591    fn test_log_internet_ping_result() {
592        let mut harness = setup_test();
593        let logger_node = harness.inspect_node.create_child("interfaces");
594        let events_node = logger_node.create_child("events");
595        let time_series_node = logger_node.create_child("time_series");
596
597        let mut interface_aware_logger = InterfaceAwareLogger::new(
598            &harness.inspect_metadata_node,
599            &harness.inspect_metadata_path,
600            InterfaceTimeSeriesGrouping::Type(vec![InterfaceType::Ethernet]),
601            events_node,
602            time_series_node,
603        );
604
605        let ping_parameters = crate::PingParameters {
606            addr: std::net::SocketAddr::new(IPV6_ADDR.into(), 80),
607            interface_name: "eth0".to_string(),
608        };
609
610        interface_aware_logger.log_internet_ping_result(
611            vec![InterfaceIdentifier::Type(InterfaceType::Ethernet)],
612            &ping_parameters,
613            &Ok(()),
614        );
615        interface_aware_logger.log_internet_ping_result(
616            vec![InterfaceIdentifier::Type(InterfaceType::Ethernet)],
617            &ping_parameters,
618            &Err(PingError::NoReply),
619        );
620
621        let tree = harness.get_inspect_data_tree();
622        assert_data_tree!(
623            @executor harness.exec,
624            tree,
625            root: contains {
626                test_stats: contains {
627                    metadata: contains {
628                        ping_results: {
629                            "0": {
630                                "@time": AnyNumericProperty,
631                                data: "Success",
632                            },
633                            "1": {
634                                "@time": AnyNumericProperty,
635                                data: "e_NoReply",
636                            },
637                        },
638                    },
639                    interfaces: contains {
640                        time_series: contains {
641                            TYPE_ethernet: contains {
642                                v6: contains {
643                                    internet_ping_results: {
644                                        "type": "bitset",
645                                        "data": AnyBytesProperty,
646                                        metadata: {
647                                            index_node_path: "root/test_stats/metadata/ping_results",
648                                        }
649                                    },
650                                }
651                            }
652                        }
653                    }
654                }
655            }
656        )
657    }
658
659    #[fuchsia::test]
660    fn test_log_fetch_result() {
661        let mut harness = setup_test();
662        let logger_node = harness.inspect_node.create_child("interfaces");
663        let events_node = logger_node.create_child("events");
664        let time_series_node = logger_node.create_child("time_series");
665
666        let mut interface_aware_logger = InterfaceAwareLogger::new(
667            &harness.inspect_metadata_node,
668            &harness.inspect_metadata_path,
669            InterfaceTimeSeriesGrouping::Type(vec![InterfaceType::Ethernet]),
670            events_node,
671            time_series_node,
672        );
673
674        let fetch_parameters = crate::FetchParameters {
675            interface_name: "eth0".to_string(),
676            domain: "example.com".to_string(),
677            ip: std::net::IpAddr::V4(std::net::Ipv4Addr::new(8, 8, 8, 8)),
678            path: "".to_string(),
679            expected_statuses: vec![204],
680        };
681
682        interface_aware_logger.log_fetch_result(
683            vec![InterfaceIdentifier::Type(InterfaceType::Ethernet)],
684            &fetch_parameters,
685            &Ok(204),
686        );
687        interface_aware_logger.log_fetch_result(
688            vec![InterfaceIdentifier::Type(InterfaceType::Ethernet)],
689            &fetch_parameters,
690            &Err(FetchError::ReadTcpStreamTimeout),
691        );
692
693        let tree = harness.get_inspect_data_tree();
694        assert_data_tree!(
695            @executor harness.exec,
696            tree,
697            root: contains {
698                test_stats: contains {
699                    metadata: contains {
700                        fetch_results: {
701                            "0": {
702                                "@time": AnyNumericProperty,
703                                data: "Completed_204",
704                            },
705                            "1": {
706                                "@time": AnyNumericProperty,
707                                data: "e_ReadTcpTimeout",
708                            },
709                        },
710                    },
711                    interfaces: contains {
712                        time_series: contains {
713                            TYPE_ethernet: contains {
714                                v4: contains {
715                                    fetch_results: {
716                                        "type": "bitset",
717                                        "data": AnyBytesProperty,
718                                        metadata: {
719                                            index_node_path: "root/test_stats/metadata/fetch_results",
720                                        }
721                                    },
722                                }
723                            }
724                        }
725                    }
726                }
727            }
728        )
729    }
730
731    #[fuchsia::test]
732    fn test_log_ping_result_events() {
733        let mut harness = setup_test();
734        let logger_node = harness.inspect_node.create_child("interfaces");
735        let events_node = logger_node.create_child("events");
736        let time_series_node = logger_node.create_child("time_series");
737
738        let mut interface_aware_logger = InterfaceAwareLogger::new(
739            &harness.inspect_metadata_node,
740            &harness.inspect_metadata_path,
741            InterfaceTimeSeriesGrouping::Type(vec![InterfaceType::Ethernet]),
742            events_node,
743            time_series_node,
744        );
745
746        let ping_parameters_v4 = crate::PingParameters {
747            addr: std::net::SocketAddr::new(IPV4_ADDR.into(), 80),
748            interface_name: "eth0".to_string(),
749        };
750        let ping_parameters_v6 = crate::PingParameters {
751            addr: std::net::SocketAddr::new(IPV6_ADDR.into(), 80),
752            interface_name: "eth0".to_string(),
753        };
754
755        interface_aware_logger.log_gateway_ping_result(
756            vec![InterfaceIdentifier::Type(InterfaceType::Ethernet)],
757            &ping_parameters_v4,
758            &Ok(()),
759        );
760        interface_aware_logger.log_internet_ping_result(
761            vec![InterfaceIdentifier::Type(InterfaceType::Ethernet)],
762            &ping_parameters_v6,
763            &Err(PingError::NoReply),
764        );
765
766        let tree = harness.get_inspect_data_tree();
767        assert_data_tree!(
768            @executor harness.exec,
769            tree,
770            root: contains {
771                test_stats: contains {
772                    interfaces: contains {
773                        events: contains {
774                            TYPE_ethernet: {
775                                v4: {
776                                    gateway_ping_results: {
777                                        "0": contains {
778                                            "Created@time": AnyProperty,
779                                            count: 1u64,
780                                            log: {
781                                                address: "8.8.8.8:80",
782                                                interface_name: "eth0",
783                                                result: "Success",
784                                            }
785                                        }
786                                    },
787                                    internet_ping_results: {},
788                                    fetch_results: {}
789                                },
790                                v6: {
791                                    gateway_ping_results: {},
792                                    internet_ping_results: {
793                                        "0": contains {
794                                            "Created@time": AnyProperty,
795                                            count: 1u64,
796                                            log: {
797                                                address: "[2001:db8::]:80",
798                                                interface_name: "eth0",
799                                                result: "e_NoReply",
800                                            }
801                                        }
802                                    },
803                                    fetch_results: {}
804                                }
805                            }
806                        }
807                    }
808                }
809            }
810        )
811    }
812
813    #[fuchsia::test]
814    fn test_log_fetch_result_events() {
815        let mut harness = setup_test();
816        let logger_node = harness.inspect_node.create_child("interfaces");
817        let events_node = logger_node.create_child("events");
818        let time_series_node = logger_node.create_child("time_series");
819
820        let mut interface_aware_logger = InterfaceAwareLogger::new(
821            &harness.inspect_metadata_node,
822            &harness.inspect_metadata_path,
823            InterfaceTimeSeriesGrouping::Type(vec![InterfaceType::Ethernet]),
824            events_node,
825            time_series_node,
826        );
827
828        let fetch_parameters = crate::FetchParameters {
829            interface_name: "eth0".to_string(),
830            domain: "example.com".to_string(),
831            ip: IPV4_ADDR.into(),
832            path: "/".to_string(),
833            expected_statuses: vec![204],
834        };
835
836        interface_aware_logger.log_fetch_result(
837            vec![InterfaceIdentifier::Type(InterfaceType::Ethernet)],
838            &fetch_parameters,
839            &Ok(204),
840        );
841
842        let tree = harness.get_inspect_data_tree();
843        assert_data_tree!(
844            @executor harness.exec,
845            tree,
846            root: contains {
847                test_stats: contains {
848                    interfaces: contains {
849                        events: contains {
850                            TYPE_ethernet: {
851                                v4: {
852                                    gateway_ping_results: {},
853                                    internet_ping_results: {},
854                                    fetch_results: {
855                                        "0": contains {
856                                            "Created@time": AnyProperty,
857                                            count: 1u64,
858                                            log: {
859                                                host_and_path: "example.com/",
860                                                interface_name: "eth0",
861                                                resolved_address: "8.8.8.8",
862                                                result: "Completed_204",
863                                            }
864                                        }
865                                    }
866                                },
867                                v6: {
868                                    gateway_ping_results: {},
869                                    internet_ping_results: {},
870                                    fetch_results: {}
871                                }
872                            }
873                        }
874                    }
875                }
876            }
877        )
878    }
879}