1use anyhow::{Context as _, Error, format_err};
5use fidl_fuchsia_power_battery as fidl_battery;
6use fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211;
7use fidl_fuchsia_wlan_internal as fidl_internal;
8use fuchsia_async as fasync;
9use fuchsia_inspect::Node as InspectNode;
10use futures::channel::mpsc;
11use futures::{Future, StreamExt, select};
12use log::error;
13use std::boxed::Box;
14use std::sync::Arc;
15use windowed_stats::experimental::inspect::TimeMatrixClient;
16use wlan_common::bss::BssDescription;
17use wlan_legacy_metrics_registry as metrics;
18
19mod config;
20mod convert;
21mod processors;
22pub(crate) mod util;
23pub use crate::config::{CobaltAllowlist, DeviceMobility, TelemetryConfig};
24pub use crate::processors::connect_disconnect::DisconnectInfo;
25pub use crate::processors::pno_scan::PnoScanDisabledReason;
26pub use crate::processors::power::{IfacePowerLevel, UnclearPowerDemand};
27pub use crate::processors::scan::ScanResult;
28pub use crate::processors::sme_timeout::TimeoutSource;
29pub use crate::processors::toggle_events::ClientConnectionsToggleEvent;
30pub use util::sender::TelemetrySender;
31#[cfg(test)]
32mod testing;
33
34#[derive(Debug)]
35pub enum TelemetryEvent {
36 ConnectResult {
37 result: fidl_ieee80211::StatusCode,
38 bss: Box<BssDescription>,
39 is_credential_rejected: bool,
40 is_owe_transition: bool,
41 },
42 Disconnect {
43 info: DisconnectInfo,
44 },
45 ChannelSwitched {
46 channel: wlan_common::channel::Channel,
47 },
48 ClientConnectionsToggle {
51 event: ClientConnectionsToggleEvent,
52 },
53 ClientIfaceCreated {
54 iface_id: u16,
55 },
56 ClientIfaceDestroyed {
57 iface_id: u16,
58 },
59 IfaceCreationFailure,
60 IfaceDestructionFailure,
61 ScanStart,
62 ScanResult {
63 result: ScanResult,
64 },
65 IfacePowerLevelChanged {
66 iface_power_level: IfacePowerLevel,
67 iface_id: u16,
68 },
69 SuspendImminent,
71 UnclearPowerDemand(UnclearPowerDemand),
73 BatteryChargeStatus(fidl_battery::ChargeStatus),
74 RecoveryEvent,
75 RecoveryResult {
76 result: Result<(), ()>,
77 },
78 SmeTimeout {
79 source: TimeoutSource,
80 },
81 ChipPowerUpFailure,
82 ResetTxPowerScenario,
83 SetTxPowerScenario {
84 scenario: fidl_internal::TxPowerScenario,
85 },
86 PnoScanFailure,
87 PnoScanEnabled,
88 PnoScanResultsReceived,
89 PnoScanDisabled {
90 reason: PnoScanDisabledReason,
91 },
92}
93
94pub struct ThrottledErrorLogger {
99 time_of_last_log: fasync::MonotonicInstant,
100 pub suppressed_errors: std::collections::HashMap<String, usize>,
101 minutes_between_reports: i64,
102}
103
104impl ThrottledErrorLogger {
105 pub fn new(minutes_between_reports: i64) -> Self {
106 Self {
107 time_of_last_log: fasync::MonotonicInstant::from_nanos(0),
108 suppressed_errors: std::collections::HashMap::new(),
109 minutes_between_reports,
110 }
111 }
112
113 pub fn throttle_log(&mut self, message: String, level: log::Level) {
114 let curr_time = fasync::MonotonicInstant::now();
115 let time_since_last_log = curr_time - self.time_of_last_log;
116
117 if time_since_last_log.into_minutes() > self.minutes_between_reports {
118 log::log!(level, "{}", message);
119 if !self.suppressed_errors.is_empty() {
120 for (log, count) in self.suppressed_errors.iter() {
121 log::warn!("Suppressed {} instances: {}", count, log);
122 }
123 self.suppressed_errors.clear();
124 }
125 self.time_of_last_log = curr_time;
126 } else {
127 let count = self.suppressed_errors.entry(message).or_default();
128 *count += 1;
129 }
130 }
131
132 pub fn throttle_error(&mut self, result: Result<(), Error>) {
133 if let Err(e) = result {
134 self.throttle_log(e.to_string(), log::Level::Error);
135 }
136 }
137}
138
139pub async fn setup_cobalt_proxy()
141-> Result<fidl_fuchsia_metrics::MetricEventLoggerProxy, anyhow::Error> {
142 let cobalt_svc = fuchsia_component::client::connect_to_protocol::<
143 fidl_fuchsia_metrics::MetricEventLoggerFactoryMarker,
144 >()
145 .context("failed to connect to metrics service")?;
146
147 let (cobalt_proxy, cobalt_server) =
148 fidl::endpoints::create_proxy::<fidl_fuchsia_metrics::MetricEventLoggerMarker>();
149
150 let project_spec = fidl_fuchsia_metrics::ProjectSpec {
151 customer_id: Some(metrics::CUSTOMER_ID),
152 project_id: Some(metrics::PROJECT_ID),
153 ..Default::default()
154 };
155
156 match cobalt_svc.create_metric_event_logger(&project_spec, cobalt_server).await {
157 Ok(_) => Ok(cobalt_proxy),
158 Err(err) => Err(format_err!("failed to create metrics event logger: {:?}", err)),
159 }
160}
161
162pub fn setup_disconnected_cobalt_proxy()
165-> Result<fidl_fuchsia_metrics::MetricEventLoggerProxy, anyhow::Error> {
166 Ok(fidl::endpoints::create_proxy::<fidl_fuchsia_metrics::MetricEventLoggerMarker>().0)
168}
169
170const TELEMETRY_QUERY_INTERVAL: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(10);
172
173pub fn serve_telemetry(
174 cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
175 monitor_svc_proxy: fidl_fuchsia_wlan_device_service::DeviceMonitorProxy,
176 inspect_node: InspectNode,
177 inspect_path: &str,
178 config: TelemetryConfig,
179 allowlist: CobaltAllowlist,
180) -> (TelemetrySender, impl Future<Output = Result<(), Error>> + use<>) {
181 let (sender, mut receiver) =
182 mpsc::channel::<TelemetryEvent>(util::sender::TELEMETRY_EVENT_BUFFER_SIZE);
183 let sender = TelemetrySender::new(sender);
184
185 let cobalt_logger =
186 Arc::new(util::cobalt_logger::FilteredCobaltLogger::new(cobalt_proxy, allowlist));
187
188 const METADATA_NODE_NAME: &str = "metadata";
190 let inspect_metadata_node = inspect_node.create_child(METADATA_NODE_NAME);
191 let inspect_metadata_path = format!("{inspect_path}/{METADATA_NODE_NAME}");
192 let inspect_time_series_node = inspect_node.create_child("time_series");
193
194 let time_matrix_client = TimeMatrixClient::new(inspect_time_series_node.clone_weak());
195
196 let connect_disconnect = config.enable_connect_disconnect.then(|| {
198 processors::connect_disconnect::ConnectDisconnectLogger::new(
199 cobalt_logger.clone(),
200 &inspect_node,
201 &inspect_metadata_node,
202 &inspect_metadata_path,
203 &time_matrix_client,
204 config.device_mobility,
205 )
206 });
207 let iface_logger = config
208 .enable_iface_logger
209 .then(|| processors::iface::IfaceLogger::new(cobalt_logger.clone()));
210 let power_logger = config
211 .enable_power_logger
212 .then(|| processors::power::PowerLogger::new(cobalt_logger.clone(), &inspect_node));
213 let recovery_logger = config
214 .enable_recovery_logger
215 .then(|| processors::recovery::RecoveryLogger::new(cobalt_logger.clone()));
216 let mut scan_logger = config
217 .enable_scan_logger
218 .then(|| processors::scan::ScanLogger::new(cobalt_logger.clone(), &time_matrix_client));
219 let mut pno_scan_logger = config
220 .enable_pno_scan_logger
221 .then(|| processors::pno_scan::PnoScanLogger::new(cobalt_logger.clone()));
222 let sme_timeout_logger = config
223 .enable_sme_timeout_logger
224 .then(|| processors::sme_timeout::SmeTimeoutLogger::new(cobalt_logger.clone()));
225 let mut toggle_logger = config.enable_toggle_logger.then(|| {
226 processors::toggle_events::ToggleLogger::new(cobalt_logger.clone(), &inspect_node)
227 });
228 let tx_power_scenario_logger = config
229 .enable_tx_power_scenario_logger
230 .then(|| processors::tx_power_scenario::TxPowerScenarioLogger::new(cobalt_logger.clone()));
231
232 let client_iface_counters_logger = config.enable_client_iface_counters_logger.then(|| {
233 let driver_specific_time_series_node =
234 inspect_time_series_node.create_child("driver_specific");
235 let driver_counters_time_series_node =
236 driver_specific_time_series_node.create_child("counters");
237 let driver_gauges_time_series_node =
238 driver_specific_time_series_node.create_child("gauges");
239
240 let driver_counters_time_series_client =
241 TimeMatrixClient::new(driver_counters_time_series_node.clone_weak());
242 let driver_gauges_time_series_client =
243 TimeMatrixClient::new(driver_gauges_time_series_node.clone_weak());
244
245 inspect_time_series_node.record(driver_specific_time_series_node);
246 inspect_time_series_node.record(driver_counters_time_series_node);
247 inspect_time_series_node.record(driver_gauges_time_series_node);
248
249 processors::client_iface_counters::ClientIfaceCountersLogger::new(
250 cobalt_logger.clone(),
251 monitor_svc_proxy,
252 &inspect_metadata_node,
253 &inspect_metadata_path,
254 &time_matrix_client,
255 driver_counters_time_series_client,
256 driver_gauges_time_series_client,
257 )
258 });
259
260 let fut = async move {
261 let _inspect_node = inspect_node;
263 let _inspect_metadata_node = inspect_metadata_node;
264 let _inspect_time_series_node = inspect_time_series_node;
265
266 let mut telemetry_interval = fasync::Interval::new(TELEMETRY_QUERY_INTERVAL);
267 loop {
268 select! {
269 event = receiver.next() => {
270 let Some(event) = event else {
271 error!("Telemetry event stream unexpectedly terminated.");
272 return Err(format_err!("Telemetry event stream unexpectedly terminated."));
273 };
274 use TelemetryEvent::*;
275 match event {
276 ConnectResult { result, bss, is_credential_rejected, is_owe_transition } => {
277 if let Some(ref connect_disconnect) = connect_disconnect {
278 connect_disconnect.handle_connect_attempt(result, &bss, is_credential_rejected, is_owe_transition).await;
279 }
280 }
281 Disconnect { info } => {
282 if let Some(ref connect_disconnect) = connect_disconnect {
283 connect_disconnect.log_disconnect(&info).await;
284 }
285 if let Some(ref power_logger) = power_logger {
286 power_logger.handle_iface_disconnect(info.iface_id).await;
287 }
288 }
289 ChannelSwitched { channel } => {
290 if let Some(ref connect_disconnect) = connect_disconnect {
291 connect_disconnect.handle_channel_switched(channel).await;
292 }
293 }
294 ClientConnectionsToggle { event } => {
295 if let Some(ref connect_disconnect) = connect_disconnect {
296 connect_disconnect.handle_client_connections_toggle(&event).await;
297 }
298 if let Some(ref mut toggle_logger) = toggle_logger {
299 toggle_logger.handle_toggle_event(event).await;
300 }
301 }
302 ClientIfaceCreated { iface_id } => {
303 if let Some(ref client_iface_counters_logger) = client_iface_counters_logger {
304 client_iface_counters_logger.handle_iface_created(iface_id).await;
305 }
306 }
307 ClientIfaceDestroyed { iface_id } => {
308 if let Some(ref connect_disconnect) = connect_disconnect {
309 connect_disconnect.handle_iface_destroyed().await;
310 }
311 if let Some(ref client_iface_counters_logger) = client_iface_counters_logger {
312 client_iface_counters_logger.handle_iface_destroyed(iface_id).await;
313 }
314 if let Some(ref power_logger) = power_logger {
315 power_logger.handle_iface_destroyed(iface_id).await;
316 }
317 }
318 IfaceCreationFailure => {
319 if let Some(ref iface_logger) = iface_logger {
320 iface_logger.handle_iface_creation_failure().await;
321 }
322 }
323 IfaceDestructionFailure => {
324 if let Some(ref iface_logger) = iface_logger {
325 iface_logger.handle_iface_destruction_failure().await;
326 }
327 }
328 ScanStart => {
329 if let Some(ref mut scan_logger) = scan_logger {
330 scan_logger.handle_scan_start().await;
331 }
332 }
333 ScanResult { result } => {
334 if let Some(ref mut scan_logger) = scan_logger {
335 scan_logger.handle_scan_result(result).await;
336 }
337 }
338 IfacePowerLevelChanged { iface_power_level, iface_id } => {
339 if let Some(ref power_logger) = power_logger {
340 power_logger.log_iface_power_event(iface_power_level, iface_id).await;
341 }
342 }
343 SuspendImminent => {
346 if let Some(ref power_logger) = power_logger {
347 power_logger.handle_suspend_imminent().await;
348 }
349 if let Some(ref connect_disconnect) = connect_disconnect {
350 connect_disconnect.handle_suspend_imminent().await;
351 }
352 }
353 UnclearPowerDemand(demand) => {
354 if let Some(ref power_logger) = power_logger {
355 power_logger.handle_unclear_power_demand(demand).await;
356 }
357 }
358 ChipPowerUpFailure => {
359 if let Some(ref power_logger) = power_logger {
360 power_logger.handle_chip_power_up_failure().await;
361 }
362 if let Some(ref connect_disconnect) = connect_disconnect {
363 connect_disconnect.handle_client_connections_failed_to_start().await;
364 }
365 }
366 BatteryChargeStatus(charge_status) => {
367 if let Some(ref mut scan_logger) = scan_logger {
368 scan_logger.handle_battery_charge_status(charge_status).await;
369 }
370 if let Some(ref mut toggle_logger) = toggle_logger {
371 toggle_logger.handle_battery_charge_status(charge_status).await;
372 }
373 }
374 RecoveryEvent => {
375 if let Some(ref recovery_logger) = recovery_logger {
376 recovery_logger.handle_recovery_event().await;
377 }
378 }
379 RecoveryResult { result } => {
380 if let Some(ref recovery_logger) = recovery_logger {
381 recovery_logger.handle_recovery_result(result).await;
382 }
383 }
384 SmeTimeout { source } => {
385 if let Some(ref sme_timeout_logger) = sme_timeout_logger {
386 sme_timeout_logger.handle_sme_timeout_event(source).await;
387 }
388 }
389 ResetTxPowerScenario => {
390 if let Some(ref tx_power_scenario_logger) = tx_power_scenario_logger {
391 tx_power_scenario_logger.handle_sar_reset().await;
392 }
393 }
394 SetTxPowerScenario {scenario} => {
395 if let Some(ref tx_power_scenario_logger) = tx_power_scenario_logger {
396 tx_power_scenario_logger.handle_set_sar(scenario).await;
397 }
398 }
399 PnoScanFailure => {
400 if let Some(ref connect_disconnect) = connect_disconnect {
401 connect_disconnect.handle_pno_scan_failure().await;
402 }
403 }
404 PnoScanEnabled => {
405 if let Some(ref mut pno_scan_logger) = pno_scan_logger {
406 let is_connected = connect_disconnect
407 .as_ref()
408 .map(|cd| cd.is_connected())
409 .unwrap_or(false);
410 pno_scan_logger.handle_pno_scan_enabled(is_connected).await;
411 }
412 }
413 PnoScanResultsReceived => {
414 if let Some(ref mut pno_scan_logger) = pno_scan_logger {
415 pno_scan_logger.handle_pno_scan_results_received().await;
416 }
417 }
418 PnoScanDisabled { reason } => {
419 if let Some(ref mut pno_scan_logger) = pno_scan_logger {
420 pno_scan_logger.handle_pno_scan_disabled(reason).await;
421 }
422 }
423 }
424 }
425 _ = telemetry_interval.next() => {
426 if let Some(ref connect_disconnect) = connect_disconnect {
427 connect_disconnect.handle_periodic_telemetry().await;
428 }
429 if let Some(ref client_iface_counters_logger) = client_iface_counters_logger {
430 client_iface_counters_logger.handle_periodic_telemetry().await;
431 }
432 if let Some(ref mut pno_scan_logger) = pno_scan_logger {
433 pno_scan_logger.handle_periodic_telemetry().await;
434 }
435 }
436 }
437 }
438 };
439 (sender, fut)
440}