vector/sources/host_metrics/
filesystem.rs1use futures::StreamExt;
2use heim::units::information::byte;
3#[cfg(not(windows))]
4use heim::units::ratio::ratio;
5#[cfg(unix)]
6use nix::sys::statvfs::statvfs;
7use vector_lib::{configurable::configurable_component, metric_tags};
8
9use super::{FilterList, HostMetrics, default_all_devices, example_devices, filter_result};
10use crate::internal_events::{HostMetricsScrapeDetailError, HostMetricsScrapeFilesystemError};
11
12#[configurable_component]
14#[derive(Clone, Debug, Default)]
15pub struct FilesystemConfig {
16 #[serde(default = "default_all_devices")]
19 #[configurable(metadata(docs::examples = "example_devices()"))]
20 devices: FilterList,
21
22 #[serde(default = "default_all_devices")]
25 #[configurable(metadata(docs::examples = "example_filesystems()"))]
26 filesystems: FilterList,
27
28 #[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 .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 .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 .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 .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.clone(),
134 );
135
136 #[cfg(unix)]
142 if let Ok(stat) = statvfs(partition.mount_point()) {
143 let inodes_total = stat.files() as f64;
144 let inodes_free = stat.files_free() as f64;
145 let inodes_used = (inodes_total - inodes_free).max(0.0);
146 let inodes_used_ratio = if inodes_total > 0.0 {
147 inodes_used / inodes_total
148 } else {
149 0.0
150 };
151
152 output.gauge("filesystem_inodes_total", inodes_total, tags.clone());
153 output.gauge("filesystem_inodes_free", inodes_free, tags.clone());
154 output.gauge("filesystem_inodes_used", inodes_used, tags.clone());
155 output.gauge("filesystem_inodes_used_ratio", inodes_used_ratio, tags);
156 }
157 #[cfg(windows)]
158 drop(tags);
159 }
160 }
161 Err(error) => {
162 emit!(HostMetricsScrapeDetailError {
163 message: "Failed to load partitions info.",
164 error,
165 });
166 }
167 }
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::{
174 super::{
175 HostMetrics, HostMetricsConfig, MetricsBuffer,
176 tests::{all_gauges, assert_filtered_metrics, count_name, count_tag},
177 },
178 FilesystemConfig,
179 };
180
181 #[cfg(not(windows))]
182 #[tokio::test]
183 async fn generates_filesystem_metrics() {
184 let mut buffer = MetricsBuffer::new(None);
185 HostMetrics::new(HostMetricsConfig::default())
186 .filesystem_metrics(&mut buffer)
187 .await;
188 let metrics = buffer.metrics;
189 assert!(!metrics.is_empty());
190 assert!(all_gauges(&metrics));
191
192 let base_metrics = [
194 "filesystem_free_bytes",
195 "filesystem_total_bytes",
196 "filesystem_used_bytes",
197 "filesystem_used_ratio",
198 ];
199
200 let num_filesystems = count_name(&metrics, "filesystem_free_bytes");
202 assert!(num_filesystems > 0);
203 for name in &base_metrics {
204 assert_eq!(count_name(&metrics, name), num_filesystems, "name={name}");
205 }
206
207 let inode_metrics = [
210 "filesystem_inodes_total",
211 "filesystem_inodes_free",
212 "filesystem_inodes_used",
213 "filesystem_inodes_used_ratio",
214 ];
215 let num_inode_total = count_name(&metrics, "filesystem_inodes_total");
216 assert!(
217 num_inode_total > 0,
218 "Expected at least one filesystem to report inode metrics"
219 );
220
221 for name in &inode_metrics {
223 assert_eq!(count_name(&metrics, name), num_inode_total, "name={name}");
224 }
225
226 assert_eq!(count_tag(&metrics, "filesystem"), metrics.len());
228 assert_eq!(count_tag(&metrics, "mountpoint"), metrics.len());
229 }
230
231 #[cfg(windows)]
232 #[tokio::test]
233 async fn generates_filesystem_metrics() {
234 let mut buffer = MetricsBuffer::new(None);
235 HostMetrics::new(HostMetricsConfig::default())
236 .filesystem_metrics(&mut buffer)
237 .await;
238 let metrics = buffer.metrics;
239 assert!(!metrics.is_empty());
240 assert!(metrics.len() % 3 == 0);
241 assert!(all_gauges(&metrics));
242
243 for name in &[
245 "filesystem_free_bytes",
246 "filesystem_total_bytes",
247 "filesystem_used_bytes",
248 ] {
249 assert_eq!(
250 count_name(&metrics, name),
251 metrics.len() / 3,
252 "name={}",
253 name
254 );
255 }
256
257 assert_eq!(count_tag(&metrics, "filesystem"), metrics.len());
259 assert_eq!(count_tag(&metrics, "mountpoint"), metrics.len());
260 }
261
262 #[tokio::test]
263 async fn filesystem_metrics_filters_on_device() {
264 assert_filtered_metrics("device", |devices| async move {
265 let mut buffer = MetricsBuffer::new(None);
266 HostMetrics::new(HostMetricsConfig {
267 filesystem: FilesystemConfig {
268 devices,
269 ..Default::default()
270 },
271 ..Default::default()
272 })
273 .filesystem_metrics(&mut buffer)
274 .await;
275 buffer.metrics
276 })
277 .await;
278 }
279
280 #[tokio::test]
281 async fn filesystem_metrics_filters_on_filesystem() {
282 assert_filtered_metrics("filesystem", |filesystems| async move {
283 let mut buffer = MetricsBuffer::new(None);
284 HostMetrics::new(HostMetricsConfig {
285 filesystem: FilesystemConfig {
286 filesystems,
287 ..Default::default()
288 },
289 ..Default::default()
290 })
291 .filesystem_metrics(&mut buffer)
292 .await;
293 buffer.metrics
294 })
295 .await;
296 }
297
298 #[tokio::test]
299 async fn filesystem_metrics_filters_on_mountpoint() {
300 assert_filtered_metrics("mountpoint", |mountpoints| async move {
301 let mut buffer = MetricsBuffer::new(None);
302 HostMetrics::new(HostMetricsConfig {
303 filesystem: FilesystemConfig {
304 mountpoints,
305 ..Default::default()
306 },
307 ..Default::default()
308 })
309 .filesystem_metrics(&mut buffer)
310 .await;
311 buffer.metrics
312 })
313 .await;
314 }
315}