wlan_telemetry/
lib.rs

1// Copyright 2024 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.
4use anyhow::{format_err, Context as _, Error};
5use fuchsia_inspect::Node as InspectNode;
6use futures::channel::mpsc;
7use futures::{future, select, Future, StreamExt, TryFutureExt};
8use log::error;
9use std::boxed::Box;
10use windowed_stats::experimental::serve::serve_time_matrix_inspection;
11use wlan_common::bss::BssDescription;
12use {
13    fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211, fuchsia_async as fasync,
14    fuchsia_inspect_auto_persist as auto_persist, wlan_legacy_metrics_registry as metrics,
15};
16
17mod processors;
18pub(crate) mod util;
19pub use crate::processors::connect_disconnect::DisconnectInfo;
20pub use crate::processors::power::{IfacePowerLevel, UnclearPowerDemand};
21pub use crate::processors::scan::ScanResult;
22pub use crate::processors::toggle_events::ClientConnectionsToggleEvent;
23pub use util::sender::TelemetrySender;
24#[cfg(test)]
25mod testing;
26
27#[derive(Debug)]
28pub enum TelemetryEvent {
29    ConnectResult {
30        result: fidl_ieee80211::StatusCode,
31        bss: Box<BssDescription>,
32    },
33    Disconnect {
34        info: DisconnectInfo,
35    },
36    // We should maintain docstrings if we can see any possibility of ambiguity for an enum
37    /// Client connections enabled or disabled
38    ClientConnectionsToggle {
39        event: ClientConnectionsToggleEvent,
40    },
41    ClientIfaceCreated {
42        iface_id: u16,
43    },
44    ClientIfaceDestroyed {
45        iface_id: u16,
46    },
47    ScanStart,
48    ScanResult {
49        result: ScanResult,
50    },
51    IfacePowerLevelChanged {
52        iface_power_level: IfacePowerLevel,
53        iface_id: u16,
54    },
55    /// System suspension imminent
56    SuspendImminent,
57    /// Unclear power level requested by policy layer
58    UnclearPowerDemand(UnclearPowerDemand),
59}
60
61/// Attempts to connect to the Cobalt service.
62pub async fn setup_cobalt_proxy(
63) -> Result<fidl_fuchsia_metrics::MetricEventLoggerProxy, anyhow::Error> {
64    let cobalt_1dot1_svc = fuchsia_component::client::connect_to_protocol::<
65        fidl_fuchsia_metrics::MetricEventLoggerFactoryMarker,
66    >()
67    .context("failed to connect to metrics service")?;
68
69    let (cobalt_1dot1_proxy, cobalt_1dot1_server) =
70        fidl::endpoints::create_proxy::<fidl_fuchsia_metrics::MetricEventLoggerMarker>();
71
72    let project_spec = fidl_fuchsia_metrics::ProjectSpec {
73        customer_id: Some(metrics::CUSTOMER_ID),
74        project_id: Some(metrics::PROJECT_ID),
75        ..Default::default()
76    };
77
78    match cobalt_1dot1_svc.create_metric_event_logger(&project_spec, cobalt_1dot1_server).await {
79        Ok(_) => Ok(cobalt_1dot1_proxy),
80        Err(err) => Err(format_err!("failed to create metrics event logger: {:?}", err)),
81    }
82}
83
84/// Attempts to create a disconnected FIDL channel with types matching the Cobalt service. This
85/// allows for a fallback with a uniform code path in case of a failure to connect to Cobalt.
86pub fn setup_disconnected_cobalt_proxy(
87) -> Result<fidl_fuchsia_metrics::MetricEventLoggerProxy, anyhow::Error> {
88    // Create a disconnected proxy
89    Ok(fidl::endpoints::create_proxy::<fidl_fuchsia_metrics::MetricEventLoggerMarker>().0)
90}
91
92pub fn setup_persistence_req_sender(
93) -> Result<(auto_persist::PersistenceReqSender, impl Future<Output = ()>), anyhow::Error> {
94    fuchsia_component::client::connect_to_protocol::<
95        fidl_fuchsia_diagnostics_persist::DataPersistenceMarker,
96    >()
97    .map(auto_persist::create_persistence_req_sender)
98}
99
100/// Creates a disconnected channel with the same types as the persistence service. This allows for
101/// a fallback with a uniform code path in case of a failure to connect to the persistence service.
102pub fn setup_disconnected_persistence_req_sender() -> auto_persist::PersistenceReqSender {
103    let (sender, _receiver) = mpsc::channel::<String>(1);
104    // Note: because we drop the receiver here, be careful about log spam when sending
105    // tags through the `sender` below. This is automatically handled by `auto_persist::AutoPersist`
106    // because it only logs the first time sending fails, so just use that wrapper type instead of
107    // writing directly to this channel.
108    sender
109}
110
111/// How often to refresh time series stats. Also how often to request packet counters.
112const TELEMETRY_QUERY_INTERVAL: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(10);
113
114pub fn serve_telemetry(
115    cobalt_1dot1_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
116    monitor_svc_proxy: fidl_fuchsia_wlan_device_service::DeviceMonitorProxy,
117    inspect_node: InspectNode,
118    inspect_path: &str,
119    persistence_req_sender: auto_persist::PersistenceReqSender,
120) -> (TelemetrySender, impl Future<Output = Result<(), Error>>) {
121    let (sender, mut receiver) =
122        mpsc::channel::<TelemetryEvent>(util::sender::TELEMETRY_EVENT_BUFFER_SIZE);
123    let sender = TelemetrySender::new(sender);
124
125    // Inspect nodes to hold time series and metadata for other nodes
126    const METADATA_NODE_NAME: &str = "metadata";
127    let inspect_metadata_node = inspect_node.create_child(METADATA_NODE_NAME);
128    let inspect_time_series_node = inspect_node.create_child("time_series");
129    let driver_specific_time_series_node = inspect_time_series_node.create_child("driver_specific");
130    let driver_counters_time_series_node =
131        driver_specific_time_series_node.create_child("counters");
132    let driver_gauges_time_series_node = driver_specific_time_series_node.create_child("gauges");
133
134    let (time_matrix_client, time_series_fut) =
135        serve_time_matrix_inspection(inspect_time_series_node);
136    let (driver_counters_time_series_client, driver_counters_time_series_fut) =
137        serve_time_matrix_inspection(driver_counters_time_series_node);
138    let (driver_gauges_time_series_client, driver_gauges_time_series_fut) =
139        serve_time_matrix_inspection(driver_gauges_time_series_node);
140
141    // Create and initialize modules
142    let connect_disconnect = processors::connect_disconnect::ConnectDisconnectLogger::new(
143        cobalt_1dot1_proxy.clone(),
144        &inspect_node,
145        &inspect_metadata_node,
146        &format!("{inspect_path}/{METADATA_NODE_NAME}"),
147        persistence_req_sender,
148        &time_matrix_client,
149    );
150    let power_logger =
151        processors::power::PowerLogger::new(cobalt_1dot1_proxy.clone(), &inspect_node);
152    let mut scan_logger = processors::scan::ScanLogger::new(cobalt_1dot1_proxy.clone());
153    let mut toggle_logger =
154        processors::toggle_events::ToggleLogger::new(cobalt_1dot1_proxy.clone(), &inspect_node);
155
156    let client_iface_counters_logger =
157        processors::client_iface_counters::ClientIfaceCountersLogger::new(
158            cobalt_1dot1_proxy,
159            monitor_svc_proxy,
160            &time_matrix_client,
161            driver_counters_time_series_client,
162            driver_gauges_time_series_client,
163        );
164
165    let fut = async move {
166        // Prevent the inspect nodes from being dropped while the loop is running.
167        let _inspect_node = inspect_node;
168        let _inspect_metadata_node = inspect_metadata_node;
169        let _driver_specific_time_series_node = driver_specific_time_series_node;
170
171        let mut telemetry_interval = fasync::Interval::new(TELEMETRY_QUERY_INTERVAL);
172        loop {
173            select! {
174                event = receiver.next() => {
175                    let Some(event) = event else {
176                        error!("Telemetry event stream unexpectedly terminated.");
177                        return Err(format_err!("Telemetry event stream unexpectedly terminated."));
178                    };
179                    use TelemetryEvent::*;
180                    match event {
181                        ConnectResult { result, bss } => {
182                            connect_disconnect.handle_connect_attempt(result, &bss).await;
183                        }
184                        Disconnect { info } => {
185                            connect_disconnect.log_disconnect(&info).await;
186                            power_logger.handle_iface_disconnect(info.iface_id).await;
187                        }
188                        ClientConnectionsToggle { event } => {
189                            toggle_logger.log_toggle_event(event).await;
190                        }
191                        ClientIfaceCreated { iface_id } => {
192                            client_iface_counters_logger.handle_iface_created(iface_id).await;
193                        }
194                        ClientIfaceDestroyed { iface_id } => {
195                            client_iface_counters_logger.handle_iface_destroyed(iface_id).await;
196                            power_logger.handle_iface_destroyed(iface_id).await;
197                        }
198                        ScanStart => {
199                            scan_logger.handle_scan_start().await;
200                        }
201                        ScanResult { result } => {
202                            scan_logger.handle_scan_result(result).await;
203                        }
204                        IfacePowerLevelChanged { iface_power_level, iface_id } => {
205                            power_logger.log_iface_power_event(iface_power_level, iface_id).await;
206                        }
207                        // TODO(b/340921554): either watch for suspension directly in the library,
208                        // or plumb this from callers once suspend mechanisms are integrated
209                        SuspendImminent => {
210                            power_logger.handle_suspend_imminent().await;
211                        }
212                        UnclearPowerDemand(demand) => {
213                            power_logger.handle_unclear_power_demand(demand).await;
214                        }
215
216                    }
217                }
218                _ = telemetry_interval.next() => {
219                    connect_disconnect.handle_periodic_telemetry().await;
220                    client_iface_counters_logger.handle_periodic_telemetry().await;
221                }
222            }
223        }
224    };
225    let fut = future::try_join4(
226        fut,
227        time_series_fut,
228        driver_counters_time_series_fut,
229        driver_gauges_time_series_fut,
230    )
231    .map_ok(|((), (), (), ())| ());
232    (sender, fut)
233}