1#![allow(unused)]
8
9use super::{
10 InterfaceIdentifier, InterfaceTimeSeriesGrouping, InterfaceType, identifiers_from_port_class,
11};
12
13use fidl_fuchsia_net_interfaces_ext::PortClass;
14use fuchsia_inspect::Node as InspectNode;
15use fuchsia_inspect_contrib::id_enum::IdEnum;
16use fuchsia_sync::Mutex;
17use std::collections::HashMap;
18use std::sync::Arc;
19use windowed_stats::experimental::inspect::{InspectSender, InspectedTimeMatrix};
20use windowed_stats::experimental::series::interpolation::LastSample;
21use windowed_stats::experimental::series::metadata::{BitSetMap, BitSetNode};
22use windowed_stats::experimental::series::statistic::Union;
23use windowed_stats::experimental::series::{SamplingProfile, TimeMatrix};
24
25use crate::{IpVersions, LinkState};
26
27#[derive(Debug)]
28enum TimeSeriesType {
29 LinkProperties,
32 LinkState,
35}
36
37impl std::fmt::Display for TimeSeriesType {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 let name = match *self {
40 Self::LinkProperties => "link_properties",
41 Self::LinkState => "link_state",
42 };
43 write!(f, "{}", name)
44 }
45}
46
47#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
49pub struct LinkProperties {
50 pub has_address: bool,
53 pub has_default_route: bool,
54 pub has_dns: bool,
60 pub has_http_reachability: bool,
65}
66
67impl LinkProperties {
68 fn to_bitset(&self) -> u64 {
73 let mut result = 0u64;
74 if self.has_address {
75 result |= 1;
76 }
77
78 if self.has_default_route {
79 result |= 1 << 1;
80 }
81
82 if self.has_dns {
83 result |= 1 << 2;
84 }
85
86 if self.has_http_reachability {
87 result |= 1 << 3;
88 }
89 result
90 }
91}
92
93#[cfg(test)]
94impl From<u64> for LinkProperties {
95 fn from(data: u64) -> Self {
96 Self {
97 has_address: (data & 1) == 1,
98 has_default_route: (data & 1 << 1) >= 1,
99 has_dns: (data & 1 << 2) >= 1,
100 has_http_reachability: (data & 1 << 3) >= 1,
101 }
102 }
103}
104
105impl IdEnum for LinkState {
106 type Id = u8;
107 fn to_id(&self) -> Self::Id {
110 match self {
111 LinkState::None => 0,
112 LinkState::Removed => 1,
113 LinkState::Down => 2,
114 LinkState::Up => 3,
115 LinkState::Local => 4,
116 LinkState::Gateway => 5,
117 LinkState::Internet => 6,
118 }
119 }
120}
121
122#[cfg(test)]
123impl From<u64> for LinkState {
124 fn from(data: u64) -> Self {
125 match data {
126 0 => Self::None,
127 1 => Self::Removed,
128 2 => Self::Down,
129 3 => Self::Up,
130 4 => Self::Local,
131 5 => Self::Gateway,
132 6 => Self::Internet,
133 unknown => {
134 log::info!("invalid ordinal provided: {data:?}");
135 Self::None
136 }
137 }
138 }
139}
140
141fn ip_versions_time_series<S: InspectSender>(
143 client: &S,
144 inspect_metadata_path: &str,
145 series_type: TimeSeriesType,
146 identifier: InterfaceIdentifier,
147) -> IpVersions<InspectedTimeMatrix<u64>> {
148 let metadata_node = match series_type {
151 TimeSeriesType::LinkProperties => InspectMetadataNode::LINK_PROPERTIES,
152 TimeSeriesType::LinkState => InspectMetadataNode::LINK_STATE,
153 };
154 let bitset_node = BitSetNode::from_path(format!("{}/{}", inspect_metadata_path, metadata_node));
155 IpVersions {
157 ipv4: single_time_matrix(
158 client,
159 format!("{}_v4_{}", series_type, identifier),
160 bitset_node.clone(),
161 ),
162 ipv6: single_time_matrix(client, format!("{}_v6_{}", series_type, identifier), bitset_node),
163 }
164}
165
166fn single_time_matrix<S: InspectSender>(
168 client: &S,
169 time_series_name: String,
170 bitset_node: BitSetNode,
171) -> InspectedTimeMatrix<u64> {
172 client.inspect_time_matrix_with_metadata(
173 time_series_name,
174 TimeMatrix::<Union<u64>, LastSample>::new(
175 SamplingProfile::highly_granular(),
176 LastSample::or(0),
177 ),
178 bitset_node,
179 )
180}
181
182impl IpVersions<InspectedTimeMatrix<u64>> {
183 fn log_v4(&self, data: u64) {
185 self.ipv4.fold_or_log_error(data);
186 }
187
188 fn log_v6(&self, data: u64) {
189 self.ipv6.fold_or_log_error(data);
190 }
191}
192
193struct PerInterfaceTimeSeries {
196 link_properties: Arc<Mutex<IpVersions<LinkProperties>>>,
198 link_properties_time_matrix: IpVersions<InspectedTimeMatrix<u64>>,
200 link_state: Arc<Mutex<IpVersions<LinkState>>>,
202 link_state_time_matrix: IpVersions<InspectedTimeMatrix<u64>>,
204}
205
206impl PerInterfaceTimeSeries {
207 pub fn new<S: InspectSender>(
208 client: &S,
209 inspect_metadata_path: &str,
210 identifier: InterfaceIdentifier,
211 ) -> Self {
212 Self {
213 link_properties: Arc::new(Mutex::new(IpVersions::default())),
214 link_properties_time_matrix: ip_versions_time_series(
215 client,
216 inspect_metadata_path,
217 TimeSeriesType::LinkProperties,
218 identifier.clone(),
219 ),
220 link_state: Arc::new(Mutex::new(IpVersions::default())),
221 link_state_time_matrix: ip_versions_time_series(
222 client,
223 inspect_metadata_path,
224 TimeSeriesType::LinkState,
225 identifier,
226 ),
227 }
228 }
229
230 fn log_link_properties_v4(&self, link_properties: LinkProperties) {
231 self.link_properties_time_matrix.log_v4(link_properties.to_bitset());
232 }
233
234 fn log_link_properties_v6(&self, link_properties: LinkProperties) {
235 self.link_properties_time_matrix.log_v6(link_properties.to_bitset());
236 }
237
238 fn maybe_log_link_properties(&self, new: &IpVersions<LinkProperties>) {
239 let mut curr = self.link_properties.lock();
240
241 if new.ipv4 != curr.ipv4 {
247 curr.ipv4 = new.ipv4;
248 self.log_link_properties_v4(curr.ipv4);
249 }
250
251 if new.ipv6 != curr.ipv6 {
252 curr.ipv6 = new.ipv6;
253 self.log_link_properties_v6(curr.ipv6);
254 }
255 }
256
257 fn log_link_state_v4(&self, link_state: LinkState) {
258 self.link_state_time_matrix.log_v4(1 << (link_state.to_id() as u64));
259 }
260
261 fn log_link_state_v6(&self, link_state: LinkState) {
262 self.link_state_time_matrix.log_v6(1 << (link_state.to_id() as u64));
263 }
264
265 fn maybe_log_link_state(&self, new: &IpVersions<LinkState>) {
266 let mut curr = self.link_state.lock();
267
268 if new.ipv4 != curr.ipv4 {
270 curr.ipv4 = new.ipv4;
271 self.log_link_state_v4(curr.ipv4);
272 }
273
274 if new.ipv6 != curr.ipv6 {
275 curr.ipv6 = new.ipv6;
276 self.log_link_state_v6(curr.ipv6);
277 }
278 }
279}
280
281pub struct LinkPropertiesStateLogger {
283 time_series_stats: HashMap<InterfaceIdentifier, PerInterfaceTimeSeries>,
286 inspect_metadata_node: InspectMetadataNode,
287}
288
289impl LinkPropertiesStateLogger {
290 pub fn new<S: InspectSender>(
291 inspect_metadata_node: &InspectNode,
292 inspect_metadata_path: &str,
293 interface_grouping: InterfaceTimeSeriesGrouping,
294 time_matrix_client: &S,
295 ) -> Self {
296 Self {
297 time_series_stats: match interface_grouping {
299 InterfaceTimeSeriesGrouping::Type(tys) => tys.into_iter().map(|ty| {
300 let identifier = InterfaceIdentifier::Type(ty);
301 (
302 identifier.clone(),
303 PerInterfaceTimeSeries::new(
304 time_matrix_client,
305 inspect_metadata_path,
306 identifier,
307 ),
308 )
309 }),
310 }
311 .collect(),
312 inspect_metadata_node: InspectMetadataNode::new(inspect_metadata_node),
313 }
314 }
315
316 pub fn update_link_properties(
321 &self,
322 interface_identifiers: Vec<InterfaceIdentifier>,
323 link_properties: &IpVersions<LinkProperties>,
324 ) {
325 interface_identifiers.iter().for_each(|identifier| {
326 if let Some(time_series) = self.time_series_stats.get(identifier) {
327 time_series.maybe_log_link_properties(&link_properties);
328 }
329 });
330 }
331
332 pub fn update_link_state(
337 &self,
338 interface_identifiers: Vec<InterfaceIdentifier>,
339 link_state: &IpVersions<LinkState>,
340 ) {
341 interface_identifiers.iter().for_each(|identifier| {
342 if let Some(time_series) = self.time_series_stats.get(identifier) {
343 time_series.maybe_log_link_state(&link_state);
344 }
345 });
346 }
347}
348
349struct InspectMetadataNode {
352 link_properties: InspectNode,
353 link_state: InspectNode,
354}
355
356impl InspectMetadataNode {
357 const LINK_PROPERTIES: &'static str = "link_properties";
358 const LINK_STATE: &'static str = "link_state";
359
360 fn new(inspect_node: &InspectNode) -> Self {
361 let link_properties = inspect_node.create_child(Self::LINK_PROPERTIES);
362 let link_state = inspect_node.create_child(Self::LINK_STATE);
363
364 let link_properties_metadata = BitSetMap::from_ordered([
365 "has_address",
366 "has_default_route",
367 "has_dns",
368 "has_http_reachability",
369 ]);
370 let link_state_metadata = BitSetMap::from_ordered([
371 "None", "Removed", "Down", "Up", "Local", "Gateway", "Internet",
372 ]);
373
374 link_properties_metadata.record(&link_properties);
375 link_state_metadata.record(&link_state);
376
377 Self { link_properties, link_state }
378 }
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384 use diagnostics_assertions::{AnyBytesProperty, AnyNumericProperty, assert_data_tree};
385
386 use crate::telemetry::testing::setup_test;
387 use windowed_stats::experimental::clock::Timed;
388 use windowed_stats::experimental::inspect::TimeMatrixClient;
389 use windowed_stats::experimental::testing::TimeMatrixCall;
390
391 #[fuchsia::test]
392 fn test_log_time_series_metadata_to_inspect() {
393 let mut harness = setup_test();
394
395 let client =
396 TimeMatrixClient::new(harness.inspect_node.create_child("link_properties_state"));
397 let _link_properties_state = LinkPropertiesStateLogger::new(
398 &harness.inspect_metadata_node,
399 &harness.inspect_metadata_path,
400 InterfaceTimeSeriesGrouping::Type(vec![InterfaceType::Ethernet]),
401 &client,
402 );
403
404 let tree = harness.get_inspect_data_tree();
405 assert_data_tree!(
406 @executor harness.exec,
407 tree,
408 root: contains {
409 test_stats: contains {
410 metadata: {
411 link_properties: {
412 index: {
413 "0": "has_address",
414 "1": "has_default_route",
415 "2": "has_dns",
416 "3": "has_http_reachability",
417 },
418 },
419 link_state: {
420 index: {
421 "0": "None",
422 "1": "Removed",
423 "2": "Down",
424 "3": "Up",
425 "4": "Local",
426 "5": "Gateway",
427 "6": "Internet",
428 },
429 },
430 },
431 link_properties_state: contains {
432 link_properties_v4_TYPE_ethernet: {
433 "type": "bitset",
434 "data": AnyBytesProperty,
435 metadata: {
436 index_node_path: "root/test_stats/metadata/link_properties",
437 }
438 },
439 link_properties_v6_TYPE_ethernet: {
440 "type": "bitset",
441 "data": AnyBytesProperty,
442 metadata: {
443 index_node_path: "root/test_stats/metadata/link_properties",
444 }
445 },
446 link_state_v4_TYPE_ethernet: {
447 "type": "bitset",
448 "data": AnyBytesProperty,
449 metadata: {
450 index_node_path: "root/test_stats/metadata/link_state",
451 }
452 },
453 link_state_v6_TYPE_ethernet: {
454 "type": "bitset",
455 "data": AnyBytesProperty,
456 metadata: {
457 index_node_path: "root/test_stats/metadata/link_state",
458 }
459 }
460 }
461 }
462 }
463 )
464 }
465
466 #[fuchsia::test]
467 fn test_log_link_properties() {
468 let harness = setup_test();
469
470 let link_properties_state = LinkPropertiesStateLogger::new(
471 &harness.inspect_metadata_node,
472 &harness.inspect_metadata_path,
473 InterfaceTimeSeriesGrouping::Type(vec![InterfaceType::Ethernet]),
474 &harness.mock_time_matrix_client,
475 );
476
477 link_properties_state.update_link_properties(
480 vec![InterfaceIdentifier::Type(InterfaceType::WlanClient)],
481 &IpVersions {
482 ipv4: LinkProperties { has_address: true, ..Default::default() },
483 ipv6: LinkProperties::default(),
484 },
485 );
486
487 let mut time_matrix_calls = harness.mock_time_matrix_client.drain_calls();
491 assert_eq!(&time_matrix_calls.drain::<u64>("link_properties_v4_TYPE_ethernet")[..], &[]);
492 assert_eq!(&time_matrix_calls.drain::<u64>("link_properties_v6_TYPE_ethernet")[..], &[]);
493 assert_eq!(&time_matrix_calls.drain::<u64>("link_properties_v4_TYPE_wlanclient")[..], &[]);
494 assert_eq!(&time_matrix_calls.drain::<u64>("link_properties_v6_TYPE_wlanclient")[..], &[]);
495
496 link_properties_state.update_link_properties(
498 vec![InterfaceIdentifier::Type(InterfaceType::Ethernet)],
499 &IpVersions {
500 ipv4: LinkProperties { has_address: true, ..Default::default() },
501 ipv6: LinkProperties {
502 has_address: true,
503 has_default_route: true,
504 ..Default::default()
505 },
506 },
507 );
508
509 time_matrix_calls = harness.mock_time_matrix_client.drain_calls();
510 assert_eq!(
512 &time_matrix_calls.drain::<u64>("link_properties_v4_TYPE_ethernet")[..],
513 &[TimeMatrixCall::Fold(Timed::now(1 << 0)),]
514 );
515 assert_eq!(
518 &time_matrix_calls.drain::<u64>("link_properties_v6_TYPE_ethernet")[..],
519 &[TimeMatrixCall::Fold(Timed::now((1 << 0) | (1 << 1))),]
520 );
521
522 link_properties_state.update_link_properties(
525 vec![InterfaceIdentifier::Type(InterfaceType::Ethernet)],
526 &IpVersions {
527 ipv4: LinkProperties { has_address: true, ..Default::default() },
528 ipv6: LinkProperties {
529 has_address: true,
530 has_default_route: true,
531 has_dns: true,
532 ..Default::default()
533 },
534 },
535 );
536 time_matrix_calls = harness.mock_time_matrix_client.drain_calls();
537 assert_eq!(&time_matrix_calls.drain::<u64>("link_properties_v4_TYPE_ethernet")[..], &[]);
538 assert_eq!(
539 &time_matrix_calls.drain::<u64>("link_properties_v6_TYPE_ethernet")[..],
540 &[TimeMatrixCall::Fold(Timed::now((1 << 0) | (1 << 1) | (1 << 2))),]
541 );
542 }
543
544 #[fuchsia::test]
545 fn test_log_link_state() {
546 let harness = setup_test();
547
548 let link_properties_state = LinkPropertiesStateLogger::new(
549 &harness.inspect_metadata_node,
550 &harness.inspect_metadata_path,
551 InterfaceTimeSeriesGrouping::Type(vec![InterfaceType::Ethernet]),
552 &harness.mock_time_matrix_client,
553 );
554
555 link_properties_state.update_link_state(
558 vec![InterfaceIdentifier::Type(InterfaceType::WlanClient)],
559 &IpVersions { ipv4: Default::default(), ipv6: LinkState::Gateway },
560 );
561
562 let mut time_matrix_calls = harness.mock_time_matrix_client.drain_calls();
566 assert_eq!(&time_matrix_calls.drain::<u64>("link_state_v4_TYPE_ethernet")[..], &[]);
567 assert_eq!(&time_matrix_calls.drain::<u64>("link_state_v6_TYPE_ethernet")[..], &[]);
568 assert_eq!(&time_matrix_calls.drain::<u64>("link_properties_v4_TYPE_wlanclient")[..], &[]);
569 assert_eq!(&time_matrix_calls.drain::<u64>("link_properties_v6_TYPE_wlanclient")[..], &[]);
570
571 link_properties_state.update_link_state(
573 vec![InterfaceIdentifier::Type(InterfaceType::Ethernet)],
574 &IpVersions { ipv4: LinkState::Internet, ipv6: LinkState::Local },
575 );
576
577 time_matrix_calls = harness.mock_time_matrix_client.drain_calls();
578 assert_eq!(
579 &time_matrix_calls.drain::<u64>("link_state_v4_TYPE_ethernet")[..],
580 &[TimeMatrixCall::Fold(Timed::now(1 << LinkState::Internet.to_id())),]
581 );
582 assert_eq!(
583 &time_matrix_calls.drain::<u64>("link_state_v6_TYPE_ethernet")[..],
584 &[TimeMatrixCall::Fold(Timed::now(1 << LinkState::Local.to_id())),]
585 );
586
587 link_properties_state.update_link_state(
590 vec![InterfaceIdentifier::Type(InterfaceType::Ethernet)],
591 &IpVersions { ipv4: LinkState::Internet, ipv6: LinkState::Gateway },
592 );
593 time_matrix_calls = harness.mock_time_matrix_client.drain_calls();
594 assert_eq!(&time_matrix_calls.drain::<u64>("link_state_v4_TYPE_ethernet")[..], &[]);
595 assert_eq!(
596 &time_matrix_calls.drain::<u64>("link_state_v6_TYPE_ethernet")[..],
597 &[TimeMatrixCall::Fold(Timed::now(1 << LinkState::Gateway.to_id())),]
598 );
599 }
600}