vector/sources/host_metrics/
filesystem.rs

1use futures::StreamExt;
2use heim::units::information::byte;
3#[cfg(not(windows))]
4use heim::units::ratio::ratio;
5use vector_lib::{configurable::configurable_component, metric_tags};
6
7use super::{FilterList, HostMetrics, default_all_devices, example_devices, filter_result};
8use crate::internal_events::{HostMetricsScrapeDetailError, HostMetricsScrapeFilesystemError};
9
10/// Options for the filesystem metrics collector.
11#[configurable_component]
12#[derive(Clone, Debug, Default)]
13pub struct FilesystemConfig {
14    /// Lists of device name patterns to include or exclude in gathering
15    /// usage metrics.
16    #[serde(default = "default_all_devices")]
17    #[configurable(metadata(docs::examples = "example_devices()"))]
18    devices: FilterList,
19
20    /// Lists of filesystem name patterns to include or exclude in gathering
21    /// usage metrics.
22    #[serde(default = "default_all_devices")]
23    #[configurable(metadata(docs::examples = "example_filesystems()"))]
24    filesystems: FilterList,
25
26    /// Lists of mount point path patterns to include or exclude in gathering
27    /// usage metrics.
28    #[serde(default = "default_all_devices")]
29    #[configurable(metadata(docs::examples = "example_mountpoints()"))]
30    mountpoints: FilterList,
31}
32
33fn example_filesystems() -> FilterList {
34    FilterList {
35        includes: Some(vec!["ntfs".try_into().unwrap()]),
36        excludes: Some(vec!["ext*".try_into().unwrap()]),
37    }
38}
39
40fn example_mountpoints() -> FilterList {
41    FilterList {
42        includes: Some(vec!["/home".try_into().unwrap()]),
43        excludes: Some(vec!["/raid*".try_into().unwrap()]),
44    }
45}
46
47impl HostMetrics {
48    pub async fn filesystem_metrics(&self, output: &mut super::MetricsBuffer) {
49        output.name = "filesystem";
50        match heim::disk::partitions().await {
51            Ok(partitions) => {
52                for (partition, usage) in partitions
53                    .filter_map(|result| {
54                        filter_result(result, "Failed to load/parse partition data.")
55                    })
56                    // Filter on configured mountpoints
57                    .map(|partition| {
58                        self.config
59                            .filesystem
60                            .mountpoints
61                            .contains_path(Some(partition.mount_point()))
62                            .then_some(partition)
63                    })
64                    .filter_map(|partition| async { partition })
65                    // Filter on configured devices
66                    .map(|partition| {
67                        self.config
68                            .filesystem
69                            .devices
70                            .contains_path(partition.device().map(|d| d.as_ref()))
71                            .then_some(partition)
72                    })
73                    .filter_map(|partition| async { partition })
74                    // Filter on configured filesystems
75                    .map(|partition| {
76                        self.config
77                            .filesystem
78                            .filesystems
79                            .contains_str(Some(partition.file_system().as_str()))
80                            .then_some(partition)
81                    })
82                    .filter_map(|partition| async { partition })
83                    // Load usage from the partition mount point
84                    .filter_map(|partition| async {
85                        heim::disk::usage(partition.mount_point())
86                            .await
87                            .map_err(|error| {
88                                emit!(HostMetricsScrapeFilesystemError {
89                                    message: "Failed to load partitions info.",
90                                    mount_point: partition
91                                        .mount_point()
92                                        .to_str()
93                                        .unwrap_or("unknown")
94                                        .to_string(),
95                                    error,
96                                })
97                            })
98                            .map(|usage| (partition, usage))
99                            .ok()
100                    })
101                    .collect::<Vec<_>>()
102                    .await
103                {
104                    let fs = partition.file_system();
105                    let mut tags = metric_tags! {
106                        "filesystem" => fs.as_str(),
107                        "mountpoint" => partition.mount_point().to_string_lossy()
108                    };
109                    if let Some(device) = partition.device() {
110                        tags.replace("device".into(), device.to_string_lossy().to_string());
111                    }
112                    output.gauge(
113                        "filesystem_free_bytes",
114                        usage.free().get::<byte>() as f64,
115                        tags.clone(),
116                    );
117                    output.gauge(
118                        "filesystem_total_bytes",
119                        usage.total().get::<byte>() as f64,
120                        tags.clone(),
121                    );
122                    output.gauge(
123                        "filesystem_used_bytes",
124                        usage.used().get::<byte>() as f64,
125                        tags.clone(),
126                    );
127                    #[cfg(not(windows))]
128                    output.gauge(
129                        "filesystem_used_ratio",
130                        usage.ratio().get::<ratio>() as f64,
131                        tags,
132                    );
133                }
134            }
135            Err(error) => {
136                emit!(HostMetricsScrapeDetailError {
137                    message: "Failed to load partitions info.",
138                    error,
139                });
140            }
141        }
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::{
148        super::{
149            HostMetrics, HostMetricsConfig, MetricsBuffer,
150            tests::{all_gauges, assert_filtered_metrics, count_name, count_tag},
151        },
152        FilesystemConfig,
153    };
154
155    #[cfg(not(windows))]
156    #[tokio::test]
157    async fn generates_filesystem_metrics() {
158        let mut buffer = MetricsBuffer::new(None);
159        HostMetrics::new(HostMetricsConfig::default())
160            .filesystem_metrics(&mut buffer)
161            .await;
162        let metrics = buffer.metrics;
163        assert!(!metrics.is_empty());
164        assert!(metrics.len().is_multiple_of(4));
165        assert!(all_gauges(&metrics));
166
167        // There are exactly three filesystem_* names
168        for name in &[
169            "filesystem_free_bytes",
170            "filesystem_total_bytes",
171            "filesystem_used_bytes",
172            "filesystem_used_ratio",
173        ] {
174            assert_eq!(count_name(&metrics, name), metrics.len() / 4, "name={name}");
175        }
176
177        // They should all have "filesystem" and "mountpoint" tags
178        assert_eq!(count_tag(&metrics, "filesystem"), metrics.len());
179        assert_eq!(count_tag(&metrics, "mountpoint"), metrics.len());
180    }
181
182    #[cfg(windows)]
183    #[tokio::test]
184    async fn generates_filesystem_metrics() {
185        let mut buffer = MetricsBuffer::new(None);
186        HostMetrics::new(HostMetricsConfig::default())
187            .filesystem_metrics(&mut buffer)
188            .await;
189        let metrics = buffer.metrics;
190        assert!(!metrics.is_empty());
191        assert!(metrics.len() % 3 == 0);
192        assert!(all_gauges(&metrics));
193
194        // There are exactly three filesystem_* names
195        for name in &[
196            "filesystem_free_bytes",
197            "filesystem_total_bytes",
198            "filesystem_used_bytes",
199        ] {
200            assert_eq!(
201                count_name(&metrics, name),
202                metrics.len() / 3,
203                "name={}",
204                name
205            );
206        }
207
208        // They should all have "filesystem" and "mountpoint" tags
209        assert_eq!(count_tag(&metrics, "filesystem"), metrics.len());
210        assert_eq!(count_tag(&metrics, "mountpoint"), metrics.len());
211    }
212
213    #[tokio::test]
214    async fn filesystem_metrics_filters_on_device() {
215        assert_filtered_metrics("device", |devices| async move {
216            let mut buffer = MetricsBuffer::new(None);
217            HostMetrics::new(HostMetricsConfig {
218                filesystem: FilesystemConfig {
219                    devices,
220                    ..Default::default()
221                },
222                ..Default::default()
223            })
224            .filesystem_metrics(&mut buffer)
225            .await;
226            buffer.metrics
227        })
228        .await;
229    }
230
231    #[tokio::test]
232    async fn filesystem_metrics_filters_on_filesystem() {
233        assert_filtered_metrics("filesystem", |filesystems| async move {
234            let mut buffer = MetricsBuffer::new(None);
235            HostMetrics::new(HostMetricsConfig {
236                filesystem: FilesystemConfig {
237                    filesystems,
238                    ..Default::default()
239                },
240                ..Default::default()
241            })
242            .filesystem_metrics(&mut buffer)
243            .await;
244            buffer.metrics
245        })
246        .await;
247    }
248
249    #[tokio::test]
250    async fn filesystem_metrics_filters_on_mountpoint() {
251        assert_filtered_metrics("mountpoint", |mountpoints| async move {
252            let mut buffer = MetricsBuffer::new(None);
253            HostMetrics::new(HostMetricsConfig {
254                filesystem: FilesystemConfig {
255                    mountpoints,
256                    ..Default::default()
257                },
258                ..Default::default()
259            })
260            .filesystem_metrics(&mut buffer)
261            .await;
262            buffer.metrics
263        })
264        .await;
265    }
266}