Skip to main content

btrfs_cli/device/
stats.rs

1use crate::{
2    Format, RunContext, Runnable,
3    util::{open_path, print_json},
4};
5use anyhow::{Context, Result, bail};
6use btrfs_uapi::{
7    device::{DeviceStats, device_info_all, device_stats},
8    filesystem::filesystem_info,
9};
10use clap::Parser;
11use cols::Cols;
12use serde::Serialize;
13use std::{os::unix::io::AsFd, path::PathBuf};
14
15/// Show device I/O error statistics for all devices of a filesystem
16///
17/// Reads per-device counters for write, read, flush, corruption, and
18/// generation errors. The path can be a mount point or a device belonging
19/// to the filesystem.
20///
21/// With --offline, reads stats directly from the on-disk device tree
22/// without requiring a mounted filesystem.
23#[derive(Parser, Debug)]
24#[allow(clippy::doc_markdown)]
25pub struct DeviceStatsCommand {
26    /// Return a non-zero exit code if any error counter is greater than zero
27    #[clap(long, short)]
28    pub check: bool,
29
30    /// Print current values and then atomically reset all counters to zero
31    #[clap(long, short = 'z', conflicts_with = "offline")]
32    pub reset: bool,
33
34    /// Show stats in a tabular format with columns instead of per-device lines
35    #[clap(short = 'T')]
36    pub tabular: bool,
37
38    /// Read stats from the on-disk device tree (no mount required)
39    #[clap(long)]
40    pub offline: bool,
41
42    /// Path to a mounted btrfs filesystem or a device/image file
43    pub path: PathBuf,
44}
45
46#[derive(Serialize)]
47struct StatsJson {
48    device: String,
49    devid: u64,
50    write_io_errs: u64,
51    read_io_errs: u64,
52    flush_io_errs: u64,
53    corruption_errs: u64,
54    generation_errs: u64,
55}
56
57impl StatsJson {
58    fn from_uapi(path: &str, stats: &DeviceStats) -> Self {
59        Self {
60            device: path.to_string(),
61            devid: stats.devid,
62            write_io_errs: stats.write_errs,
63            read_io_errs: stats.read_errs,
64            flush_io_errs: stats.flush_errs,
65            corruption_errs: stats.corruption_errs,
66            generation_errs: stats.generation_errs,
67        }
68    }
69
70    fn from_disk(
71        path: &str,
72        devid: u64,
73        stats: &btrfs_disk::items::DeviceStats,
74    ) -> Self {
75        Self {
76            device: path.to_string(),
77            devid,
78            write_io_errs: stats.values.first().map_or(0, |v| v.1),
79            read_io_errs: stats.values.get(1).map_or(0, |v| v.1),
80            flush_io_errs: stats.values.get(2).map_or(0, |v| v.1),
81            corruption_errs: stats.values.get(3).map_or(0, |v| v.1),
82            generation_errs: stats.values.get(4).map_or(0, |v| v.1),
83        }
84    }
85
86    fn is_clean(&self) -> bool {
87        self.write_io_errs == 0
88            && self.read_io_errs == 0
89            && self.flush_io_errs == 0
90            && self.corruption_errs == 0
91            && self.generation_errs == 0
92    }
93}
94
95impl Runnable for DeviceStatsCommand {
96    fn supported_formats(&self) -> &[Format] {
97        &[Format::Text, Format::Json, Format::Modern]
98    }
99
100    fn run(&self, ctx: &RunContext) -> Result<()> {
101        if self.offline {
102            return self.run_offline(ctx.format);
103        }
104
105        let file = open_path(&self.path)?;
106        let fd = file.as_fd();
107
108        let fs = filesystem_info(fd).with_context(|| {
109            format!(
110                "failed to get filesystem info for '{}'",
111                self.path.display()
112            )
113        })?;
114
115        let devices = device_info_all(fd, &fs).with_context(|| {
116            format!("failed to get device info for '{}'", self.path.display())
117        })?;
118
119        if devices.is_empty() {
120            bail!("no devices found for '{}'", self.path.display());
121        }
122
123        let mut all_stats: Vec<StatsJson> = Vec::new();
124        let mut any_nonzero = false;
125
126        for dev in &devices {
127            let stats =
128                device_stats(fd, dev.devid, self.reset).with_context(|| {
129                    format!(
130                        "failed to get stats for device {} ({})",
131                        dev.devid, dev.path
132                    )
133                })?;
134
135            let entry = StatsJson::from_uapi(&dev.path, &stats);
136            if !entry.is_clean() {
137                any_nonzero = true;
138            }
139            all_stats.push(entry);
140        }
141
142        match ctx.format {
143            Format::Modern => print_stats_modern(&all_stats),
144            Format::Text if self.tabular => {
145                print_stats_tabular(&all_stats);
146            }
147            Format::Text => {
148                for s in &all_stats {
149                    print_stats_text(s);
150                }
151            }
152            Format::Json => {
153                print_json("device-stats", &all_stats)?;
154            }
155        }
156
157        if self.check && any_nonzero {
158            bail!("one or more devices have non-zero error counters");
159        }
160
161        Ok(())
162    }
163}
164
165impl DeviceStatsCommand {
166    fn run_offline(&self, format: Format) -> Result<()> {
167        use btrfs_disk::{
168            items::DeviceStats as DiskDeviceStats,
169            reader::{self, Traversal},
170            tree::{KeyType, TreeBlock},
171        };
172
173        let file = open_path(&self.path)?;
174        let fs = reader::filesystem_open(file).with_context(|| {
175            format!(
176                "failed to open btrfs filesystem on '{}'",
177                self.path.display()
178            )
179        })?;
180
181        let dev_tree_id = u64::from(btrfs_disk::raw::BTRFS_DEV_TREE_OBJECTID);
182        let (dev_root, _) = fs
183            .tree_roots
184            .get(&dev_tree_id)
185            .context("device tree not found")?;
186        let dev_root = *dev_root;
187
188        let header_size = std::mem::size_of::<btrfs_disk::raw::btrfs_header>();
189        let path_str = self.path.display().to_string();
190
191        let mut all_stats: Vec<StatsJson> = Vec::new();
192        let mut block_reader = fs.reader;
193
194        reader::tree_walk(
195            &mut block_reader,
196            dev_root,
197            Traversal::Dfs,
198            &mut |block| {
199                if let TreeBlock::Leaf { items, data, .. } = block {
200                    for item in items {
201                        if item.key.key_type == KeyType::PersistentItem {
202                            let start = header_size + item.offset as usize;
203                            let end = start + item.size as usize;
204                            if end <= data.len() {
205                                let ds =
206                                    DiskDeviceStats::parse(&data[start..end]);
207                                let devid = item.key.offset;
208                                all_stats.push(StatsJson::from_disk(
209                                    &path_str, devid, &ds,
210                                ));
211                            }
212                        }
213                    }
214                }
215            },
216        )
217        .with_context(|| {
218            format!("failed to walk device tree on '{}'", self.path.display())
219        })?;
220
221        if all_stats.is_empty() {
222            all_stats.push(StatsJson::from_disk(
223                &path_str,
224                1,
225                &DiskDeviceStats::parse(&[]),
226            ));
227        }
228
229        let any_nonzero = all_stats.iter().any(|s| !s.is_clean());
230
231        match format {
232            Format::Modern => print_stats_modern(&all_stats),
233            Format::Text if self.tabular => {
234                print_stats_tabular(&all_stats);
235            }
236            Format::Text => {
237                for s in &all_stats {
238                    let label = format!("{}.devid.{}", s.device, s.devid);
239                    print_stats_text_labeled(&label, s);
240                }
241            }
242            Format::Json => {
243                print_json("device-stats", &all_stats)?;
244            }
245        }
246
247        if self.check && any_nonzero {
248            bail!("one or more devices have non-zero error counters");
249        }
250
251        Ok(())
252    }
253}
254
255#[derive(Cols)]
256struct StatsRow {
257    #[column(header = "ID", right)]
258    devid: u64,
259    #[column(header = "WRITE_ERR", right)]
260    write_errs: u64,
261    #[column(header = "READ_ERR", right)]
262    read_errs: u64,
263    #[column(header = "FLUSH_ERR", right)]
264    flush_errs: u64,
265    #[column(header = "CORRUPT_ERR", right)]
266    corruption_errs: u64,
267    #[column(header = "GEN_ERR", right)]
268    generation_errs: u64,
269    #[column(header = "PATH", wrap)]
270    path: String,
271}
272
273impl StatsRow {
274    fn from_json(s: &StatsJson) -> Self {
275        Self {
276            devid: s.devid,
277            path: s.device.clone(),
278            write_errs: s.write_io_errs,
279            read_errs: s.read_io_errs,
280            flush_errs: s.flush_io_errs,
281            corruption_errs: s.corruption_errs,
282            generation_errs: s.generation_errs,
283        }
284    }
285}
286
287/// Modern output: cols-based adaptive table.
288fn print_stats_modern(stats: &[StatsJson]) {
289    let rows: Vec<StatsRow> = stats.iter().map(StatsRow::from_json).collect();
290    let mut out = std::io::stdout().lock();
291    let _ = StatsRow::print_table(&rows, &mut out);
292}
293
294/// Legacy tabular output matching btrfs-progs `-T` format exactly.
295fn print_stats_tabular(stats: &[StatsJson]) {
296    const HEADERS: [&str; 7] = [
297        "Id",
298        "Path",
299        "Write errors",
300        "Read errors",
301        "Flush errors",
302        "Corruption errors",
303        "Generation errors",
304    ];
305
306    let rows: Vec<[String; 7]> = stats
307        .iter()
308        .map(|s| {
309            [
310                s.devid.to_string(),
311                s.device.clone(),
312                s.write_io_errs.to_string(),
313                s.read_io_errs.to_string(),
314                s.flush_io_errs.to_string(),
315                s.corruption_errs.to_string(),
316                s.generation_errs.to_string(),
317            ]
318        })
319        .collect();
320
321    let mut widths = HEADERS.map(str::len);
322    for row in &rows {
323        for (i, cell) in row.iter().enumerate() {
324            widths[i] = widths[i].max(cell.len());
325        }
326    }
327
328    // Header (left-aligned).
329    for (i, hdr) in HEADERS.iter().enumerate() {
330        if i > 0 {
331            print!("  ");
332        }
333        print!("{hdr:<w$}", w = widths[i]);
334    }
335    println!();
336
337    // Separator.
338    for (i, w) in widths.iter().enumerate() {
339        if i > 0 {
340            print!("  ");
341        }
342        print!("{}", "-".repeat(*w));
343    }
344    println!();
345
346    // Data rows (right-aligned).
347    for row in &rows {
348        for (i, cell) in row.iter().enumerate() {
349            if i > 0 {
350                print!("  ");
351            }
352            print!("{cell:>w$}", w = widths[i]);
353        }
354        println!();
355    }
356}
357
358fn print_stats_text(s: &StatsJson) {
359    let p = &s.device;
360    println!("[{p}].{:<24} {}", "write_io_errs", s.write_io_errs);
361    println!("[{p}].{:<24} {}", "read_io_errs", s.read_io_errs);
362    println!("[{p}].{:<24} {}", "flush_io_errs", s.flush_io_errs);
363    println!("[{p}].{:<24} {}", "corruption_errs", s.corruption_errs);
364    println!("[{p}].{:<24} {}", "generation_errs", s.generation_errs);
365}
366
367fn print_stats_text_labeled(label: &str, s: &StatsJson) {
368    println!("[{label}].{:<24} {}", "write_io_errs", s.write_io_errs);
369    println!("[{label}].{:<24} {}", "read_io_errs", s.read_io_errs);
370    println!("[{label}].{:<24} {}", "flush_io_errs", s.flush_io_errs);
371    println!("[{label}].{:<24} {}", "corruption_errs", s.corruption_errs);
372    println!("[{label}].{:<24} {}", "generation_errs", s.generation_errs);
373}