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