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