Initial commit
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"WebFetch(domain:crates.io)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/target
|
||||
Generated
+457
@@ -0,0 +1,457 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctrlc"
|
||||
version = "3.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162"
|
||||
dependencies = [
|
||||
"dispatch2",
|
||||
"nix",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dispatch2"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"futures-core",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify-sys"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.183"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.31.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
|
||||
dependencies = [
|
||||
"objc2-encode",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-encode"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "srvmon"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"ctrlc",
|
||||
"flate2",
|
||||
"inotify",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.50.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "srvmon"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.102"
|
||||
clap = { version = "4.6.0", features = ["derive"] }
|
||||
ctrlc = "3.5.2"
|
||||
flate2 = "1.1.9"
|
||||
inotify = "0.11.1"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
@@ -0,0 +1,85 @@
|
||||
use std::io::{self, Write};
|
||||
|
||||
use crate::latest::Run;
|
||||
use crate::top::TopEntry;
|
||||
|
||||
fn format_count(n: u64) -> String {
|
||||
if n >= 1_000_000 {
|
||||
format!("{}M", n / 1_000_000)
|
||||
} else if n >= 1_000 {
|
||||
format!("{}K", n / 1_000)
|
||||
} else {
|
||||
n.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enter_alt_screen() {
|
||||
print!("\x1b[?1049h\x1b[?25l");
|
||||
io::stdout().flush().unwrap();
|
||||
}
|
||||
|
||||
pub fn leave_alt_screen() {
|
||||
print!("\x1b[?25h\x1b[?1049l");
|
||||
io::stdout().flush().unwrap();
|
||||
}
|
||||
|
||||
pub fn render(latest: &[Run], top: &[TopEntry], lines: usize) {
|
||||
let latest_slice = &latest[..latest.len().min(lines)];
|
||||
let top_slice = &top[..top.len().min(lines)];
|
||||
|
||||
let ln_w = latest_slice.iter().map(|r| r.service.len()).max().unwrap_or(0);
|
||||
let lc_w = latest_slice
|
||||
.iter()
|
||||
.map(|r| format_count(r.count).len())
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
let rn_w = top_slice.iter().map(|t| t.service.len()).max().unwrap_or(0);
|
||||
let rc_w = top_slice
|
||||
.iter()
|
||||
.map(|t| format_count(t.count).len())
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
|
||||
let left_w = if ln_w > 0 { ln_w + 1 + lc_w } else { 6 };
|
||||
let left_w = left_w.max(6);
|
||||
|
||||
let mut out = String::new();
|
||||
out.push_str("\x1b[H");
|
||||
|
||||
out.push_str(&format!(
|
||||
"\x1b[1m{:<lw$} {}\x1b[0m\x1b[K\n",
|
||||
"Latest",
|
||||
"Top",
|
||||
lw = left_w
|
||||
));
|
||||
|
||||
for i in 0..lines {
|
||||
let left = if i < latest_slice.len() {
|
||||
format!(
|
||||
"{:<w$} {:>cw$}",
|
||||
latest_slice[i].service,
|
||||
format_count(latest_slice[i].count),
|
||||
w = ln_w,
|
||||
cw = lc_w
|
||||
)
|
||||
} else {
|
||||
" ".repeat(left_w)
|
||||
};
|
||||
let right = if i < top_slice.len() {
|
||||
format!(
|
||||
"{:<w$} {:>cw$}",
|
||||
top_slice[i].service,
|
||||
format_count(top_slice[i].count),
|
||||
w = rn_w,
|
||||
cw = rc_w
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
out.push_str(&format!("{} {}\x1b[K\n", left, right));
|
||||
}
|
||||
|
||||
out.push_str("\x1b[J");
|
||||
print!("{out}");
|
||||
io::stdout().flush().unwrap();
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use crate::logs::Entry;
|
||||
|
||||
pub struct Run {
|
||||
pub service: String,
|
||||
pub count: u64,
|
||||
}
|
||||
|
||||
pub fn build(entries: &mut [Entry]) -> Vec<Run> {
|
||||
entries.sort_by(|a, b| a.ts.partial_cmp(&b.ts).unwrap_or(std::cmp::Ordering::Equal));
|
||||
let mut runs: Vec<Run> = Vec::new();
|
||||
for entry in entries.iter() {
|
||||
if let Some(last) = runs.last_mut() {
|
||||
if last.service == entry.service {
|
||||
last.count += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
runs.push(Run {
|
||||
service: entry.service.clone(),
|
||||
count: 1,
|
||||
});
|
||||
}
|
||||
runs.reverse();
|
||||
runs
|
||||
}
|
||||
|
||||
pub fn push(latest: &mut Vec<Run>, entries: &[Entry]) {
|
||||
for entry in entries {
|
||||
if let Some(first) = latest.first_mut() {
|
||||
if first.service == entry.service {
|
||||
first.count += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
latest.insert(
|
||||
0,
|
||||
Run {
|
||||
service: entry.service.clone(),
|
||||
count: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
latest.truncate(1000);
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
use std::fs::{self, File};
|
||||
use std::io::{BufReader, Read, Seek, SeekFrom};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
use flate2::read::GzDecoder;
|
||||
use serde::Deserialize;
|
||||
|
||||
pub struct LogFile {
|
||||
pub path: PathBuf,
|
||||
pub name: String,
|
||||
pub service: String,
|
||||
}
|
||||
|
||||
pub struct Entry {
|
||||
pub service: String,
|
||||
pub ts: f64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LogLine {
|
||||
ts: f64,
|
||||
}
|
||||
|
||||
pub fn extract_service(filename: &str) -> Option<String> {
|
||||
filename
|
||||
.strip_suffix(".access.log.gz")
|
||||
.or_else(|| filename.strip_suffix(".access.log"))
|
||||
.map(String::from)
|
||||
}
|
||||
|
||||
pub fn discover(dir: &Path, suffix: &str) -> Result<Vec<LogFile>> {
|
||||
let mut files = Vec::new();
|
||||
for entry in fs::read_dir(dir)? {
|
||||
let entry = entry?;
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
if name.ends_with(suffix) {
|
||||
if let Some(service) = extract_service(&name) {
|
||||
files.push(LogFile {
|
||||
path: entry.path(),
|
||||
name,
|
||||
service,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
fn count_newlines(mut reader: impl Read) -> Result<u64> {
|
||||
let mut buf = [0u8; 65536];
|
||||
let mut count = 0u64;
|
||||
loop {
|
||||
let n = reader.read(&mut buf)?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
count += buf[..n].iter().filter(|&&b| b == b'\n').count() as u64;
|
||||
}
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// Returns (new_offset, count, truncated).
|
||||
/// If the file was truncated (size < offset), counts from 0 and signals truncation
|
||||
/// so the caller can reset the service's total count.
|
||||
pub fn count_lines_from(path: &Path, offset: u64) -> Result<(u64, u64, bool)> {
|
||||
let mut file = File::open(path)?;
|
||||
let size = file.metadata()?.len();
|
||||
let truncated = size < offset;
|
||||
let actual_offset = if truncated { 0 } else { offset };
|
||||
if size == actual_offset {
|
||||
return Ok((size, 0, truncated));
|
||||
}
|
||||
file.seek(SeekFrom::Start(actual_offset))?;
|
||||
let count = count_newlines(BufReader::with_capacity(65536, file))?;
|
||||
Ok((size, count, truncated))
|
||||
}
|
||||
|
||||
pub fn count_lines_gz(path: &Path) -> Result<u64> {
|
||||
count_newlines(GzDecoder::new(File::open(path)?))
|
||||
}
|
||||
|
||||
pub fn read_tail_entries(path: &Path, service: &str, tail_bytes: u64) -> Result<Vec<Entry>> {
|
||||
let mut file = File::open(path)?;
|
||||
let size = file.metadata()?.len();
|
||||
let start = size.saturating_sub(tail_bytes);
|
||||
file.seek(SeekFrom::Start(start))?;
|
||||
let mut data = Vec::new();
|
||||
file.read_to_end(&mut data)?;
|
||||
|
||||
let mut entries = Vec::new();
|
||||
let skip_first = start > 0;
|
||||
for (i, line) in data.split(|&b| b == b'\n').enumerate() {
|
||||
if skip_first && i == 0 {
|
||||
continue;
|
||||
}
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if let Ok(s) = std::str::from_utf8(line) {
|
||||
if let Ok(parsed) = serde_json::from_str::<LogLine>(s) {
|
||||
entries.push(Entry {
|
||||
service: service.to_string(),
|
||||
ts: parsed.ts,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
/// Returns (new_offset, count, truncated, entries).
|
||||
pub fn read_new_entries(
|
||||
path: &Path,
|
||||
service: &str,
|
||||
offset: u64,
|
||||
) -> Result<(u64, u64, bool, Vec<Entry>)> {
|
||||
let mut file = File::open(path)?;
|
||||
let size = file.metadata()?.len();
|
||||
let truncated = size < offset;
|
||||
let actual_offset = if truncated { 0 } else { offset };
|
||||
if size == actual_offset {
|
||||
return Ok((actual_offset, 0, truncated, vec![]));
|
||||
}
|
||||
file.seek(SeekFrom::Start(actual_offset))?;
|
||||
let mut data = Vec::new();
|
||||
file.read_to_end(&mut data)?;
|
||||
|
||||
let end = match data.iter().rposition(|&b| b == b'\n') {
|
||||
Some(pos) => pos + 1,
|
||||
None => return Ok((actual_offset, 0, truncated, vec![])),
|
||||
};
|
||||
|
||||
let new_offset = actual_offset + end as u64;
|
||||
let mut entries = Vec::new();
|
||||
|
||||
for line in data[..end].split(|&b| b == b'\n') {
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if let Ok(s) = std::str::from_utf8(line) {
|
||||
if let Ok(parsed) = serde_json::from_str::<LogLine>(s) {
|
||||
entries.push(Entry {
|
||||
service: service.to_string(),
|
||||
ts: parsed.ts,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let count = entries.len() as u64;
|
||||
Ok((new_offset, count, truncated, entries))
|
||||
}
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
mod display;
|
||||
mod latest;
|
||||
mod logs;
|
||||
mod state;
|
||||
mod top;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsString;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use clap::Parser;
|
||||
use inotify::{Inotify, WatchDescriptor, WatchMask};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "srvmon", about = "Monitor Caddy access logs")]
|
||||
struct Cli {
|
||||
/// Log directory path
|
||||
#[arg(short, long, default_value = "/var/log/caddy")]
|
||||
dir: PathBuf,
|
||||
|
||||
/// Number of lines to display per column
|
||||
#[arg(short, long, default_value_t = 8)]
|
||||
lines: usize,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
let dir = &cli.dir;
|
||||
|
||||
if !dir.is_dir() {
|
||||
bail!("{} is not a directory", dir.display());
|
||||
}
|
||||
|
||||
ctrlc::set_handler(|| {
|
||||
display::leave_alt_screen();
|
||||
std::process::exit(0);
|
||||
})?;
|
||||
|
||||
let mut state = state::State::load(dir);
|
||||
|
||||
// Process compressed logs
|
||||
let gz_new: Vec<_> = logs::discover(dir, ".access.log.gz")?
|
||||
.into_iter()
|
||||
.filter(|f| !state.processed_gz.contains(&f.name))
|
||||
.collect();
|
||||
|
||||
for (i, f) in gz_new.iter().enumerate() {
|
||||
eprint!("\rProcessing compressed logs ({}/{})...", i + 1, gz_new.len());
|
||||
let count = logs::count_lines_gz(&f.path)?;
|
||||
*state.counts.entry(f.service.clone()).or_default() += count;
|
||||
state.processed_gz.insert(f.name.clone());
|
||||
}
|
||||
if !gz_new.is_empty() {
|
||||
eprintln!(" done");
|
||||
}
|
||||
|
||||
// Process active log files
|
||||
let log_files = logs::discover(dir, ".access.log")?;
|
||||
let mut all_recent = Vec::new();
|
||||
|
||||
for (i, f) in log_files.iter().enumerate() {
|
||||
eprint!("\rProcessing logs ({}/{})...", i + 1, log_files.len());
|
||||
let offset = state.offsets.get(&f.name).copied().unwrap_or(0);
|
||||
let (new_offset, count, truncated) = logs::count_lines_from(&f.path, offset)?;
|
||||
if truncated {
|
||||
state.counts.insert(f.service.clone(), count);
|
||||
} else {
|
||||
*state.counts.entry(f.service.clone()).or_default() += count;
|
||||
}
|
||||
state.offsets.insert(f.name.clone(), new_offset);
|
||||
|
||||
let tail = logs::read_tail_entries(&f.path, &f.service, 65536)?;
|
||||
all_recent.extend(tail);
|
||||
}
|
||||
if !log_files.is_empty() {
|
||||
eprintln!(" done");
|
||||
}
|
||||
|
||||
let mut latest_runs = latest::build(&mut all_recent);
|
||||
state.save(dir)?;
|
||||
|
||||
display::enter_alt_screen();
|
||||
display::render(&latest_runs, &top::sorted(&state.counts), cli.lines);
|
||||
|
||||
// Set up inotify
|
||||
let mut inotify = Inotify::init()?;
|
||||
let dir_wd = inotify
|
||||
.watches()
|
||||
.add(dir, WatchMask::CREATE | WatchMask::MOVED_TO)?;
|
||||
|
||||
let mut wd_map: HashMap<WatchDescriptor, logs::LogFile> = HashMap::new();
|
||||
for f in logs::discover(dir, ".access.log")? {
|
||||
let wd = inotify.watches().add(&f.path, WatchMask::MODIFY)?;
|
||||
wd_map.insert(wd, f);
|
||||
}
|
||||
|
||||
let mut buffer = [0u8; 4096];
|
||||
loop {
|
||||
let events = match inotify.read_events_blocking(&mut buffer) {
|
||||
Ok(ev) => ev,
|
||||
Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
|
||||
let events: Vec<(WatchDescriptor, Option<OsString>)> = events
|
||||
.map(|e| (e.wd, e.name.map(|n| n.to_owned())))
|
||||
.collect();
|
||||
|
||||
let mut new_entries = Vec::new();
|
||||
let mut changed = false;
|
||||
|
||||
for (wd, name) in &events {
|
||||
if *wd == dir_wd {
|
||||
let Some(name) = name else { continue };
|
||||
let name_str = name.to_string_lossy();
|
||||
|
||||
if name_str.ends_with(".access.log") {
|
||||
if let Some(service) = logs::extract_service(&name_str) {
|
||||
let path = dir.join(&*name_str);
|
||||
let file_wd = inotify.watches().add(&path, WatchMask::MODIFY)?;
|
||||
wd_map.insert(file_wd, logs::LogFile {
|
||||
path,
|
||||
name: name_str.into_owned(),
|
||||
service,
|
||||
});
|
||||
changed = true;
|
||||
}
|
||||
} else if name_str.ends_with(".access.log.gz") {
|
||||
if let Some(service) = logs::extract_service(&name_str) {
|
||||
if !state.processed_gz.contains(&*name_str) {
|
||||
let path = dir.join(&*name_str);
|
||||
match logs::count_lines_gz(&path) {
|
||||
Ok(count) => {
|
||||
*state.counts.entry(service).or_default() += count;
|
||||
state.processed_gz.insert(name_str.into_owned());
|
||||
changed = true;
|
||||
}
|
||||
Err(_) => {} // file not ready yet, will catch on next event
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(f) = wd_map.get(wd) {
|
||||
let offset = state.offsets.get(&f.name).copied().unwrap_or(0);
|
||||
let (new_offset, count, truncated, entries) =
|
||||
logs::read_new_entries(&f.path, &f.service, offset)?;
|
||||
if count > 0 {
|
||||
if truncated {
|
||||
state.counts.insert(f.service.clone(), count);
|
||||
} else {
|
||||
*state.counts.entry(f.service.clone()).or_default() += count;
|
||||
}
|
||||
state.offsets.insert(f.name.clone(), new_offset);
|
||||
new_entries.extend(entries);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if changed {
|
||||
new_entries
|
||||
.sort_by(|a, b| a.ts.partial_cmp(&b.ts).unwrap_or(std::cmp::Ordering::Equal));
|
||||
latest::push(&mut latest_runs, &new_entries);
|
||||
state.save(dir)?;
|
||||
display::render(&latest_runs, &top::sorted(&state.counts), cli.lines);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
pub struct State {
|
||||
pub counts: HashMap<String, u64>,
|
||||
pub offsets: HashMap<String, u64>,
|
||||
pub processed_gz: HashSet<String>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn load(dir: &Path) -> Self {
|
||||
let path = dir.join(".srvmon.state.json");
|
||||
fs::read_to_string(&path)
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str(&s).ok())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn save(&self, dir: &Path) -> Result<()> {
|
||||
let tmp = dir.join(".srvmon.state.json.tmp");
|
||||
let final_path = dir.join(".srvmon.state.json");
|
||||
fs::write(&tmp, serde_json::to_string(self)?)?;
|
||||
fs::rename(&tmp, &final_path)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct TopEntry {
|
||||
pub service: String,
|
||||
pub count: u64,
|
||||
}
|
||||
|
||||
pub fn sorted(counts: &HashMap<String, u64>) -> Vec<TopEntry> {
|
||||
let mut top: Vec<_> = counts
|
||||
.iter()
|
||||
.map(|(s, &c)| TopEntry {
|
||||
service: s.clone(),
|
||||
count: c,
|
||||
})
|
||||
.collect();
|
||||
top.sort_by(|a, b| b.count.cmp(&a.count));
|
||||
top
|
||||
}
|
||||
Reference in New Issue
Block a user