Skip to main content

fuchsia_inspect/
health.rs

1// Copyright 2020 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
5//! # Health-checking inspect node.
6//!
7//! Health installs a health checking inspect node.  This node reports the current program's health
8//! status in the form of an enumeration string and, in case of an unhealthy status, a free-form
9//! message.
10//!
11//! Possible statuses are as follows:
12//!
13//! - `OK`, the Node is HEALTHY
14//! - `STARTING_UP`, the node is not yet HEALTHY
15//! - `UNHEALTHY`, the node is NOT HEALTHY (the program is required to provide a status message).
16//! - any other value, the node is NOT HEALTHY.
17//!
18//! # Usage
19//!
20//! To use the health checker one must first obtain a `fuchsia_inspect::Node` to add the health
21//! information into. Once that is available, use `fuchsia_inspect::health::Node::new(...)` to
22//! add a standardized health checker.
23//!
24//! # Examples
25//!
26//! ```
27//! use fuchsia_inspect as inspect;
28//! use fuchsia_inspect::health;
29//!
30//! let inspector = /* the inspector of your choice */
31//! let mut root = inspector.root();  // Or perhaps a different Inspect Node of your choice.
32//! let mut health = health::Node::new(root);
33//!
34//! health.set_ok();
35//! // ...
36//! health.set_unhealthy("I am not feeling well."); // Report an error
37//! // ...
38//! health.set_ok(); // The component is healthy again.
39//! ```
40
41use super::{InspectType, Property, StringProperty};
42use injectable_time::TimeSource;
43use std::fmt;
44
45#[cfg(not(target_os = "fuchsia"))]
46use injectable_time::UtcInstant as TimeType;
47
48#[cfg(target_os = "fuchsia")]
49use injectable_time::MonotonicInstant as TimeType;
50
51/// A trait of a standardized health checker.
52///
53/// Contains the methods to set standardized health status.  All standardized health status reporters
54/// must implement this trait.
55pub trait Reporter {
56    /// Sets the health status to `STARTING_UP`.
57    fn set_starting_up(&mut self);
58    /// Sets the health status to `OK`.
59    fn set_ok(&mut self);
60    /// Sets the health status to `UNHEALTHY`.  A `message` that explains why the node is unhealthy
61    /// MUST be given.
62    fn set_unhealthy(&mut self, message: &str);
63}
64
65// The metric node name, as exposed by the health checker.
66const FUCHSIA_INSPECT_HEALTH: &str = "fuchsia.inspect.Health";
67
68const STATUS_PROPERTY_KEY: &str = "status";
69
70const MESSAGE_PROPERTY_KEY: &str = "message";
71
72/// Predefined statuses, per the Inspect health specification.  Note that the specification
73/// also allows custom string statuses.
74#[derive(Debug, PartialEq, Eq)]
75enum Status {
76    /// The health checker is available, but has not been initialized with program status yet.
77    StartingUp,
78    /// The program reports unhealthy status.  The program MUST provide a status message if reporting
79    /// unhealthy.
80    Unhealthy,
81    /// The program reports healthy operation.  The definition of healthy is up to the program.
82    Ok,
83}
84
85impl fmt::Display for Status {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        match self {
88            Status::StartingUp => write!(f, "STARTING_UP"),
89            Status::Unhealthy => write!(f, "UNHEALTHY"),
90            Status::Ok => write!(f, "OK"),
91        }
92    }
93}
94
95/// Contains subsystem health information.  A global instance of Health is used implicitly
96/// if the user calls the functions `init()`, `ok()` and `unhealthy(...)`.
97///
98/// Use as: ```fuchsia_inspect::health::Node``.
99#[derive(Debug)]
100pub struct Node {
101    // The generic inspect node that hosts the health metric.
102    node: super::Node,
103
104    // The health status of the property
105    status: StringProperty,
106
107    // The detailed status message, filled out in case the health status is not OK.
108    message: Option<StringProperty>,
109}
110
111impl InspectType for Node {
112    fn into_recorded(self) -> crate::writer::types::RecordedInspectType {
113        crate::writer::types::RecordedInspectType::Boxed(Box::new(self))
114    }
115}
116
117impl Reporter for Node {
118    /// Sets the health status to `STARTING_UP`.
119    fn set_starting_up(&mut self) {
120        self.set_status_enum(Status::StartingUp, None);
121    }
122
123    /// Sets the health status to `OK`.
124    fn set_ok(&mut self) {
125        self.set_status_enum(Status::Ok, None);
126    }
127
128    /// Sets the health status to `UNHEALTHY`.  A `message` that explains why the node is healthy
129    /// MUST be given.
130    fn set_unhealthy(&mut self, message: &str) {
131        self.set_status_enum(Status::Unhealthy, Some(message));
132    }
133}
134
135impl Node {
136    /// Creates a new health checking node as a child of `parent`.  The initial observed state
137    /// is `STARTING_UP`, and remains so until the programs call one of `set_ok` or `set_unhealthy`.
138    pub fn new(parent: &super::Node) -> Self {
139        Self::new_internal(parent, TimeType::new())
140    }
141
142    // Creates a health node using a specified timestamp. Useful for tests.
143    #[cfg(test)]
144    pub fn new_with_timestamp<T: TimeSource>(parent: &super::Node, timestamper: T) -> Self {
145        Self::new_internal(parent, timestamper)
146    }
147
148    fn new_internal<T: TimeSource>(parent: &super::Node, timestamper: T) -> Self {
149        let node = parent.create_child(FUCHSIA_INSPECT_HEALTH);
150        node.record_int("start_timestamp_nanos", timestamper.now());
151        let status = node.create_string(STATUS_PROPERTY_KEY, Status::StartingUp.to_string());
152        let message = None;
153        Node { node, status, message }
154    }
155
156    // Sets the health status from the supplied `status` and `message`.  Panics if setting invalid
157    // status, e.g. setting `UNHEALTHY` without a message.
158    fn set_status_enum(&mut self, status: Status, message: Option<&str>) {
159        assert!(status != Status::Unhealthy || message.is_some(), "UNHEALTHY must have a message.");
160        self.set_status(&status.to_string(), message);
161    }
162
163    // Sets an arbitrary status and an arbitrary (optional) message into the health report.
164    // Prefer setting standard status using one of the predefined API methods.  This one will
165    // allow you to set whatever you want.
166    fn set_status(&mut self, status: &str, message: Option<&str>) {
167        self.status.set(status);
168        match (&self.message, message) {
169            (_, None) => self.message = None,
170            (Some(m), Some(n)) => m.set(n),
171            (None, Some(n)) => {
172                self.message = Some(self.node.create_string(MESSAGE_PROPERTY_KEY, n))
173            }
174        }
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use crate::Inspector;
182    use diagnostics_assertions::assert_data_tree;
183    use injectable_time::FakeTime;
184
185    #[fuchsia::test]
186    async fn health_checker_lifecycle() {
187        let inspector = Inspector::default();
188        let root = inspector.root();
189        // In the beginning, the inspector has no stats.
190        assert_data_tree!(inspector, root: contains {});
191
192        let fake_time = FakeTime::new();
193        fake_time.set_ticks(42);
194        let mut health = Node::new_with_timestamp(root, fake_time);
195        assert_data_tree!(inspector,
196        root: contains {
197            "fuchsia.inspect.Health": {
198                status: "STARTING_UP",
199                start_timestamp_nanos: 42i64,
200            }
201        });
202
203        health.set_ok();
204        assert_data_tree!(inspector,
205        root: contains {
206            "fuchsia.inspect.Health": {
207                status: "OK",
208                start_timestamp_nanos: 42i64,
209            }
210        });
211
212        health.set_unhealthy("Bad state");
213        assert_data_tree!(inspector,
214        root: contains {
215            "fuchsia.inspect.Health": {
216                status: "UNHEALTHY",
217                message: "Bad state",
218                start_timestamp_nanos: 42i64,
219            }
220        });
221
222        // Verify that the message changes.
223        health.set_unhealthy("Another bad state");
224        assert_data_tree!(inspector,
225        root: contains {
226            "fuchsia.inspect.Health": {
227                status: "UNHEALTHY",
228                message: "Another bad state",
229                start_timestamp_nanos: 42i64,
230            }
231        });
232
233        // Also verifies that there is no more message.
234        health.set_ok();
235        assert_data_tree!(inspector,
236        root: contains {
237            "fuchsia.inspect.Health": {
238                status: "OK",
239                start_timestamp_nanos: 42i64,
240            }
241        });
242
243        // Revert to STARTING_UP, but only for tests.
244        health.set_starting_up();
245        assert_data_tree!(inspector,
246        root: contains {
247            "fuchsia.inspect.Health": {
248                status: "STARTING_UP",
249                start_timestamp_nanos: 42i64,
250            }
251        });
252    }
253
254    #[fuchsia::test]
255    async fn health_is_recordable() {
256        let inspector = Inspector::default();
257        let root = inspector.root();
258
259        let fake_time = FakeTime::new();
260        fake_time.set_ticks(42);
261        {
262            let mut health = Node::new_with_timestamp(root, fake_time);
263            health.set_ok();
264            assert_data_tree!(inspector,
265                root: contains {
266                    "fuchsia.inspect.Health": {
267                        status: "OK",
268                        start_timestamp_nanos: 42i64,
269                    }
270            });
271
272            root.record(health);
273        }
274
275        assert_data_tree!(inspector,
276            root: contains {
277                "fuchsia.inspect.Health": {
278                    status: "OK",
279                    start_timestamp_nanos: 42i64,
280                }
281        });
282    }
283}