1#![allow(unused)]
8
9use fidl_fuchsia_net_interfaces_ext::PortClass;
10use fuchsia_inspect::Node as InspectNode;
11use fuchsia_inspect_contrib::id_enum::IdEnum;
12use fuchsia_sync::Mutex;
13use std::collections::HashMap;
14use std::sync::Arc;
15use windowed_stats::experimental::clock::Timed;
16use windowed_stats::experimental::series::interpolation::LastSample;
17use windowed_stats::experimental::series::metadata::{BitSetMap, BitSetNode};
18use windowed_stats::experimental::series::statistic::Union;
19use windowed_stats::experimental::series::{SamplingProfile, TimeMatrix};
20use windowed_stats::experimental::serve::{InspectSender, InspectedTimeMatrix};
21
22use crate::{IpVersions, LinkState};
23
24#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
29pub enum InterfaceType {
30 Ethernet,
31 WlanClient,
32 WlanAp,
33 Blackhole,
34 Bluetooth,
35 Virtual,
36}
37
38impl std::fmt::Display for InterfaceType {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 let name = format!("{:?}", self);
41 write!(f, "{}", name.to_lowercase())
42 }
43}
44
45pub enum InterfaceTimeSeriesGrouping {
53 Type(Vec<InterfaceType>),
54}
55
56#[derive(Clone, Debug, Eq, Hash, PartialEq)]
61pub enum InterfaceIdentifier {
62 Type(InterfaceType),
64}
65
66impl std::fmt::Display for InterfaceIdentifier {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 let name = match self {
69 Self::Type(ty) => format!("TYPE_{ty}"),
70 };
71 write!(f, "{}", name)
72 }
73}
74
75pub fn identifiers_from_port_class(port_class: PortClass) -> Vec<InterfaceIdentifier> {
76 match port_class {
77 PortClass::Ethernet => vec![InterfaceType::Ethernet],
78 PortClass::WlanClient => vec![InterfaceType::WlanClient],
79 PortClass::WlanAp => vec![InterfaceType::WlanAp],
80 PortClass::Blackhole => vec![InterfaceType::Blackhole],
81 PortClass::Loopback => vec![InterfaceType::Bluetooth, InterfaceType::Virtual],
82 PortClass::Virtual | PortClass::Ppp | PortClass::Bridge | PortClass::Lowpan => {
83 vec![InterfaceType::Virtual]
84 }
85 }
86 .into_iter()
87 .map(|port_class| InterfaceIdentifier::Type(port_class))
88 .collect()
89}
90
91#[derive(Debug)]
92enum TimeSeriesType {
93 LinkProperties,
96 LinkState,
99}
100
101impl std::fmt::Display for TimeSeriesType {
102 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103 let name = match *self {
104 Self::LinkProperties => "link_properties",
105 Self::LinkState => "link_state",
106 };
107 write!(f, "{}", name)
108 }
109}
110
111#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
113pub struct LinkProperties {
114 pub has_address: bool,
117 pub has_default_route: bool,
118 pub has_dns: bool,
124 pub has_http_reachability: bool,
129}
130
131impl LinkProperties {
132 fn to_bitset(&self) -> u64 {
137 let mut result = 0u64;
138 if self.has_address {
139 result |= 1;
140 }
141
142 if self.has_default_route {
143 result |= 1 << 1;
144 }
145
146 if self.has_dns {
147 result |= 1 << 2;
148 }
149
150 if self.has_http_reachability {
151 result |= 1 << 3;
152 }
153 result
154 }
155}
156
157#[cfg(test)]
158impl From<u64> for LinkProperties {
159 fn from(data: u64) -> Self {
160 Self {
161 has_address: (data & 1) == 1,
162 has_default_route: (data & 1 << 1) >= 1,
163 has_dns: (data & 1 << 2) >= 1,
164 has_http_reachability: (data & 1 << 3) >= 1,
165 }
166 }
167}
168
169impl IdEnum for LinkState {
170 type Id = u8;
171 fn to_id(&self) -> Self::Id {
174 match self {
175 LinkState::None => 0,
176 LinkState::Removed => 1,
177 LinkState::Down => 2,
178 LinkState::Up => 3,
179 LinkState::Local => 4,
180 LinkState::Gateway => 5,
181 LinkState::Internet => 6,
182 }
183 }
184}
185
186#[cfg(test)]
187impl From<u64> for LinkState {
188 fn from(data: u64) -> Self {
189 match data {
190 0 => Self::None,
191 1 => Self::Removed,
192 2 => Self::Down,
193 3 => Self::Up,
194 4 => Self::Local,
195 5 => Self::Gateway,
196 6 => Self::Internet,
197 unknown => {
198 log::info!("invalid ordinal provided: {data:?}");
199 Self::None
200 }
201 }
202 }
203}
204
205fn ip_versions_time_series<S: InspectSender>(
207 client: &S,
208 inspect_metadata_path: &str,
209 series_type: TimeSeriesType,
210 identifier: InterfaceIdentifier,
211) -> IpVersions<InspectedTimeMatrix<u64>> {
212 let metadata_node = match series_type {
215 TimeSeriesType::LinkProperties => InspectMetadataNode::LINK_PROPERTIES,
216 TimeSeriesType::LinkState => InspectMetadataNode::LINK_STATE,
217 };
218 let bitset_node = BitSetNode::from_path(format!("{}/{}", inspect_metadata_path, metadata_node));
219 IpVersions {
221 ipv4: single_time_matrix(
222 client,
223 format!("{}_v4_{}", series_type, identifier),
224 bitset_node.clone(),
225 ),
226 ipv6: single_time_matrix(client, format!("{}_v6_{}", series_type, identifier), bitset_node),
227 }
228}
229
230fn single_time_matrix<S: InspectSender>(
232 client: &S,
233 time_series_name: String,
234 bitset_node: BitSetNode,
235) -> InspectedTimeMatrix<u64> {
236 client.inspect_time_matrix_with_metadata(
237 time_series_name,
238 TimeMatrix::<Union<u64>, LastSample>::new(
239 SamplingProfile::highly_granular(),
240 LastSample::or(0),
241 ),
242 bitset_node,
243 )
244}
245
246impl IpVersions<InspectedTimeMatrix<u64>> {
247 fn log_v4(&self, data: u64) {
249 self.ipv4.fold_or_log_error(Timed::now(data));
250 }
251
252 fn log_v6(&self, data: u64) {
253 self.ipv6.fold_or_log_error(Timed::now(data));
254 }
255}
256
257struct PerInterfaceTimeSeries {
260 link_properties: Arc<Mutex<IpVersions<LinkProperties>>>,
262 link_properties_time_matrix: IpVersions<InspectedTimeMatrix<u64>>,
264 link_state: Arc<Mutex<IpVersions<LinkState>>>,
266 link_state_time_matrix: IpVersions<InspectedTimeMatrix<u64>>,
268}
269
270impl PerInterfaceTimeSeries {
271 pub fn new<S: InspectSender>(
272 client: &S,
273 inspect_metadata_path: &str,
274 identifier: InterfaceIdentifier,
275 ) -> Self {
276 Self {
277 link_properties: Arc::new(Mutex::new(IpVersions::default())),
278 link_properties_time_matrix: ip_versions_time_series(
279 client,
280 inspect_metadata_path,
281 TimeSeriesType::LinkProperties,
282 identifier.clone(),
283 ),
284 link_state: Arc::new(Mutex::new(IpVersions::default())),
285 link_state_time_matrix: ip_versions_time_series(
286 client,
287 inspect_metadata_path,
288 TimeSeriesType::LinkState,
289 identifier,
290 ),
291 }
292 }
293
294 fn log_link_properties_v4(&self, link_properties: LinkProperties) {
295 self.link_properties_time_matrix.log_v4(link_properties.to_bitset());
296 }
297
298 fn log_link_properties_v6(&self, link_properties: LinkProperties) {
299 self.link_properties_time_matrix.log_v6(link_properties.to_bitset());
300 }
301
302 fn maybe_log_link_properties(&self, new: &IpVersions<LinkProperties>) {
303 let mut curr = self.link_properties.lock();
304
305 if new.ipv4 != curr.ipv4 {
311 curr.ipv4 = new.ipv4;
312 self.log_link_properties_v4(curr.ipv4);
313 }
314
315 if new.ipv6 != curr.ipv6 {
316 curr.ipv6 = new.ipv6;
317 self.log_link_properties_v6(curr.ipv6);
318 }
319 }
320
321 fn log_link_state_v4(&self, link_state: LinkState) {
322 self.link_state_time_matrix.log_v4(1 << (link_state.to_id() as u64));
323 }
324
325 fn log_link_state_v6(&self, link_state: LinkState) {
326 self.link_state_time_matrix.log_v6(1 << (link_state.to_id() as u64));
327 }
328
329 fn maybe_log_link_state(&self, new: &IpVersions<LinkState>) {
330 let mut curr = self.link_state.lock();
331
332 if new.ipv4 != curr.ipv4 {
334 curr.ipv4 = new.ipv4;
335 self.log_link_state_v4(curr.ipv4);
336 }
337
338 if new.ipv6 != curr.ipv6 {
339 curr.ipv6 = new.ipv6;
340 self.log_link_state_v6(curr.ipv6);
341 }
342 }
343}
344
345pub struct LinkPropertiesStateLogger {
347 time_series_stats: HashMap<InterfaceIdentifier, PerInterfaceTimeSeries>,
350 inspect_metadata_node: InspectMetadataNode,
351}
352
353impl LinkPropertiesStateLogger {
354 pub fn new<S: InspectSender>(
355 inspect_metadata_node: &InspectNode,
356 inspect_metadata_path: &str,
357 interface_grouping: InterfaceTimeSeriesGrouping,
358 time_matrix_client: &S,
359 ) -> Self {
360 Self {
361 time_series_stats: match interface_grouping {
363 InterfaceTimeSeriesGrouping::Type(tys) => tys.into_iter().map(|ty| {
364 let identifier = InterfaceIdentifier::Type(ty);
365 (
366 identifier.clone(),
367 PerInterfaceTimeSeries::new(
368 time_matrix_client,
369 inspect_metadata_path,
370 identifier,
371 ),
372 )
373 }),
374 }
375 .collect(),
376 inspect_metadata_node: InspectMetadataNode::new(inspect_metadata_node),
377 }
378 }
379
380 pub fn update_link_properties(
385 &self,
386 interface_identifiers: Vec<InterfaceIdentifier>,
387 link_properties: &IpVersions<LinkProperties>,
388 ) {
389 interface_identifiers.iter().for_each(|identifier| {
390 if let Some(time_series) = self.time_series_stats.get(identifier) {
391 time_series.maybe_log_link_properties(&link_properties);
392 }
393 });
394 }
395
396 pub fn update_link_state(
401 &self,
402 interface_identifiers: Vec<InterfaceIdentifier>,
403 link_state: &IpVersions<LinkState>,
404 ) {
405 interface_identifiers.iter().for_each(|identifier| {
406 if let Some(time_series) = self.time_series_stats.get(identifier) {
407 time_series.maybe_log_link_state(&link_state);
408 }
409 });
410 }
411}
412
413struct InspectMetadataNode {
416 link_properties: InspectNode,
417 link_state: InspectNode,
418}
419
420impl InspectMetadataNode {
421 const LINK_PROPERTIES: &'static str = "link_properties";
422 const LINK_STATE: &'static str = "link_state";
423
424 fn new(inspect_node: &InspectNode) -> Self {
425 let link_properties = inspect_node.create_child(Self::LINK_PROPERTIES);
426 let link_state = inspect_node.create_child(Self::LINK_STATE);
427
428 let link_properties_metadata = BitSetMap::from_ordered([
429 "has_address",
430 "has_default_route",
431 "has_dns",
432 "has_http_reachability",
433 ]);
434 let link_state_metadata = BitSetMap::from_ordered([
435 "None", "Removed", "Down", "Up", "Local", "Gateway", "Internet",
436 ]);
437
438 link_properties_metadata.record(&link_properties);
439 link_state_metadata.record(&link_state);
440
441 Self { link_properties, link_state }
442 }
443}
444
445#[cfg(test)]
446mod tests {
447 use super::*;
448 use diagnostics_assertions::{assert_data_tree, AnyBytesProperty};
449
450 use crate::telemetry::testing::setup_test;
451 use windowed_stats::experimental::serve::serve_time_matrix_inspection;
452 use windowed_stats::experimental::testing::TimeMatrixCall;
453
454 #[fuchsia::test]
455 fn test_log_time_series_metadata_to_inspect() {
456 let mut harness = setup_test();
457
458 let (client, _server) = serve_time_matrix_inspection(
459 harness.inspect_node.create_child("link_properties_state"),
460 );
461 let _link_properties_state = LinkPropertiesStateLogger::new(
462 &harness.inspect_metadata_node,
463 &harness.inspect_metadata_path,
464 InterfaceTimeSeriesGrouping::Type(vec![InterfaceType::Ethernet]),
465 &client,
466 );
467
468 let tree = harness.get_inspect_data_tree();
469 assert_data_tree!(
470 @executor harness.exec,
471 tree,
472 root: contains {
473 test_stats: contains {
474 metadata: {
475 link_properties: {
476 index: {
477 "0": "has_address",
478 "1": "has_default_route",
479 "2": "has_dns",
480 "3": "has_http_reachability",
481 },
482 },
483 link_state: {
484 index: {
485 "0": "None",
486 "1": "Removed",
487 "2": "Down",
488 "3": "Up",
489 "4": "Local",
490 "5": "Gateway",
491 "6": "Internet",
492 },
493 },
494 },
495 link_properties_state: contains {
496 link_properties_v4_TYPE_ethernet: {
497 "type": "bitset",
498 "data": AnyBytesProperty,
499 metadata: {
500 index_node_path: "root/test_stats/metadata/link_properties",
501 }
502 },
503 link_properties_v6_TYPE_ethernet: {
504 "type": "bitset",
505 "data": AnyBytesProperty,
506 metadata: {
507 index_node_path: "root/test_stats/metadata/link_properties",
508 }
509 },
510 link_state_v4_TYPE_ethernet: {
511 "type": "bitset",
512 "data": AnyBytesProperty,
513 metadata: {
514 index_node_path: "root/test_stats/metadata/link_state",
515 }
516 },
517 link_state_v6_TYPE_ethernet: {
518 "type": "bitset",
519 "data": AnyBytesProperty,
520 metadata: {
521 index_node_path: "root/test_stats/metadata/link_state",
522 }
523 }
524 }
525 }
526 }
527 )
528 }
529
530 #[fuchsia::test]
531 fn test_log_link_properties() {
532 let harness = setup_test();
533
534 let link_properties_state = LinkPropertiesStateLogger::new(
535 &harness.inspect_metadata_node,
536 &harness.inspect_metadata_path,
537 InterfaceTimeSeriesGrouping::Type(vec![InterfaceType::Ethernet]),
538 &harness.mock_time_matrix_client,
539 );
540
541 link_properties_state.update_link_properties(
544 vec![InterfaceIdentifier::Type(InterfaceType::WlanClient)],
545 &IpVersions {
546 ipv4: LinkProperties { has_address: true, ..Default::default() },
547 ipv6: LinkProperties::default(),
548 },
549 );
550
551 let mut time_matrix_calls = harness.mock_time_matrix_client.fold_buffered_samples();
555 assert_eq!(&time_matrix_calls.drain::<u64>("link_properties_v4_TYPE_ethernet")[..], &[]);
556 assert_eq!(&time_matrix_calls.drain::<u64>("link_properties_v6_TYPE_ethernet")[..], &[]);
557 assert_eq!(&time_matrix_calls.drain::<u64>("link_properties_v4_TYPE_wlanclient")[..], &[]);
558 assert_eq!(&time_matrix_calls.drain::<u64>("link_properties_v6_TYPE_wlanclient")[..], &[]);
559
560 link_properties_state.update_link_properties(
562 vec![InterfaceIdentifier::Type(InterfaceType::Ethernet)],
563 &IpVersions {
564 ipv4: LinkProperties { has_address: true, ..Default::default() },
565 ipv6: LinkProperties {
566 has_address: true,
567 has_default_route: true,
568 ..Default::default()
569 },
570 },
571 );
572
573 time_matrix_calls = harness.mock_time_matrix_client.fold_buffered_samples();
574 assert_eq!(
576 &time_matrix_calls.drain::<u64>("link_properties_v4_TYPE_ethernet")[..],
577 &[TimeMatrixCall::Fold(Timed::now(1 << 0)),]
578 );
579 assert_eq!(
582 &time_matrix_calls.drain::<u64>("link_properties_v6_TYPE_ethernet")[..],
583 &[TimeMatrixCall::Fold(Timed::now((1 << 0) | (1 << 1))),]
584 );
585
586 link_properties_state.update_link_properties(
589 vec![InterfaceIdentifier::Type(InterfaceType::Ethernet)],
590 &IpVersions {
591 ipv4: LinkProperties { has_address: true, ..Default::default() },
592 ipv6: LinkProperties {
593 has_address: true,
594 has_default_route: true,
595 has_dns: true,
596 ..Default::default()
597 },
598 },
599 );
600 time_matrix_calls = harness.mock_time_matrix_client.fold_buffered_samples();
601 assert_eq!(&time_matrix_calls.drain::<u64>("link_properties_v4_TYPE_ethernet")[..], &[]);
602 assert_eq!(
603 &time_matrix_calls.drain::<u64>("link_properties_v6_TYPE_ethernet")[..],
604 &[TimeMatrixCall::Fold(Timed::now((1 << 0) | (1 << 1) | (1 << 2))),]
605 );
606 }
607
608 #[fuchsia::test]
609 fn test_log_link_state() {
610 let harness = setup_test();
611
612 let link_properties_state = LinkPropertiesStateLogger::new(
613 &harness.inspect_metadata_node,
614 &harness.inspect_metadata_path,
615 InterfaceTimeSeriesGrouping::Type(vec![InterfaceType::Ethernet]),
616 &harness.mock_time_matrix_client,
617 );
618
619 link_properties_state.update_link_state(
622 vec![InterfaceIdentifier::Type(InterfaceType::WlanClient)],
623 &IpVersions { ipv4: Default::default(), ipv6: LinkState::Gateway },
624 );
625
626 let mut time_matrix_calls = harness.mock_time_matrix_client.fold_buffered_samples();
630 assert_eq!(&time_matrix_calls.drain::<u64>("link_state_v4_TYPE_ethernet")[..], &[]);
631 assert_eq!(&time_matrix_calls.drain::<u64>("link_state_v6_TYPE_ethernet")[..], &[]);
632 assert_eq!(&time_matrix_calls.drain::<u64>("link_properties_v4_TYPE_wlanclient")[..], &[]);
633 assert_eq!(&time_matrix_calls.drain::<u64>("link_properties_v6_TYPE_wlanclient")[..], &[]);
634
635 link_properties_state.update_link_state(
637 vec![InterfaceIdentifier::Type(InterfaceType::Ethernet)],
638 &IpVersions { ipv4: LinkState::Internet, ipv6: LinkState::Local },
639 );
640
641 time_matrix_calls = harness.mock_time_matrix_client.fold_buffered_samples();
642 assert_eq!(
643 &time_matrix_calls.drain::<u64>("link_state_v4_TYPE_ethernet")[..],
644 &[TimeMatrixCall::Fold(Timed::now(1 << LinkState::Internet.to_id())),]
645 );
646 assert_eq!(
647 &time_matrix_calls.drain::<u64>("link_state_v6_TYPE_ethernet")[..],
648 &[TimeMatrixCall::Fold(Timed::now(1 << LinkState::Local.to_id())),]
649 );
650
651 link_properties_state.update_link_state(
654 vec![InterfaceIdentifier::Type(InterfaceType::Ethernet)],
655 &IpVersions { ipv4: LinkState::Internet, ipv6: LinkState::Gateway },
656 );
657 time_matrix_calls = harness.mock_time_matrix_client.fold_buffered_samples();
658 assert_eq!(&time_matrix_calls.drain::<u64>("link_state_v4_TYPE_ethernet")[..], &[]);
659 assert_eq!(
660 &time_matrix_calls.drain::<u64>("link_state_v6_TYPE_ethernet")[..],
661 &[TimeMatrixCall::Fold(Timed::now(1 << LinkState::Gateway.to_id())),]
662 );
663 }
664}