1mod 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, 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 pub fn send(&self, event: TelemetryEvent) {
76 match self.sender.lock().try_send(event) {
77 Ok(_) => {
78 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 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 LinkPropertiesUpdate {
136 interface_identifiers: Vec<InterfaceIdentifier>,
137 link_properties: IpVersions<LinkProperties>,
138 },
139 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
162const 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 let inspect_events_node = inspect_node.create_child("events");
175
176 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 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
262macro_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 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 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 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 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 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 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 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 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 &[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 for i in 0..=15 {
829 let link_properties = LinkProperties::from(i);
830 let mut expected_data_vec = vec![];
831 if i != 0 {
835 expected_data_vec.push(TimeMatrixCall::Fold(Timed::now(i)));
836 }
837
838 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 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 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 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 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 cobalt_events: Vec<MetricEvent>,
942 mock_time_matrix_client: MockTimeMatrixClient,
943
944 exec: fasync::TestExecutor,
946 }
947
948 impl TestHelper {
949 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 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 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}