Skip to main content

reachability_core/telemetry/
mod.rs

1// Copyright 2023 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
5mod convert;
6pub mod processors;
7use crate::fetch::FetchError;
8use crate::ping::PingError;
9use crate::telemetry::processors::link_properties_state::{
10    LinkProperties, LinkPropertiesStateLogger,
11};
12use crate::telemetry::processors::{
13    InterfaceIdentifier, InterfaceTimeSeriesGrouping, InterfaceType,
14};
15use crate::{FetchParameters, IpVersions, LinkState, PingParameters, State};
16use processors::interface_aware_logger::InterfaceAwareLogger;
17
18use anyhow::{Context, Error, format_err};
19use cobalt_client::traits::AsEventCode;
20use fidl_fuchsia_metrics::MetricEvent;
21use fuchsia_async as fasync;
22use fuchsia_cobalt_builders::MetricEventExt;
23use fuchsia_inspect::Node as InspectNode;
24use fuchsia_sync::Mutex;
25use futures::channel::mpsc;
26use futures::{Future, StreamExt, select};
27use log::{info, warn};
28use network_policy_metrics_registry as metrics;
29use static_assertions::const_assert_eq;
30use std::sync::Arc;
31use std::sync::atomic::{AtomicBool, Ordering};
32use windowed_stats::experimental::inspect::TimeMatrixClient;
33
34#[cfg(test)]
35mod testing;
36
37pub async fn create_metrics_logger(
38    factory_proxy: fidl_fuchsia_metrics::MetricEventLoggerFactoryProxy,
39) -> Result<fidl_fuchsia_metrics::MetricEventLoggerProxy, Error> {
40    let (cobalt_proxy, cobalt_server) =
41        fidl::endpoints::create_proxy::<fidl_fuchsia_metrics::MetricEventLoggerMarker>();
42
43    let project_spec = fidl_fuchsia_metrics::ProjectSpec {
44        customer_id: None, // defaults to fuchsia.
45        project_id: Some(metrics::PROJECT_ID),
46        ..Default::default()
47    };
48
49    let status = factory_proxy
50        .create_metric_event_logger(&project_spec, cobalt_server)
51        .await
52        .context("create metrics event logger")?;
53
54    match status {
55        Ok(_) => Ok(cobalt_proxy),
56        Err(err) => Err(format_err!("failed to create metrics event logger: {:?}", err)),
57    }
58}
59
60#[derive(Clone, Debug)]
61pub struct TelemetrySender {
62    sender: Arc<Mutex<mpsc::Sender<TelemetryEvent>>>,
63    sender_is_blocked: Arc<AtomicBool>,
64}
65
66impl TelemetrySender {
67    pub fn new(sender: mpsc::Sender<TelemetryEvent>) -> Self {
68        Self {
69            sender: Arc::new(Mutex::new(sender)),
70            sender_is_blocked: Arc::new(AtomicBool::new(false)),
71        }
72    }
73
74    // Send telemetry event. Log an error if it fails.
75    pub fn send(&self, event: TelemetryEvent) {
76        match self.sender.lock().try_send(event) {
77            Ok(_) => {
78                // If sender has been blocked before, set bool to false and log message.
79                if self
80                    .sender_is_blocked
81                    .compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst)
82                    .is_ok()
83                {
84                    info!("TelemetrySender recovered and resumed sending");
85                }
86            }
87            Err(_) => {
88                // If sender has not been blocked before, set bool to true and log error message.
89                if self
90                    .sender_is_blocked
91                    .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
92                    .is_ok()
93                {
94                    warn!(
95                        "TelemetrySender dropped a msg: either buffer is full or no receiver is waiting"
96                    );
97                }
98            }
99        }
100    }
101}
102
103#[derive(Debug, Clone, Default, PartialEq)]
104struct SystemStateSummary {
105    system_state: IpVersions<Option<State>>,
106}
107
108#[derive(Debug, Clone, Default, PartialEq)]
109struct NetworkConfig {
110    has_default_ipv4_route: bool,
111    has_default_ipv6_route: bool,
112}
113
114#[derive(Debug, Clone, Default)]
115pub struct SystemStateUpdate {
116    pub(crate) system_state: IpVersions<Option<State>>,
117}
118
119#[derive(Debug)]
120pub enum TelemetryEvent {
121    SystemStateUpdate {
122        update: SystemStateUpdate,
123    },
124    NetworkConfig {
125        has_default_ipv4_route: bool,
126        has_default_ipv6_route: bool,
127    },
128    GatewayProbe {
129        internet_available: bool,
130        gateway_discoverable: bool,
131        gateway_pingable: bool,
132    },
133    // The LinkProperties update corresponding to the interface described by
134    // the vector of interface identifiers.
135    LinkPropertiesUpdate {
136        interface_identifiers: Vec<InterfaceIdentifier>,
137        link_properties: IpVersions<LinkProperties>,
138    },
139    // The LinkState update corresponding to the interface described by the
140    // vector of interface identifiers.
141    LinkStateUpdate {
142        interface_identifiers: Vec<InterfaceIdentifier>,
143        link_state: IpVersions<LinkState>,
144    },
145    GatewayPingResult {
146        interface_identifiers: Vec<InterfaceIdentifier>,
147        ping_parameters: PingParameters,
148        gateway_ping_result: Result<(), PingError>,
149    },
150    InternetPingResult {
151        interface_identifiers: Vec<InterfaceIdentifier>,
152        ping_parameters: PingParameters,
153        internet_ping_result: Result<(), PingError>,
154    },
155    FetchResult {
156        interface_identifiers: Vec<InterfaceIdentifier>,
157        fetch_parameters: FetchParameters,
158        fetch_result: Result<u16, FetchError>,
159    },
160}
161
162/// Capacity of "first come, first serve" slots available to clients of
163/// the mpsc::Sender<TelemetryEvent>. This threshold is arbitrary.
164const TELEMETRY_EVENT_BUFFER_SIZE: usize = 100;
165
166const TELEMETRY_QUERY_INTERVAL: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(10);
167const METADATA_NODE_NAME: &str = "metadata";
168
169pub fn serve_telemetry(
170    cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
171    inspect_node: InspectNode,
172) -> (TelemetrySender, impl Future<Output = Result<(), Error>>) {
173    // Inspect nodes to hold Inspect events
174    let inspect_events_node = inspect_node.create_child("events");
175
176    // Inspect nodes to hold time series and metadata for other nodes.
177    let inspect_time_series_node = inspect_node.create_child("time_series");
178    let link_properties_state_time_series_node =
179        inspect_time_series_node.create_child("link_properties_state");
180    let time_matrix_client =
181        TimeMatrixClient::new(link_properties_state_time_series_node.clone_weak());
182
183    inspect_node.record(link_properties_state_time_series_node);
184    let inspect_metadata_node = inspect_node.create_child(METADATA_NODE_NAME);
185    let link_properties_state_logger = LinkPropertiesStateLogger::new(
186        &inspect_metadata_node,
187        &format!("root/telemetry/{METADATA_NODE_NAME}"),
188        InterfaceTimeSeriesGrouping::Type(vec![InterfaceType::Ethernet, InterfaceType::WlanClient]),
189        &time_matrix_client,
190    );
191
192    let interface_aware_events_node = inspect_events_node.create_child("interfaces");
193    let interface_aware_time_series_node = inspect_time_series_node.create_child("interfaces");
194    let interface_aware_logger = InterfaceAwareLogger::new(
195        &inspect_metadata_node,
196        &format!("root/telemetry/{METADATA_NODE_NAME}"),
197        InterfaceTimeSeriesGrouping::Type(vec![InterfaceType::Ethernet, InterfaceType::WlanClient]),
198        interface_aware_events_node,
199        interface_aware_time_series_node,
200    );
201
202    // Record the time series and metadata nodes so they do not get dropped.
203    inspect_node.record(inspect_events_node);
204    inspect_node.record(inspect_time_series_node);
205    inspect_node.record(inspect_metadata_node);
206
207    let (sender, fut) = serve_telemetry_inner(
208        cobalt_proxy,
209        inspect_node,
210        link_properties_state_logger,
211        interface_aware_logger,
212    );
213    (sender, fut)
214}
215
216fn serve_telemetry_inner(
217    cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
218    inspect_node: InspectNode,
219    link_properties_state_logger: LinkPropertiesStateLogger,
220    interface_aware_logger: InterfaceAwareLogger,
221) -> (TelemetrySender, impl Future<Output = Result<(), Error>>) {
222    let (sender, mut receiver) = mpsc::channel::<TelemetryEvent>(TELEMETRY_EVENT_BUFFER_SIZE);
223    let sender = TelemetrySender::new(sender);
224    let cloned_sender = sender.clone();
225
226    let fut = async move {
227        let link_properties_state_logger = link_properties_state_logger;
228
229        let mut report_interval_stream = fasync::Interval::new(TELEMETRY_QUERY_INTERVAL);
230        const ONE_MINUTE: zx::MonotonicDuration = zx::MonotonicDuration::from_minutes(1);
231        const_assert_eq!(ONE_MINUTE.into_nanos() % TELEMETRY_QUERY_INTERVAL.into_nanos(), 0);
232        const INTERVAL_TICKS_PER_MINUTE: u64 =
233            (ONE_MINUTE.into_nanos() / TELEMETRY_QUERY_INTERVAL.into_nanos()) as u64;
234        const INTERVAL_TICKS_PER_HR: u64 = INTERVAL_TICKS_PER_MINUTE * 60;
235        let mut interval_tick = 0u64;
236        let mut telemetry = Telemetry::new(
237            cloned_sender,
238            cobalt_proxy,
239            link_properties_state_logger,
240            interface_aware_logger,
241            inspect_node,
242        );
243        loop {
244            select! {
245                event = receiver.next() => {
246                    if let Some(event) = event {
247                        telemetry.handle_telemetry_event(event).await;
248                    }
249                }
250                _ = report_interval_stream.next() => {
251                    interval_tick += 1;
252                    if interval_tick % INTERVAL_TICKS_PER_HR == 0 {
253                        telemetry.handle_hourly_telemetry().await;
254                    }
255                }
256            }
257        }
258    };
259    (sender, fut)
260}
261
262// Macro wrapper for logging simple events (occurrence, integer, histogram, string)
263// and log a warning when the status is not Ok.
264macro_rules! log_cobalt {
265    ($cobalt_proxy:expr, $method_name:ident, $metric_id:expr, $value:expr, $event_codes:expr $(,)?) => {{
266        let status = $cobalt_proxy.$method_name($metric_id, $value, $event_codes).await;
267        match status {
268            Ok(Ok(())) => (),
269            Ok(Err(e)) => warn!("Failed logging metric: {}, error: {:?}", $metric_id, e),
270            Err(e) => warn!("Failed logging metric: {}, error: {}", $metric_id, e),
271        }
272    }};
273}
274
275macro_rules! log_cobalt_batch {
276    ($cobalt_proxy:expr, $events:expr, $context:expr $(,)?) => {{
277        let status = $cobalt_proxy.log_metric_events($events).await;
278        match status {
279            Ok(Ok(())) => (),
280            Ok(Err(e)) => {
281                warn!("Failed logging batch metrics, context: {}, error: {:?}", $context, e)
282            }
283            Err(e) => warn!("Failed logging batch metrics, context: {}, error: {}", $context, e),
284        }
285    }};
286}
287
288struct Telemetry {
289    cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
290    state_summary: Option<SystemStateSummary>,
291    state_last_refreshed_for_cobalt: fasync::MonotonicInstant,
292    state_last_refreshed_for_inspect: fasync::MonotonicInstant,
293    reachability_lost_at: Option<(
294        fasync::MonotonicInstant,
295        metrics::ReachabilityGlobalSnapshotDurationMetricDimensionRouteConfig,
296    )>,
297    network_config: Option<NetworkConfig>,
298    network_config_last_refreshed: fasync::MonotonicInstant,
299    link_properties_state_logger: LinkPropertiesStateLogger,
300    interface_aware_logger: InterfaceAwareLogger,
301
302    _inspect_node: InspectNode,
303}
304
305impl Telemetry {
306    pub fn new(
307        _telemetry_sender: TelemetrySender,
308        cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
309        link_properties_state_logger: LinkPropertiesStateLogger,
310        interface_aware_logger: InterfaceAwareLogger,
311        inspect_node: InspectNode,
312    ) -> Self {
313        Self {
314            cobalt_proxy,
315            state_summary: None,
316            state_last_refreshed_for_cobalt: fasync::MonotonicInstant::now(),
317            state_last_refreshed_for_inspect: fasync::MonotonicInstant::now(),
318            reachability_lost_at: None,
319            network_config: None,
320            network_config_last_refreshed: fasync::MonotonicInstant::now(),
321            link_properties_state_logger,
322            interface_aware_logger,
323            _inspect_node: inspect_node,
324        }
325    }
326
327    pub async fn handle_telemetry_event(&mut self, event: TelemetryEvent) {
328        let now = fasync::MonotonicInstant::now();
329        match event {
330            TelemetryEvent::SystemStateUpdate { update: SystemStateUpdate { system_state } } => {
331                let new_state = Some(match &self.state_summary {
332                    Some(summary) => SystemStateSummary { system_state, ..summary.clone() },
333                    None => SystemStateSummary { system_state, ..SystemStateSummary::default() },
334                });
335                if self.state_summary != new_state {
336                    // Only log if system state has changed to prevent spamming Cobalt,
337                    // since this telemetry event may happen in quick succession.
338                    self.log_system_state_metrics(
339                        new_state,
340                        now,
341                        "handle_telemetry_event(TelemetryEvent::SystemStateUpdate)",
342                    )
343                    .await
344                }
345            }
346            TelemetryEvent::NetworkConfig { has_default_ipv4_route, has_default_ipv6_route } => {
347                let new_config =
348                    Some(NetworkConfig { has_default_ipv4_route, has_default_ipv6_route });
349                self.log_network_config_metrics(
350                    new_config,
351                    now,
352                    "handle_telemetry_event(TelemetryEvent::NetworkConfig)",
353                )
354                .await;
355            }
356            TelemetryEvent::GatewayProbe {
357                internet_available,
358                gateway_discoverable,
359                gateway_pingable,
360            } => {
361                if internet_available {
362                    let metric_id = match (gateway_discoverable, gateway_pingable) {
363                        (true, true) => None,
364                        (true, false) => {
365                            Some(metrics::INTERNET_AVAILABLE_GATEWAY_NOT_PINGABLE_METRIC_ID)
366                        }
367                        (false, true) => {
368                            Some(metrics::INTERNET_AVAILABLE_GATEWAY_NOT_DISCOVERABLE_METRIC_ID)
369                        }
370                        (false, false) => Some(metrics::INTERNET_AVAILABLE_GATEWAY_LOST_METRIC_ID),
371                    };
372                    if let Some(metric_id) = metric_id {
373                        log_cobalt!(self.cobalt_proxy, log_occurrence, metric_id, 1, &[]);
374                    }
375                }
376            }
377            TelemetryEvent::LinkPropertiesUpdate { interface_identifiers, link_properties } => {
378                self.link_properties_state_logger
379                    .update_link_properties(interface_identifiers, &link_properties);
380            }
381            TelemetryEvent::LinkStateUpdate { interface_identifiers, link_state } => {
382                self.link_properties_state_logger
383                    .update_link_state(interface_identifiers, &link_state);
384            }
385            TelemetryEvent::GatewayPingResult {
386                interface_identifiers,
387                ping_parameters,
388                gateway_ping_result,
389            } => {
390                self.interface_aware_logger.log_gateway_ping_result(
391                    interface_identifiers,
392                    &ping_parameters,
393                    &gateway_ping_result,
394                );
395            }
396            TelemetryEvent::InternetPingResult {
397                interface_identifiers,
398                ping_parameters,
399                internet_ping_result,
400            } => {
401                self.interface_aware_logger.log_internet_ping_result(
402                    interface_identifiers,
403                    &ping_parameters,
404                    &internet_ping_result,
405                );
406            }
407            TelemetryEvent::FetchResult {
408                interface_identifiers,
409                fetch_parameters,
410                fetch_result,
411            } => {
412                self.interface_aware_logger.log_fetch_result(
413                    interface_identifiers,
414                    &fetch_parameters,
415                    &fetch_result,
416                );
417            }
418        }
419    }
420
421    async fn log_system_state_metrics(
422        &mut self,
423        new_state: Option<SystemStateSummary>,
424        now: fasync::MonotonicInstant,
425        ctx: &'static str,
426    ) {
427        let duration_cobalt = now - self.state_last_refreshed_for_cobalt;
428        let mut metric_events = vec![];
429
430        if let Some(prev) = &self.state_summary {
431            if prev.system_state.has_interface_up() {
432                metric_events.push(
433                    MetricEvent::builder(
434                        metrics::REACHABILITY_STATE_UP_OR_ABOVE_DURATION_METRIC_ID,
435                    )
436                    .as_integer(duration_cobalt.into_micros()),
437                );
438
439                let route_config_dim = convert::convert_route_config(&prev.system_state);
440                let internet_available =
441                    convert::convert_yes_no_dim(prev.system_state.has_internet());
442                let gateway_reachable =
443                    convert::convert_yes_no_dim(prev.system_state.has_gateway());
444                let dns_active = convert::convert_yes_no_dim(prev.system_state.has_dns());
445                let http_status = if prev.system_state.has_http() {
446                    metrics::NetworkPolicyMetricDimensionHttpStatus::HttpOnly
447                } else {
448                    metrics::NetworkPolicyMetricDimensionHttpStatus::Neither
449                };
450                // We only log metric when at least one of IPv4 or IPv6 is in the SystemState.
451                if let Some(route_config) = route_config_dim {
452                    metric_events.push(
453                        MetricEvent::builder(
454                            metrics::REACHABILITY_GLOBAL_SNAPSHOT_DURATION_METRIC_ID,
455                        )
456                        .with_event_codes(metrics::ReachabilityGlobalSnapshotDurationEventCodes {
457                            route_config,
458                            internet_available,
459                            gateway_reachable,
460                            dns_active,
461                            http_status,
462                        })
463                        .as_integer(duration_cobalt.into_micros()),
464                    );
465                }
466            }
467        }
468
469        if let (Some(prev), Some(new_state)) = (&self.state_summary, &new_state) {
470            let previously_reachable =
471                prev.system_state.has_internet() && prev.system_state.has_dns();
472            let now_reachable =
473                new_state.system_state.has_internet() && new_state.system_state.has_dns();
474            if previously_reachable && !now_reachable {
475                let route_config_dim = convert::convert_route_config(&prev.system_state);
476                if let Some(route_config_dim) = route_config_dim {
477                    metric_events.push(
478                        MetricEvent::builder(metrics::REACHABILITY_LOST_METRIC_ID)
479                            .with_event_code(route_config_dim.as_event_code())
480                            .as_occurrence(1),
481                    );
482                    self.reachability_lost_at = Some((now, route_config_dim));
483                }
484            }
485
486            if !previously_reachable && now_reachable {
487                if let Some((reachability_lost_at, route_config_dim)) = self.reachability_lost_at {
488                    metric_events.push(
489                        MetricEvent::builder(metrics::REACHABILITY_LOST_DURATION_METRIC_ID)
490                            .with_event_code(route_config_dim.as_event_code())
491                            .as_integer((now - reachability_lost_at).into_micros()),
492                    );
493                }
494                self.reachability_lost_at = None;
495            }
496        }
497
498        if !metric_events.is_empty() {
499            log_cobalt_batch!(self.cobalt_proxy, &metric_events, ctx);
500        }
501
502        if let Some(new_state) = new_state {
503            self.state_summary = Some(new_state);
504        }
505        self.state_last_refreshed_for_cobalt = now;
506        self.state_last_refreshed_for_inspect = now;
507    }
508
509    async fn log_network_config_metrics(
510        &mut self,
511        new_config: Option<NetworkConfig>,
512        now: fasync::MonotonicInstant,
513        ctx: &'static str,
514    ) {
515        let duration = now - self.network_config_last_refreshed;
516        let mut metric_events = vec![];
517
518        if let Some(prev) = &self.network_config {
519            let default_route_dim = convert::convert_default_route(
520                prev.has_default_ipv4_route,
521                prev.has_default_ipv6_route,
522            );
523            // We only log metric when at least one of IPv4 or IPv6 has default route.
524            if let Some(default_route_dim) = default_route_dim {
525                metric_events.push(
526                    MetricEvent::builder(
527                        metrics::REACHABILITY_GLOBAL_DEFAULT_ROUTE_DURATION_METRIC_ID,
528                    )
529                    .with_event_code(default_route_dim.as_event_code())
530                    .as_integer(duration.into_micros()),
531                );
532                metric_events.push(
533                    MetricEvent::builder(
534                        metrics::REACHABILITY_GLOBAL_DEFAULT_ROUTE_OCCURRENCE_METRIC_ID,
535                    )
536                    .with_event_code(default_route_dim.as_event_code())
537                    .as_occurrence(1),
538                );
539            }
540        }
541
542        if !metric_events.is_empty() {
543            log_cobalt_batch!(self.cobalt_proxy, &metric_events, ctx);
544        }
545
546        if let Some(new_config) = new_config {
547            self.network_config = Some(new_config);
548        }
549        self.network_config_last_refreshed = now;
550    }
551
552    pub async fn handle_hourly_telemetry(&mut self) {
553        let now = fasync::MonotonicInstant::now();
554        self.log_system_state_metrics(None, now, "handle_hourly_telemetry").await;
555        self.log_network_config_metrics(None, now, "handle_hourly_telemetry").await;
556    }
557}
558
559#[cfg(test)]
560mod tests {
561    use crate::{ApplicationState, LinkState};
562
563    use super::*;
564    use fidl::endpoints::create_proxy_and_stream;
565    use fidl_fuchsia_metrics::MetricEventPayload;
566    use fuchsia_inspect::Inspector;
567    use fuchsia_inspect_contrib::id_enum::IdEnum;
568
569    use futures::task::Poll;
570    use std::pin::Pin;
571    use test_case::test_case;
572    use windowed_stats::experimental::clock::Timed;
573    use windowed_stats::experimental::testing::{MockTimeMatrixClient, TimeMatrixCall};
574
575    const STEP_INCREMENT: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(1);
576
577    #[test]
578    fn test_log_state_change() {
579        let (mut test_helper, mut test_fut) = setup_test();
580
581        let mut update = SystemStateUpdate {
582            system_state: IpVersions { ipv4: Some(LinkState::Internet.into()), ipv6: None },
583        };
584        test_helper
585            .telemetry_sender
586            .send(TelemetryEvent::SystemStateUpdate { update: update.clone() });
587
588        test_helper.advance_by(zx::MonotonicDuration::from_seconds(25), &mut test_fut);
589
590        update.system_state = IpVersions {
591            ipv4: Some(State {
592                link: LinkState::Internet,
593                application: ApplicationState { dns_resolved: true, http_fetch_succeeded: true },
594            }),
595            ipv6: None,
596        };
597        test_helper
598            .telemetry_sender
599            .send(TelemetryEvent::SystemStateUpdate { update: update.clone() });
600
601        test_helper.advance_test_fut(&mut test_fut);
602
603        let logged_metrics = test_helper
604            .get_logged_metrics(metrics::REACHABILITY_STATE_UP_OR_ABOVE_DURATION_METRIC_ID);
605        assert_eq!(logged_metrics.len(), 1);
606        assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(25_000_000));
607
608        let logged_metrics = test_helper
609            .get_logged_metrics(metrics::REACHABILITY_GLOBAL_SNAPSHOT_DURATION_METRIC_ID);
610        assert_eq!(logged_metrics.len(), 1);
611        assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(25_000_000));
612        assert_eq!(
613            logged_metrics[0].event_codes,
614            &[
615                metrics::ReachabilityGlobalSnapshotDurationMetricDimensionRouteConfig::Ipv4Only
616                    as u32,
617                metrics::ReachabilityGlobalSnapshotDurationMetricDimensionInternetAvailable::Yes
618                    as u32,
619                metrics::ReachabilityGlobalSnapshotDurationMetricDimensionGatewayReachable::Yes
620                    as u32,
621                metrics::ReachabilityGlobalSnapshotDurationMetricDimensionDnsActive::No as u32,
622                metrics::ReachabilityGlobalSnapshotDurationMetricDimensionHttpStatus::Neither
623                    as u32,
624            ]
625        );
626
627        test_helper.cobalt_events.clear();
628        test_helper.advance_by(zx::MonotonicDuration::from_seconds(3575), &mut test_fut);
629
630        // At the 1 hour mark, the new state is logged via periodic telemetry.
631        // All metrics are the same as before, except for the elapsed duration and the
632        // `dns_active` flag is now true.
633        let logged_metrics = test_helper
634            .get_logged_metrics(metrics::REACHABILITY_STATE_UP_OR_ABOVE_DURATION_METRIC_ID);
635        assert_eq!(logged_metrics.len(), 1);
636        assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(3575_000_000));
637
638        let logged_metrics = test_helper
639            .get_logged_metrics(metrics::REACHABILITY_GLOBAL_SNAPSHOT_DURATION_METRIC_ID);
640        assert_eq!(logged_metrics.len(), 1);
641        assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(3575_000_000));
642        assert_eq!(
643            logged_metrics[0].event_codes,
644            &[
645                metrics::ReachabilityGlobalSnapshotDurationMetricDimensionRouteConfig::Ipv4Only
646                    as u32,
647                metrics::ReachabilityGlobalSnapshotDurationMetricDimensionInternetAvailable::Yes
648                    as u32,
649                metrics::ReachabilityGlobalSnapshotDurationMetricDimensionGatewayReachable::Yes
650                    as u32,
651                // This time dns_active is Yes, and http_status is HttpOnly
652                metrics::ReachabilityGlobalSnapshotDurationMetricDimensionDnsActive::Yes as u32,
653                metrics::ReachabilityGlobalSnapshotDurationMetricDimensionHttpStatus::HttpOnly
654                    as u32,
655            ]
656        );
657    }
658
659    #[test]
660    fn test_log_reachability_lost() {
661        let (mut test_helper, mut test_fut) = setup_test();
662
663        let mut update = SystemStateUpdate {
664            system_state: IpVersions {
665                ipv4: None,
666                ipv6: Some(State {
667                    link: LinkState::Internet,
668                    application: ApplicationState { dns_resolved: true, ..Default::default() },
669                }),
670            },
671            ..SystemStateUpdate::default()
672        };
673        test_helper
674            .telemetry_sender
675            .send(TelemetryEvent::SystemStateUpdate { update: update.clone() });
676        test_helper.advance_test_fut(&mut test_fut);
677
678        update.system_state = IpVersions {
679            ipv4: None,
680            ipv6: Some(State { link: LinkState::Internet, ..Default::default() }),
681        };
682        test_helper
683            .telemetry_sender
684            .send(TelemetryEvent::SystemStateUpdate { update: update.clone() });
685        test_helper.advance_test_fut(&mut test_fut);
686
687        // Reachability lost metric is lost because previously both `internet_available` and
688        // `dns_active` were true, and now the latter is false.
689        let logged_metrics = test_helper.get_logged_metrics(metrics::REACHABILITY_LOST_METRIC_ID);
690        assert_eq!(logged_metrics.len(), 1);
691        assert_eq!(logged_metrics[0].payload, MetricEventPayload::Count(1));
692        assert_eq!(
693            logged_metrics[0].event_codes,
694            &[metrics::ReachabilityLostMetricDimensionRouteConfig::Ipv6Only as u32]
695        );
696
697        test_helper.cobalt_events.clear();
698
699        update.system_state = IpVersions {
700            ipv4: None,
701            ipv6: Some(State { link: LinkState::Down, ..Default::default() }),
702        };
703        test_helper
704            .telemetry_sender
705            .send(TelemetryEvent::SystemStateUpdate { update: update.clone() });
706        test_helper.advance_test_fut(&mut test_fut);
707
708        // Reachability lost metric is not logged again even when `internet_available`
709        // becomes false, because it was already considered lost previously.
710        let logged_metrics = test_helper.get_logged_metrics(metrics::REACHABILITY_LOST_METRIC_ID);
711        assert_eq!(logged_metrics.len(), 0);
712
713        test_helper.cobalt_events.clear();
714
715        test_helper.advance_by(zx::MonotonicDuration::from_hours(2), &mut test_fut);
716        update.system_state = IpVersions {
717            ipv4: Some(State {
718                link: LinkState::Internet,
719                application: ApplicationState { dns_resolved: true, ..Default::default() },
720            }),
721            ipv6: None,
722        };
723        test_helper
724            .telemetry_sender
725            .send(TelemetryEvent::SystemStateUpdate { update: update.clone() });
726        test_helper.advance_test_fut(&mut test_fut);
727
728        // When reachability is recovered, the duration that reachability was lost is logged.
729        let logged_metrics =
730            test_helper.get_logged_metrics(metrics::REACHABILITY_LOST_DURATION_METRIC_ID);
731        assert_eq!(logged_metrics.len(), 1);
732        assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(7200_000_000));
733        assert_eq!(
734            logged_metrics[0].event_codes,
735            // Reachability is recovered on ipv4, but we still log the ipv6 dimension because
736            // that was the config when reachability was lost.
737            &[metrics::ReachabilityLostMetricDimensionRouteConfig::Ipv6Only as u32]
738        );
739    }
740
741    #[test_case(true, true, Some(metrics::ReachabilityGlobalDefaultRouteDurationMetricDimensionDefaultRoute::Ipv4Ipv6); "ipv4+ipv6 default routes")]
742    #[test_case(true, false, Some(metrics::ReachabilityGlobalDefaultRouteDurationMetricDimensionDefaultRoute::Ipv4Only); "Ipv4Only default routes")]
743    #[test_case(false, true, Some(metrics::ReachabilityGlobalDefaultRouteDurationMetricDimensionDefaultRoute::Ipv6Only); "Ipv6Only default routes")]
744    #[test_case(false, false, None; "no default routes")]
745    #[fuchsia::test(add_test_attr = false)]
746    fn test_log_default_route(
747        has_default_ipv4_route: bool,
748        has_default_ipv6_route: bool,
749        expected_dim: Option<
750            metrics::ReachabilityGlobalDefaultRouteDurationMetricDimensionDefaultRoute,
751        >,
752    ) {
753        let (mut test_helper, mut test_fut) = setup_test();
754
755        test_helper
756            .telemetry_sender
757            .send(TelemetryEvent::NetworkConfig { has_default_ipv4_route, has_default_ipv6_route });
758        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), &mut test_fut);
759
760        let logged_metrics = test_helper
761            .get_logged_metrics(metrics::REACHABILITY_GLOBAL_DEFAULT_ROUTE_DURATION_METRIC_ID);
762        match expected_dim {
763            Some(dim) => {
764                assert_eq!(logged_metrics.len(), 1);
765                assert_eq!(
766                    logged_metrics[0].payload,
767                    MetricEventPayload::IntegerValue(3600_000_000)
768                );
769                assert_eq!(logged_metrics[0].event_codes, &[dim as u32]);
770            }
771            None => {
772                assert_eq!(logged_metrics.len(), 0);
773            }
774        }
775
776        let logged_metrics = test_helper
777            .get_logged_metrics(metrics::REACHABILITY_GLOBAL_DEFAULT_ROUTE_OCCURRENCE_METRIC_ID);
778        match expected_dim {
779            Some(dim) => {
780                assert_eq!(logged_metrics.len(), 1);
781                assert_eq!(logged_metrics[0].payload, MetricEventPayload::Count(1));
782                assert_eq!(logged_metrics[0].event_codes, &[dim as u32]);
783            }
784            None => {
785                assert_eq!(logged_metrics.len(), 0);
786            }
787        }
788    }
789
790    #[test_case(true, true, vec![]; "negative")]
791    #[test_case(true, false, vec![metrics::INTERNET_AVAILABLE_GATEWAY_NOT_PINGABLE_METRIC_ID]; "gateway_discoverable_but_not_pingable")]
792    #[test_case(false, true, vec![metrics::INTERNET_AVAILABLE_GATEWAY_NOT_DISCOVERABLE_METRIC_ID]; "gateway_pingable_but_not_discoverable")]
793    #[test_case(false, false, vec![metrics::INTERNET_AVAILABLE_GATEWAY_LOST_METRIC_ID]; "gateway_neither_dicoverable_nor_pingable")]
794    #[fuchsia::test(add_test_attr = false)]
795    fn test_log_abnormal_state_situation(
796        gateway_discoverable: bool,
797        gateway_pingable: bool,
798        expected_metrics: Vec<u32>,
799    ) {
800        let (mut test_helper, mut test_fut) = setup_test();
801
802        test_helper.telemetry_sender.send(TelemetryEvent::GatewayProbe {
803            internet_available: true,
804            gateway_discoverable,
805            gateway_pingable,
806        });
807        test_helper.advance_test_fut(&mut test_fut);
808
809        let logged_metrics: Vec<u32> = [
810            metrics::INTERNET_AVAILABLE_GATEWAY_NOT_PINGABLE_METRIC_ID,
811            metrics::INTERNET_AVAILABLE_GATEWAY_NOT_DISCOVERABLE_METRIC_ID,
812            metrics::INTERNET_AVAILABLE_GATEWAY_LOST_METRIC_ID,
813        ]
814        .into_iter()
815        .filter(|id| test_helper.get_logged_metrics(*id).len() > 0)
816        .collect();
817
818        assert_eq!(logged_metrics, expected_metrics);
819    }
820
821    #[test]
822    fn test_link_properties_update() {
823        let (mut test_helper, mut test_fut) = setup_test();
824
825        // Iterate through all of the different combinations of booleans in the
826        // LinkProperties struct. There are 4 different booleans, so 2^4
827        // different combinations.
828        for i in 0..=15 {
829            let link_properties = LinkProperties::from(i);
830            let mut expected_data_vec = vec![];
831            // Index 0 indicates that all the booleans are false which is the
832            // same as the default value of LinkProperties. In this case no
833            // data is reported, so expected_data_vec should be empty.
834            if i != 0 {
835                expected_data_vec.push(TimeMatrixCall::Fold(Timed::now(i)));
836            }
837
838            //  There should be no calls to the `TYPE_ethernet` or `TYPE_wlanclient`
839            // time series since nothing has been logged yet.
840            let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
841            assert_eq!(
842                &time_matrix_calls.drain::<u64>("link_properties_v4_TYPE_ethernet")[..],
843                &[]
844            );
845            assert_eq!(
846                &time_matrix_calls.drain::<u64>("link_properties_v6_TYPE_ethernet")[..],
847                &[]
848            );
849            assert_eq!(
850                &time_matrix_calls.drain::<u64>("link_properties_v4_TYPE_wlanclient")[..],
851                &[]
852            );
853            assert_eq!(
854                &time_matrix_calls.drain::<u64>("link_properties_v6_TYPE_wlanclient")[..],
855                &[]
856            );
857
858            test_helper.telemetry_sender.send(TelemetryEvent::LinkPropertiesUpdate {
859                interface_identifiers: vec![InterfaceIdentifier::Type(InterfaceType::Ethernet)],
860                link_properties: IpVersions {
861                    ipv4: link_properties.clone(),
862                    ipv6: link_properties,
863                },
864            });
865            test_helper.advance_test_fut(&mut test_fut);
866
867            time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
868            assert_eq!(
869                &time_matrix_calls.drain::<u64>("link_properties_v4_TYPE_ethernet")[..],
870                expected_data_vec
871            );
872            assert_eq!(
873                &time_matrix_calls.drain::<u64>("link_properties_v6_TYPE_ethernet")[..],
874                expected_data_vec
875            );
876            // There should be no calls to the `TYPE_wlanclient` time series since the
877            // update above was for `Ethernet`.
878            assert_eq!(
879                &time_matrix_calls.drain::<u64>("link_properties_v4_TYPE_wlanclient")[..],
880                &[]
881            );
882            assert_eq!(
883                &time_matrix_calls.drain::<u64>("link_properties_v6_TYPE_wlanclient")[..],
884                &[]
885            );
886        }
887    }
888
889    #[test]
890    fn test_link_state_update() {
891        let (mut test_helper, mut test_fut) = setup_test();
892
893        // Iterate through all of the different variants in the LinkState enum.
894        // LinkState::None is the default so there should be no data recorded
895        // for that value.
896        let initial_value = LinkState::None.to_id() as u64;
897        let final_value = LinkState::Internet.to_id() as u64;
898        for i in initial_value..=final_value {
899            let link_state = i.into();
900            let mut expected_data_vec = vec![];
901            if i != initial_value {
902                expected_data_vec.push(TimeMatrixCall::Fold(Timed::now(1 << i)));
903            }
904
905            //  There should be no calls to the `TYPE_ethernet` or `TYPE_wlanclient`
906            // time series since nothing has been logged yet.
907            let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
908            assert_eq!(&time_matrix_calls.drain::<u64>("link_state_v4_TYPE_ethernet")[..], &[]);
909            assert_eq!(&time_matrix_calls.drain::<u64>("link_state_v6_TYPE_ethernet")[..], &[]);
910            assert_eq!(&time_matrix_calls.drain::<u64>("link_state_v4_TYPE_wlanclient")[..], &[]);
911            assert_eq!(&time_matrix_calls.drain::<u64>("link_state_v6_TYPE_wlanclient")[..], &[]);
912
913            test_helper.telemetry_sender.send(TelemetryEvent::LinkStateUpdate {
914                interface_identifiers: vec![InterfaceIdentifier::Type(InterfaceType::Ethernet)],
915                link_state: IpVersions { ipv4: link_state, ipv6: link_state },
916            });
917            test_helper.advance_test_fut(&mut test_fut);
918
919            time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
920            assert_eq!(
921                &time_matrix_calls.drain::<u64>("link_state_v4_TYPE_ethernet")[..],
922                expected_data_vec
923            );
924            assert_eq!(
925                &time_matrix_calls.drain::<u64>("link_state_v6_TYPE_ethernet")[..],
926                expected_data_vec
927            );
928            // There should be no calls to the `TYPE_wlanclient` time series since the
929            // update above was for `Ethernet`.
930            assert_eq!(&time_matrix_calls.drain::<u64>("link_state_v4_TYPE_wlanclient")[..], &[]);
931            assert_eq!(&time_matrix_calls.drain::<u64>("link_state_v6_TYPE_wlanclient")[..], &[]);
932        }
933    }
934
935    struct TestHelper {
936        telemetry_sender: TelemetrySender,
937        _inspector: Inspector,
938        cobalt_stream: fidl_fuchsia_metrics::MetricEventLoggerRequestStream,
939        /// As requests to Cobalt are responded to via `self.drain_cobalt_events()`,
940        /// their payloads are drained to this HashMap.
941        cobalt_events: Vec<MetricEvent>,
942        mock_time_matrix_client: MockTimeMatrixClient,
943
944        // Note: keep the executor field last in the struct so it gets dropped last.
945        exec: fasync::TestExecutor,
946    }
947
948    impl TestHelper {
949        /// Advance executor until stalled, taking care of any blocking requests
950        fn advance_test_fut<T>(&mut self, test_fut: &mut (impl Future<Output = T> + Unpin)) {
951            let mut made_progress = true;
952            while made_progress {
953                let _result = self.exec.run_until_stalled(test_fut);
954                made_progress = false;
955                while let Poll::Ready(Some(Ok(req))) =
956                    self.exec.run_until_stalled(&mut self.cobalt_stream.next())
957                {
958                    self.cobalt_events.append(&mut req.respond_to_metric_req(Ok(())));
959                    made_progress = true;
960                }
961            }
962
963            match self.exec.run_until_stalled(test_fut) {
964                Poll::Pending => (),
965                _ => panic!("expect test_fut to resolve to Poll::Pending"),
966            }
967        }
968
969        /// Advance executor by `duration`.
970        /// This function repeatedly advances the executor by 1 second, triggering
971        /// any expired timers and running the test_fut, until `duration` is reached.
972        fn advance_by<T>(
973            &mut self,
974            duration: zx::MonotonicDuration,
975            test_fut: &mut (impl Future<Output = T> + Unpin),
976        ) {
977            assert_eq!(
978                duration.into_nanos() % STEP_INCREMENT.into_nanos(),
979                0,
980                "duration {:?} is not divisible by STEP_INCREMENT",
981                duration,
982            );
983            const_assert_eq!(
984                TELEMETRY_QUERY_INTERVAL.into_nanos() % STEP_INCREMENT.into_nanos(),
985                0
986            );
987
988            self.advance_test_fut(test_fut);
989
990            for _i in 0..(duration.into_nanos() / STEP_INCREMENT.into_nanos()) {
991                self.exec.set_fake_time(fasync::MonotonicInstant::after(STEP_INCREMENT));
992                let _ = self.exec.wake_expired_timers();
993                self.advance_test_fut(test_fut);
994            }
995        }
996
997        fn get_logged_metrics(&self, metric_id: u32) -> Vec<MetricEvent> {
998            self.cobalt_events.iter().filter(|ev| ev.metric_id == metric_id).cloned().collect()
999        }
1000    }
1001
1002    trait CobaltExt {
1003        // Respond to MetricEventLoggerRequest and extract its MetricEvent.
1004        fn respond_to_metric_req(
1005            self,
1006            result: Result<(), fidl_fuchsia_metrics::Error>,
1007        ) -> Vec<fidl_fuchsia_metrics::MetricEvent>;
1008    }
1009
1010    impl CobaltExt for fidl_fuchsia_metrics::MetricEventLoggerRequest {
1011        fn respond_to_metric_req(
1012            self,
1013            result: Result<(), fidl_fuchsia_metrics::Error>,
1014        ) -> Vec<fidl_fuchsia_metrics::MetricEvent> {
1015            match self {
1016                Self::LogOccurrence { metric_id, count, event_codes, responder } => {
1017                    assert!(responder.send(result).is_ok());
1018                    vec![MetricEvent {
1019                        metric_id,
1020                        event_codes,
1021                        payload: MetricEventPayload::Count(count),
1022                    }]
1023                }
1024                Self::LogInteger { metric_id, value, event_codes, responder } => {
1025                    assert!(responder.send(result).is_ok());
1026                    vec![MetricEvent {
1027                        metric_id,
1028                        event_codes,
1029                        payload: MetricEventPayload::IntegerValue(value),
1030                    }]
1031                }
1032                Self::LogIntegerHistogram { metric_id, histogram, event_codes, responder } => {
1033                    assert!(responder.send(result).is_ok());
1034                    vec![MetricEvent {
1035                        metric_id,
1036                        event_codes,
1037                        payload: MetricEventPayload::Histogram(histogram),
1038                    }]
1039                }
1040                Self::LogString { metric_id, string_value, event_codes, responder } => {
1041                    assert!(responder.send(result).is_ok());
1042                    vec![MetricEvent {
1043                        metric_id,
1044                        event_codes,
1045                        payload: MetricEventPayload::StringValue(string_value),
1046                    }]
1047                }
1048                Self::LogMetricEvents { events, responder } => {
1049                    assert!(responder.send(result).is_ok());
1050                    events
1051                }
1052            }
1053        }
1054    }
1055
1056    fn setup_test() -> (TestHelper, Pin<Box<impl Future<Output = Result<(), Error>>>>) {
1057        let mut exec = fasync::TestExecutor::new_with_fake_time();
1058        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(0));
1059
1060        let (cobalt_proxy, cobalt_stream) =
1061            create_proxy_and_stream::<fidl_fuchsia_metrics::MetricEventLoggerMarker>();
1062
1063        let inspector = Inspector::default();
1064        let inspect_node = inspector.root().create_child("telemetrytest");
1065        let inspect_metadata_node = inspect_node.create_child(METADATA_NODE_NAME);
1066        let mock_time_matrix_client = MockTimeMatrixClient::new();
1067
1068        let link_properties_state_logger = LinkPropertiesStateLogger::new(
1069            &inspect_metadata_node,
1070            &format!("root/telemetrytest/{METADATA_NODE_NAME}"),
1071            InterfaceTimeSeriesGrouping::Type(vec![
1072                InterfaceType::Ethernet,
1073                InterfaceType::WlanClient,
1074            ]),
1075            &mock_time_matrix_client,
1076        );
1077
1078        let interface_aware_logger_node = inspect_node.create_child("interfaces");
1079        let events_node = interface_aware_logger_node.create_child("events");
1080        let time_series_node = interface_aware_logger_node.create_child("time_series");
1081        let interface_aware_logger = InterfaceAwareLogger::new(
1082            &inspect_metadata_node,
1083            &format!("root/telemetrytest/{METADATA_NODE_NAME}"),
1084            InterfaceTimeSeriesGrouping::Type(vec![
1085                InterfaceType::Ethernet,
1086                InterfaceType::WlanClient,
1087            ]),
1088            events_node,
1089            time_series_node,
1090        );
1091
1092        inspect_node.record(interface_aware_logger_node);
1093
1094        let (telemetry_sender, test_fut) = serve_telemetry_inner(
1095            cobalt_proxy,
1096            inspect_node,
1097            link_properties_state_logger,
1098            interface_aware_logger,
1099        );
1100        let mut test_fut = Box::pin(test_fut);
1101
1102        assert_matches::assert_matches!(exec.run_until_stalled(&mut test_fut), Poll::Pending);
1103
1104        let test_helper = TestHelper {
1105            telemetry_sender,
1106            _inspector: inspector,
1107            cobalt_stream,
1108            cobalt_events: vec![],
1109            mock_time_matrix_client,
1110            exec,
1111        };
1112        (test_helper, test_fut)
1113    }
1114}