monitord/varlink/
metrics.rs

1//! This code is adapted from the code generated by `zlink-codegen` from Varlink IDL.
2
3use serde::{Deserialize, Serialize};
4use zlink::{proxy, ReplyError};
5
6/// Proxy trait for calling methods on the interface.
7#[proxy("io.systemd.Metrics")]
8pub trait Metrics {
9    /// A struct representing various metric value types. A metric can be of one type
10    /// [Requires 'more' flag]
11    #[zlink(more)]
12    async fn list(
13        &mut self,
14    ) -> zlink::Result<
15        impl futures_util::Stream<Item = zlink::Result<Result<ListOutput, MetricsError>>>,
16    >;
17    /// Method to get the metric families
18    /// [Requires 'more' flag]
19    #[zlink(more)]
20    async fn describe(
21        &mut self,
22    ) -> zlink::Result<
23        impl futures_util::Stream<Item = zlink::Result<Result<DescribeOutput, MetricsError>>>,
24    >;
25}
26
27/// Output parameters for the List method.
28#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
29pub struct ListOutput {
30    pub name: String,
31    pub value: serde_json::Value,
32    pub object: Option<String>,
33    pub fields: Option<std::collections::HashMap<String, serde_json::Value>>,
34}
35
36impl ListOutput {
37    /// Returns the name of the metric
38    pub fn name(&self) -> &str {
39        &self.name
40    }
41
42    /// Returns the name of the metric
43    pub fn name_suffix(&self) -> &str {
44        self.name
45            .rsplit_once('.')
46            .map(|(_, suffix)| suffix)
47            .unwrap_or(&self.name)
48    }
49
50    /// Returns the value of the metric
51    pub fn value(&self) -> &serde_json::Value {
52        &self.value
53    }
54
55    /// Returns the object name if present
56    pub fn object(&self) -> Option<&str> {
57        self.object.as_deref()
58    }
59
60    /// Returns the object name or empty string if not present
61    pub fn object_name(&self) -> String {
62        self.object.as_deref().unwrap_or("").to_string()
63    }
64
65    /// Returns the string value. Caller must validate value is a string first.
66    pub fn value_as_string(&self) -> &str {
67        self.value
68            .as_str()
69            .expect("value_as_string called on non-string value; validate metric type first")
70    }
71
72    /// Returns the int value. Caller must validate value is an integer first.
73    pub fn value_as_int(&self) -> i64 {
74        self.value
75            .as_i64()
76            .expect("value_as_int called on non-integer value; validate metric type first")
77    }
78
79    /// Returns the bool value. Caller must validate value is a bool first.
80    pub fn value_as_bool(&self) -> bool {
81        self.value
82            .as_bool()
83            .expect("value_as_bool called on non-boolean value; validate metric type first")
84    }
85
86    /// Returns the fields map if present
87    pub fn fields(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
88        self.fields.as_ref()
89    }
90
91    /// Extract a string field value from the fields map by field name
92    pub fn get_field_as_str(&self, field_name: &str) -> Option<&str> {
93        self.fields
94            .as_ref()
95            .and_then(|f| f.get(field_name))
96            .and_then(|v| v.as_str())
97    }
98}
99/// Output parameters for the Describe method.
100#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
101pub struct DescribeOutput {
102    pub name: String,
103    pub description: String,
104    pub r#type: MetricFamilyType,
105}
106/// An enum representing various metric family types
107#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
108#[serde(rename_all = "snake_case")]
109pub enum MetricFamilyType {
110    /// A counter metric family type which is a monotonically increasing value
111    Counter,
112    /// A gauge metric family type which is a value that can go up and down
113    Gauge,
114    /// A string metric family type
115    String,
116}
117
118/// Errors that can occur in this interface.
119#[derive(Debug, Clone, PartialEq, ReplyError)]
120#[zlink(interface = "io.systemd.Metrics")]
121pub enum MetricsError {
122    /// No such metric found
123    NoSuchMetric,
124}
125
126impl std::fmt::Display for MetricsError {
127    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128        match self {
129            MetricsError::NoSuchMetric => write!(f, "No such metric found"),
130        }
131    }
132}
133
134impl std::error::Error for MetricsError {}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_object_name_with_value() {
142        let output = ListOutput {
143            name: "test.metric".to_string(),
144            value: serde_json::Value::Null,
145            object: Some("my-service.service".to_string()),
146            fields: None,
147        };
148
149        assert_eq!(output.object_name(), "my-service.service");
150    }
151
152    #[test]
153    fn test_object_name_without_value() {
154        let output = ListOutput {
155            name: "test.metric".to_string(),
156            value: serde_json::Value::Null,
157            object: None,
158            fields: None,
159        };
160
161        assert_eq!(output.object_name(), "");
162    }
163
164    #[test]
165    fn test_object_name_with_empty_string() {
166        let output = ListOutput {
167            name: "test.metric".to_string(),
168            value: serde_json::Value::Null,
169            object: Some("".to_string()),
170            fields: None,
171        };
172
173        assert_eq!(output.object_name(), "");
174    }
175
176    #[test]
177    fn test_object_returns_option() {
178        let output_with_object = ListOutput {
179            name: "test.metric".to_string(),
180            value: serde_json::Value::Null,
181            object: Some("service.service".to_string()),
182            fields: None,
183        };
184
185        let output_without_object = ListOutput {
186            name: "test.metric".to_string(),
187            value: serde_json::Value::Null,
188            object: None,
189            fields: None,
190        };
191
192        assert_eq!(output_with_object.object(), Some("service.service"));
193        assert_eq!(output_without_object.object(), None);
194    }
195
196    #[test]
197    fn test_get_field_as_str_existing_field() {
198        let mut fields = std::collections::HashMap::new();
199        fields.insert("type".to_string(), serde_json::json!("service"));
200        fields.insert("state".to_string(), serde_json::json!("active"));
201
202        let output = ListOutput {
203            name: "test.metric".to_string(),
204            value: serde_json::Value::Null,
205            object: None,
206            fields: Some(fields),
207        };
208
209        assert_eq!(output.get_field_as_str("type"), Some("service"));
210        assert_eq!(output.get_field_as_str("state"), Some("active"));
211    }
212
213    #[test]
214    fn test_get_field_as_str_missing_field() {
215        let fields = std::collections::HashMap::new();
216
217        let output = ListOutput {
218            name: "test.metric".to_string(),
219            value: serde_json::Value::Null,
220            object: None,
221            fields: Some(fields),
222        };
223
224        assert_eq!(output.get_field_as_str("nonexistent"), None);
225    }
226
227    #[test]
228    fn test_get_field_as_str_no_fields() {
229        let output = ListOutput {
230            name: "test.metric".to_string(),
231            value: serde_json::Value::Null,
232            object: None,
233            fields: None,
234        };
235
236        assert_eq!(output.get_field_as_str("type"), None);
237    }
238
239    #[test]
240    fn test_get_field_as_str_non_string_value() {
241        let mut fields = std::collections::HashMap::new();
242        fields.insert("number".to_string(), serde_json::json!(123));
243        fields.insert("bool".to_string(), serde_json::json!(true));
244
245        let output = ListOutput {
246            name: "test.metric".to_string(),
247            value: serde_json::Value::Null,
248            object: None,
249            fields: Some(fields),
250        };
251
252        assert_eq!(output.get_field_as_str("number"), None);
253        assert_eq!(output.get_field_as_str("bool"), None);
254    }
255
256    #[test]
257    fn test_name_suffix() {
258        let output = ListOutput {
259            name: "io.systemd.unit_active_state".to_string(),
260            value: serde_json::Value::Null,
261            object: None,
262            fields: None,
263        };
264
265        assert_eq!(output.name_suffix(), "unit_active_state");
266    }
267
268    #[test]
269    fn test_name_suffix_no_dots() {
270        let output = ListOutput {
271            name: "simple_name".to_string(),
272            value: serde_json::Value::Null,
273            object: None,
274            fields: None,
275        };
276
277        assert_eq!(output.name_suffix(), "simple_name");
278    }
279
280    #[test]
281    fn test_name_suffix_empty() {
282        let output = ListOutput {
283            name: "".to_string(),
284            value: serde_json::Value::Null,
285            object: None,
286            fields: None,
287        };
288
289        assert_eq!(output.name_suffix(), "");
290    }
291
292    #[test]
293    fn test_value_as_string_with_value() {
294        let output = ListOutput {
295            name: "test.metric".to_string(),
296            value: serde_json::json!("active"),
297            object: None,
298            fields: None,
299        };
300
301        assert_eq!(output.value_as_string(), "active");
302    }
303
304    #[test]
305    fn test_value_as_string_empty_string() {
306        let output = ListOutput {
307            name: "test.metric".to_string(),
308            value: serde_json::json!(""),
309            object: None,
310            fields: None,
311        };
312
313        assert_eq!(output.value_as_string(), "");
314    }
315
316    #[test]
317    fn test_value_as_int_with_value() {
318        let output = ListOutput {
319            name: "test.metric".to_string(),
320            value: serde_json::json!(42),
321            object: None,
322            fields: None,
323        };
324
325        assert_eq!(output.value_as_int(), 42);
326    }
327
328    #[test]
329    #[should_panic(expected = "value_as_int called on non-integer value")]
330    fn test_value_as_int_without_value() {
331        let output = ListOutput {
332            name: "test.metric".to_string(),
333            value: serde_json::Value::Null,
334            object: None,
335            fields: None,
336        };
337
338        output.value_as_int();
339    }
340
341    #[test]
342    fn test_value_as_int_zero() {
343        let output = ListOutput {
344            name: "test.metric".to_string(),
345            value: serde_json::json!(0),
346            object: None,
347            fields: None,
348        };
349
350        assert_eq!(output.value_as_int(), 0);
351    }
352
353    #[test]
354    fn test_value_as_int_negative() {
355        let output = ListOutput {
356            name: "test.metric".to_string(),
357            value: serde_json::json!(-5),
358            object: None,
359            fields: None,
360        };
361
362        assert_eq!(output.value_as_int(), -5);
363    }
364
365    #[test]
366    fn test_value_as_int_large_number() {
367        let output = ListOutput {
368            name: "test.metric".to_string(),
369            value: serde_json::json!(9999999999_i64),
370            object: None,
371            fields: None,
372        };
373
374        assert_eq!(output.value_as_int(), 9999999999);
375    }
376
377    #[test]
378    fn test_value_as_bool_true() {
379        let output = ListOutput {
380            name: "test.metric".to_string(),
381            value: serde_json::json!(true),
382            object: None,
383            fields: None,
384        };
385
386        assert_eq!(output.value_as_bool(), true);
387    }
388
389    #[test]
390    fn test_value_as_bool_false() {
391        let output = ListOutput {
392            name: "test.metric".to_string(),
393            value: serde_json::json!(false),
394            object: None,
395            fields: None,
396        };
397
398        assert_eq!(output.value_as_bool(), false);
399    }
400
401    #[test]
402    #[should_panic(expected = "value_as_bool called on non-boolean value")]
403    fn test_value_as_bool_none() {
404        let output = ListOutput {
405            name: "test.metric".to_string(),
406            value: serde_json::Value::Null,
407            object: None,
408            fields: None,
409        };
410
411        output.value_as_bool();
412    }
413}