dhcp_client/
inspect.rs

1// Copyright 2025 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.
4
5use std::collections::VecDeque;
6
7use derivative::Derivative;
8use dhcp_client_core::client::DebugLogPrefix;
9use diagnostics_traits::Inspector;
10use fuchsia_async as fasync;
11use fuchsia_sync::Mutex;
12use net_types::SpecifiedAddr;
13
14pub(crate) struct Inspect {
15    inner: Mutex<InspectInner>,
16}
17
18impl Inspect {
19    pub(crate) fn new() -> Self {
20        Self { inner: Mutex::new(InspectInner::new()) }
21    }
22
23    pub(crate) fn record(&self, inspector: &mut impl Inspector) {
24        let inner = self.inner.lock();
25        inner.record(inspector);
26    }
27
28    pub(crate) fn update(
29        &self,
30        state: StateInspect,
31        lease: LeaseChangeInspect,
32        debug_log_prefix: DebugLogPrefix,
33    ) {
34        let mut inner = self.inner.lock();
35        inner.update_state(state);
36        inner.update_lease(lease, debug_log_prefix);
37    }
38}
39
40struct InspectInner {
41    current_state: StateInspect,
42    current_lease: Option<LeaseInspect>,
43    state_history: LengthLimitedVecDeque<StateHistoryInspect>,
44    lease_history: LengthLimitedVecDeque<LeaseInspect>,
45}
46
47const MAX_HISTORY_LENGTH: usize = 10;
48
49#[derive(Derivative)]
50#[derivative(Default(bound = ""))]
51struct LengthLimitedVecDeque<T> {
52    inner: VecDeque<T>,
53}
54
55impl<T> LengthLimitedVecDeque<T> {
56    fn push(&mut self, value: T) {
57        if self.inner.len() >= MAX_HISTORY_LENGTH {
58            let _: Option<T> = self.inner.pop_front();
59        }
60        self.inner.push_back(value)
61    }
62}
63
64impl InspectInner {
65    fn new() -> Self {
66        Self {
67            current_state: StateInspect::new(),
68            current_lease: None,
69            state_history: Default::default(),
70            lease_history: Default::default(),
71        }
72    }
73
74    fn record(&self, inspector: &mut impl Inspector) {
75        let Self { current_state, current_lease, state_history, lease_history } = self;
76        inspector.record_child("CurrentState", |inspector| {
77            current_state.record(inspector);
78        });
79        if let Some(current_lease) = current_lease {
80            inspector.record_child("CurrentLease", |inspector| {
81                current_lease.record(inspector);
82            });
83        }
84        inspector.record_child("StateHistory", |inspector| {
85            for entry in state_history.inner.iter() {
86                inspector.record_unnamed_child(|inspector| {
87                    entry.record(inspector);
88                });
89            }
90        });
91        inspector.record_child("LeaseHistory", |inspector| {
92            for entry in lease_history.inner.iter() {
93                inspector.record_unnamed_child(|inspector| {
94                    entry.record(inspector);
95                });
96            }
97        });
98    }
99
100    fn update_state(&mut self, state: StateInspect) {
101        let old_state_inspect = std::mem::replace(&mut self.current_state, state);
102        self.state_history.push(old_state_inspect.into());
103    }
104
105    fn update_lease(&mut self, lease: LeaseChangeInspect, debug_log_prefix: DebugLogPrefix) {
106        let Self { current_lease, lease_history, .. } = self;
107        match lease {
108            LeaseChangeInspect::NoChange => (),
109            LeaseChangeInspect::LeaseDropped => {
110                let Some(current_lease) = current_lease.take() else {
111                    log::error!(
112                        "{debug_log_prefix} recording lease drop in \
113                        inspect history with no current lease"
114                    );
115                    return;
116                };
117                lease_history.push(current_lease);
118            }
119            LeaseChangeInspect::LeaseAdded { start_time, prefix_len, properties } => {
120                if let Some(prev_lease) = current_lease.take() {
121                    lease_history.push(prev_lease);
122                }
123                *current_lease =
124                    Some(LeaseInspect { start_time, renewed_time: None, prefix_len, properties });
125            }
126            LeaseChangeInspect::LeaseRenewed { renewed_time, properties } => {
127                let Some(prev_lease) = current_lease.as_mut() else {
128                    log::error!(
129                        "{debug_log_prefix} recording lease renewal in \
130                        inspect history with no current lease"
131                    );
132                    return;
133                };
134                let start_time = prev_lease.start_time;
135                let prefix_len = prev_lease.prefix_len;
136                *prev_lease = LeaseInspect {
137                    start_time,
138                    renewed_time: Some(renewed_time),
139                    prefix_len,
140                    // Take the properties from the renewed version in case they
141                    // changed with the renewal.
142                    properties,
143                };
144            }
145        }
146    }
147}
148
149pub(crate) struct StateInspect {
150    pub(crate) state: dhcp_client_core::client::State<fasync::MonotonicInstant>,
151    pub(crate) time: fasync::MonotonicInstant,
152}
153
154impl StateInspect {
155    fn new() -> Self {
156        Self {
157            state: dhcp_client_core::client::State::default(),
158            time: fasync::MonotonicInstant::now(),
159        }
160    }
161
162    fn record(&self, inspector: &mut impl Inspector) {
163        let StateInspect { state, time } = self;
164        inspector.record_inspectable_value("State", state);
165        inspector.record_instant(diagnostics_traits::instant_property_name!("Entered"), time);
166    }
167}
168
169struct StateHistoryInspect {
170    time: fasync::MonotonicInstant,
171    state_name: &'static str,
172}
173
174impl StateHistoryInspect {
175    fn record(&self, inspector: &mut impl Inspector) {
176        let Self { state_name: state, time } = self;
177        inspector.record_str("State", state);
178        inspector.record_instant(diagnostics_traits::instant_property_name!("Entered"), time);
179    }
180}
181
182impl From<StateInspect> for StateHistoryInspect {
183    fn from(state_inspect: StateInspect) -> Self {
184        let StateInspect { state, time } = state_inspect;
185        let state_name = state.state_name();
186        Self { time, state_name }
187    }
188}
189
190pub(crate) enum LeaseChangeInspect {
191    NoChange,
192    LeaseDropped,
193    LeaseAdded {
194        start_time: fasync::MonotonicInstant,
195        /// `prefix_len` is only recorded when a lease is initially acquired
196        /// because we do not currently support updating the prefix length
197        /// attached to an address that we have installed.
198        prefix_len: u8,
199        properties: LeaseInspectProperties,
200    },
201    LeaseRenewed {
202        renewed_time: fasync::MonotonicInstant,
203        properties: LeaseInspectProperties,
204    },
205}
206
207#[derive(Clone, Copy)]
208pub(crate) struct LeaseInspectProperties {
209    pub(crate) ip_address: SpecifiedAddr<net_types::ip::Ipv4Addr>,
210    pub(crate) lease_length: fasync::MonotonicDuration,
211    pub(crate) dns_server_count: usize,
212    pub(crate) routers_count: usize,
213}
214
215#[derive(Clone, Copy)]
216pub(crate) struct LeaseInspect {
217    pub(crate) start_time: fasync::MonotonicInstant,
218    pub(crate) renewed_time: Option<fasync::MonotonicInstant>,
219    pub(crate) prefix_len: u8,
220    properties: LeaseInspectProperties,
221}
222
223impl LeaseInspect {
224    fn record(&self, inspector: &mut impl Inspector) {
225        let Self {
226            start_time,
227            renewed_time,
228            prefix_len,
229            properties:
230                LeaseInspectProperties { ip_address, lease_length, dns_server_count, routers_count },
231        } = self;
232        inspector.record_ip_addr("IpAddress", **ip_address);
233        inspector.record_instant(diagnostics_traits::instant_property_name!("Start"), start_time);
234        match renewed_time {
235            Some(renewed_time) => {
236                inspector.record_instant(
237                    diagnostics_traits::instant_property_name!("Renewed"),
238                    renewed_time,
239                );
240            }
241            None => {
242                inspector.record_str(
243                    diagnostics_traits::instant_property_name!("Renewed").into(),
244                    "None",
245                );
246            }
247        }
248        inspector.record_int("LeaseLengthSecs", lease_length.into_seconds());
249        inspector.record_usize("DnsServerCount", *dns_server_count);
250        inspector.record_uint("PrefixLen", *prefix_len);
251        inspector.record_usize("Routers", *routers_count);
252    }
253}