diff --git a/Cargo.lock b/Cargo.lock index 966af438..7ed3aed3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -294,6 +294,12 @@ dependencies = [ "objc2", ] +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + [[package]] name = "du-dust" version = "1.2.4" @@ -309,6 +315,7 @@ dependencies = [ "lscolors", "nu-ansi-term", "portable-atomic", + "prometheus-client", "rayon", "regex", "serde", @@ -435,6 +442,15 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.29" @@ -542,6 +558,29 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + [[package]] name = "portable-atomic" version = "1.13.0" @@ -584,6 +623,29 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prometheus-client" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cca3d75b4566b9a29fe1ed623587fb058e826eb329a0be4b7c4da1ebb2d7a6ca" +dependencies = [ + "dtoa", + "itoa", + "parking_lot", + "prometheus-client-derive-encode", +] + +[[package]] +name = "prometheus-client-derive-encode" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9adf1691c04c0a5ff46ff8f262b58beb07b0dbb61f96f9f54f6cbd82106ed87f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "quote" version = "1.0.43" @@ -619,6 +681,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.12.2" @@ -673,6 +744,12 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.228" @@ -722,6 +799,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + [[package]] name = "stfu8" version = "0.2.7" diff --git a/Cargo.toml b/Cargo.toml index c03ccdce..abd60835 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ serde_json = "1.0" sysinfo = "0.37" ctrlc = "3" chrono = "0.4" +prometheus-client = "0.24.1" [target.'cfg(not(target_has_atomic = "64"))'.dependencies] portable-atomic = "1.4" diff --git a/completions/_dust b/completions/_dust index 28483922..4c2c8113 100644 --- a/completions/_dust +++ b/completions/_dust @@ -107,6 +107,7 @@ m\:"last modified time"))' \ '(-D --only-dir)--only-file[Only files will be displayed. (Finds your largest files)]' \ '-j[Output the directory tree as json to the current directory]' \ '--output-json[Output the directory tree as json to the current directory]' \ +'--output-metrics[Output the directory tree as OpenMetrics / prometheus metrics]' \ '-h[Print help (see more with '\''--help'\'')]' \ '--help[Print help (see more with '\''--help'\'')]' \ '-V[Print version]' \ diff --git a/completions/_dust.ps1 b/completions/_dust.ps1 index 9129d190..84c3ee86 100644 --- a/completions/_dust.ps1 +++ b/completions/_dust.ps1 @@ -91,6 +91,7 @@ Register-ArgumentCompleter -Native -CommandName 'dust' -ScriptBlock { [CompletionResult]::new('--only-file', '--only-file', [CompletionResultType]::ParameterName, 'Only files will be displayed. (Finds your largest files)') [CompletionResult]::new('-j', '-j', [CompletionResultType]::ParameterName, 'Output the directory tree as json to the current directory') [CompletionResult]::new('--output-json', '--output-json', [CompletionResultType]::ParameterName, 'Output the directory tree as json to the current directory') + [CompletionResult]::new('--output-metrics', '--output-metrics', [CompletionResultType]::ParameterName, 'Output the directory tree as OpenMetrics / prometheus metrics') [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') diff --git a/completions/dust.bash b/completions/dust.bash index 9f5da2cd..f57a8386 100644 --- a/completions/dust.bash +++ b/completions/dust.bash @@ -23,7 +23,7 @@ _dust() { case "${cmd}" in dust) - opts="-d -T -n -p -X -I -L -x -s -r -c -C -b -B -z -R -f -i -v -e -t -w -P -D -F -o -S -j -M -A -y -m -h -V --depth --threads --config --number-of-lines --full-paths --ignore-directory --ignore-all-in-file --dereference-links --limit-filesystem --apparent-size --reverse --no-colors --force-colors --no-percent-bars --bars-on-right --min-size --screen-reader --skip-total --filecount --ignore-hidden --invert-filter --filter --file-types --terminal-width --no-progress --print-errors --only-dir --only-file --output-format --stack-size --output-json --mtime --atime --ctime --files0-from --files-from --collapse --filetime --help --version [PATH]..." + opts="-d -T -n -p -X -I -L -x -s -r -c -C -b -B -z -R -f -i -v -e -t -w -P -D -F -o -S -j -M -A -y -m -h -V --depth --threads --config --number-of-lines --full-paths --ignore-directory --ignore-all-in-file --dereference-links --limit-filesystem --apparent-size --reverse --no-colors --force-colors --no-percent-bars --bars-on-right --min-size --screen-reader --skip-total --filecount --ignore-hidden --invert-filter --filter --file-types --terminal-width --no-progress --print-errors --only-dir --only-file --output-format --stack-size --output-json --output-metrics --mtime --atime --ctime --files0-from --files-from --collapse --filetime --help --version [PATH]..." if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 diff --git a/completions/dust.elv b/completions/dust.elv index f7561401..67e0741e 100644 --- a/completions/dust.elv +++ b/completions/dust.elv @@ -88,6 +88,7 @@ set edit:completion:arg-completer[dust] = {|@words| cand --only-file 'Only files will be displayed. (Finds your largest files)' cand -j 'Output the directory tree as json to the current directory' cand --output-json 'Output the directory tree as json to the current directory' + cand --output-metrics 'Output the directory tree as OpenMetrics / prometheus metrics' cand -h 'Print help (see more with ''--help'')' cand --help 'Print help (see more with ''--help'')' cand -V 'Print version' diff --git a/completions/dust.fish b/completions/dust.fish index 8b52c6a7..49c713b4 100644 --- a/completions/dust.fish +++ b/completions/dust.fish @@ -47,5 +47,6 @@ complete -c dust -l print-errors -d 'Print path with errors' complete -c dust -s D -l only-dir -d 'Only directories will be displayed' complete -c dust -s F -l only-file -d 'Only files will be displayed. (Finds your largest files)' complete -c dust -s j -l output-json -d 'Output the directory tree as json to the current directory' +complete -c dust -l output-metrics -d 'Output the directory tree as OpenMetrics / prometheus metrics' complete -c dust -s h -l help -d 'Print help (see more with \'--help\')' complete -c dust -s V -l version -d 'Print version' diff --git a/man-page/dust.1 b/man-page/dust.1 index 5de50488..b2a9ce1c 100644 --- a/man-page/dust.1 +++ b/man-page/dust.1 @@ -4,7 +4,7 @@ .SH NAME Dust \- Like du but more intuitive .SH SYNOPSIS -\fBdust\fR [\fB\-d\fR|\fB\-\-depth\fR] [\fB\-T\fR|\fB\-\-threads\fR] [\fB\-\-config\fR] [\fB\-n\fR|\fB\-\-number\-of\-lines\fR] [\fB\-p\fR|\fB\-\-full\-paths\fR] [\fB\-X\fR|\fB\-\-ignore\-directory\fR] [\fB\-I\fR|\fB\-\-ignore\-all\-in\-file\fR] [\fB\-L\fR|\fB\-\-dereference\-links\fR] [\fB\-x\fR|\fB\-\-limit\-filesystem\fR] [\fB\-s\fR|\fB\-\-apparent\-size\fR] [\fB\-r\fR|\fB\-\-reverse\fR] [\fB\-c\fR|\fB\-\-no\-colors\fR] [\fB\-C\fR|\fB\-\-force\-colors\fR] [\fB\-b\fR|\fB\-\-no\-percent\-bars\fR] [\fB\-B\fR|\fB\-\-bars\-on\-right\fR] [\fB\-z\fR|\fB\-\-min\-size\fR] [\fB\-R\fR|\fB\-\-screen\-reader\fR] [\fB\-\-skip\-total\fR] [\fB\-f\fR|\fB\-\-filecount\fR] [\fB\-i\fR|\fB\-\-ignore\-hidden\fR] [\fB\-v\fR|\fB\-\-invert\-filter\fR] [\fB\-e\fR|\fB\-\-filter\fR] [\fB\-t\fR|\fB\-\-file\-types\fR] [\fB\-w\fR|\fB\-\-terminal\-width\fR] [\fB\-P\fR|\fB\-\-no\-progress\fR] [\fB\-\-print\-errors\fR] [\fB\-D\fR|\fB\-\-only\-dir\fR] [\fB\-F\fR|\fB\-\-only\-file\fR] [\fB\-o\fR|\fB\-\-output\-format\fR] [\fB\-S\fR|\fB\-\-stack\-size\fR] [\fB\-j\fR|\fB\-\-output\-json\fR] [\fB\-M\fR|\fB\-\-mtime\fR] [\fB\-A\fR|\fB\-\-atime\fR] [\fB\-y\fR|\fB\-\-ctime\fR] [\fB\-\-files0\-from\fR] [\fB\-\-files\-from\fR] [\fB\-\-collapse\fR] [\fB\-m\fR|\fB\-\-filetime\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fIPATH\fR] +\fBdust\fR [\fB\-d\fR|\fB\-\-depth\fR] [\fB\-T\fR|\fB\-\-threads\fR] [\fB\-\-config\fR] [\fB\-n\fR|\fB\-\-number\-of\-lines\fR] [\fB\-p\fR|\fB\-\-full\-paths\fR] [\fB\-X\fR|\fB\-\-ignore\-directory\fR] [\fB\-I\fR|\fB\-\-ignore\-all\-in\-file\fR] [\fB\-L\fR|\fB\-\-dereference\-links\fR] [\fB\-x\fR|\fB\-\-limit\-filesystem\fR] [\fB\-s\fR|\fB\-\-apparent\-size\fR] [\fB\-r\fR|\fB\-\-reverse\fR] [\fB\-c\fR|\fB\-\-no\-colors\fR] [\fB\-C\fR|\fB\-\-force\-colors\fR] [\fB\-b\fR|\fB\-\-no\-percent\-bars\fR] [\fB\-B\fR|\fB\-\-bars\-on\-right\fR] [\fB\-z\fR|\fB\-\-min\-size\fR] [\fB\-R\fR|\fB\-\-screen\-reader\fR] [\fB\-\-skip\-total\fR] [\fB\-f\fR|\fB\-\-filecount\fR] [\fB\-i\fR|\fB\-\-ignore\-hidden\fR] [\fB\-v\fR|\fB\-\-invert\-filter\fR] [\fB\-e\fR|\fB\-\-filter\fR] [\fB\-t\fR|\fB\-\-file\-types\fR] [\fB\-w\fR|\fB\-\-terminal\-width\fR] [\fB\-P\fR|\fB\-\-no\-progress\fR] [\fB\-\-print\-errors\fR] [\fB\-D\fR|\fB\-\-only\-dir\fR] [\fB\-F\fR|\fB\-\-only\-file\fR] [\fB\-o\fR|\fB\-\-output\-format\fR] [\fB\-S\fR|\fB\-\-stack\-size\fR] [\fB\-j\fR|\fB\-\-output\-json\fR] [\fB\-\-output\-metrics\fR] [\fB\-M\fR|\fB\-\-mtime\fR] [\fB\-A\fR|\fB\-\-atime\fR] [\fB\-y\fR|\fB\-\-ctime\fR] [\fB\-\-files0\-from\fR] [\fB\-\-files\-from\fR] [\fB\-\-collapse\fR] [\fB\-m\fR|\fB\-\-filetime\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fIPATH\fR] .SH DESCRIPTION Like du but more intuitive .SH OPTIONS @@ -128,6 +128,9 @@ Specify memory to use as stack size \- use if you see: \*(Aqfatal runtime error: \fB\-j\fR, \fB\-\-output\-json\fR Output the directory tree as json to the current directory .TP +\fB\-\-output\-metrics\fR +Output the directory tree as OpenMetrics / prometheus metrics +.TP \fB\-M\fR, \fB\-\-mtime\fR \fI\fR +/\-n matches files modified more/less than n days ago , and n matches files modified exactly n days ago, days are rounded down.That is +n => (−∞, curr−(n+1)), n => [curr−(n+1), curr−n), and \-n => (𝑐𝑢𝑟𝑟−𝑛, +∞) .TP diff --git a/src/cli.rs b/src/cli.rs index bf9f37f4..c2b7e1d9 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -158,6 +158,10 @@ pub struct Cli { #[arg(short('j'), long)] pub output_json: bool, + /// Output the directory tree as OpenMetrics / prometheus metrics + #[arg(long)] + pub output_metrics: bool, + /// +/-n matches files modified more/less than n days ago , and n matches /// files modified exactly n days ago, days are rounded down.That is +n => /// (−∞, curr−(n+1)), n => [curr−(n+1), curr−n), and -n => (𝑐𝑢𝑟𝑟−𝑛, +∞) diff --git a/src/config.rs b/src/config.rs index 141500e3..a7d41140 100644 --- a/src/config.rs +++ b/src/config.rs @@ -34,6 +34,7 @@ pub struct Config { pub stack_size: Option, pub threads: Option, pub output_json: Option, + pub output_metrics: Option, pub print_errors: Option, pub files0_from: Option, pub number_of_lines: Option, @@ -157,6 +158,9 @@ impl Config { pub fn get_output_json(&self, options: &Cli) -> bool { Some(true) == self.output_json || options.output_json } + pub fn get_output_metrics(&self, options: &Cli) -> bool { + Some(true) == self.output_metrics || options.output_metrics + } pub fn get_number_of_lines(&self, options: &Cli) -> Option { let from_cmd_line = options.number_of_lines; diff --git a/src/display_node.rs b/src/display_node.rs index 9a4bcbcf..002d908b 100644 --- a/src/display_node.rs +++ b/src/display_node.rs @@ -1,10 +1,16 @@ +use std::borrow::Cow; use std::cell::RefCell; use std::path::PathBuf; +use prometheus_client::collector::Collector; +use prometheus_client::encoding::{EncodeMetric as _, MetricEncoder}; +use prometheus_client::metrics::MetricType; +use prometheus_client::metrics::gauge::ConstGauge; +use prometheus_client::registry::{Registry, Unit}; use serde::ser::SerializeStruct; use serde::{Serialize, Serializer}; -use crate::display::human_readable_number; +use crate::display::{get_printable_name, human_readable_number}; #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] pub struct DisplayNode { @@ -28,6 +34,87 @@ impl DisplayNode { }; out } + + pub fn into_metrics( + self, + cli_params: Option>, + by_filecount: bool, + skip_total: bool, + ) -> String { + #[derive(Debug)] + struct DustExporter { + root_node: DisplayNode, + by_filecount: bool, + skip_total: bool, + } + impl Collector for DustExporter { + fn encode( + &self, + mut enc: prometheus_client::encoding::DescriptorEncoder, + ) -> Result<(), std::fmt::Error> { + let mut metric_encoder = match self.by_filecount { + false => enc.encode_descriptor( + "dust_file_size", + "Total size of files in this folder / size of this file.", + Some(&Unit::Bytes), + MetricType::Gauge, + )?, + true => enc.encode_descriptor( + "dust_file_count", + "Total number of files in this folder / '1' for files.", + None, + MetricType::Gauge, + )?, + }; + self.root_node + .encode_metrics(&mut metric_encoder, self.skip_total)?; + Ok(()) + } + } + + let global_labels = cli_params_to_label(cli_params.as_ref()); + + let mut registry = Registry::with_labels(global_labels.into_iter()); + registry.register_collector(Box::new(DustExporter { + root_node: self, + by_filecount, + skip_total, + })); + + let mut out = String::new(); + prometheus_client::encoding::text::encode(&mut out, ®istry) + .expect("String's Write impl never fails"); + out + } + fn encode_metrics( + &self, + metric_encoder: &mut MetricEncoder, + skip_self: bool, + ) -> Result<(), std::fmt::Error> { + if !skip_self { + let g = ConstGauge::new(self.size); + let labels = [("path", get_printable_name(&self.name, false))]; + let labeled = metric_encoder.encode_family(&labels)?; + g.encode(labeled)?; + } + for child in self.children.iter() { + child.encode_metrics(metric_encoder, false)?; + } + Ok(()) + } +} + +fn cli_params_to_label( + cli_params: Option<&Vec>, +) -> Option<(Cow<'static, str>, Cow<'static, str>)> { + let params = cli_params?; + + let value = params.iter().fold(None, |acc, param| match acc { + Some(acc) => Some(acc + " " + param.as_str()), + None => Some(param.to_string()), + })?; + + Some((Cow::Borrowed("paths"), Cow::Owned(value))) } // Only used for -j 'json' flag combined with -o 'output_type' flag diff --git a/src/main.rs b/src/main.rs index b9c75937..9a90cd68 100644 --- a/src/main.rs +++ b/src/main.rs @@ -326,6 +326,15 @@ fn print_output( } }); println!("{}", serde_json::to_string(&tree).unwrap()); + } else if config.get_output_metrics(&options) { + print!( + "{}", + tree.into_metrics( + options.params.clone(), + by_filecount, + config.get_skip_total(&options) + ) + ); } else { let idd = InitialDisplayData { short_paths: !config.get_full_paths(&options),