From 88127ad376a73e188ed8f69d155668ee2686f616 Mon Sep 17 00:00:00 2001 From: Steve Sampson Date: Tue, 12 Sep 2023 16:57:41 -0300 Subject: [PATCH] Get Stats From Docker Daemon via HTTP --- Cargo.lock | 372 +++++++++++++++++- Cargo.toml | 9 +- src/cli.rs | 22 ++ src/docker/collector.rs | 68 ++-- .../method/get_containers/implementation.rs | 27 ++ src/docker/method/get_containers/mod.rs | 2 + src/docker/method/get_containers/models.rs | 98 +++++ src/docker/method/get_stats/implementation.rs | 29 ++ src/docker/method/get_stats/mod.rs | 2 + src/docker/method/get_stats/models.rs | 190 +++++++++ src/docker/method/mod.rs | 27 ++ src/docker/metrics.rs | 4 +- src/docker/mod.rs | 94 +++-- src/docker/util.rs | 193 --------- src/error.rs | 8 + src/main.rs | 34 +- 16 files changed, 893 insertions(+), 286 deletions(-) create mode 100644 src/cli.rs create mode 100644 src/docker/method/get_containers/implementation.rs create mode 100644 src/docker/method/get_containers/mod.rs create mode 100644 src/docker/method/get_containers/models.rs create mode 100644 src/docker/method/get_stats/implementation.rs create mode 100644 src/docker/method/get_stats/mod.rs create mode 100644 src/docker/method/get_stats/models.rs create mode 100644 src/docker/method/mod.rs delete mode 100644 src/docker/util.rs diff --git a/Cargo.lock b/Cargo.lock index 78ebc37..ecd6c70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -136,7 +136,7 @@ checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" dependencies = [ "async-trait", "axum-core", - "bitflags", + "bitflags 1.3.2", "bytes", "futures-util", "http", @@ -210,6 +210,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + [[package]] name = "bitvec" version = "1.0.1" @@ -398,6 +404,22 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + [[package]] name = "cpufeatures" version = "0.2.9" @@ -437,12 +459,48 @@ dependencies = [ "subtle", ] +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "errno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "fallible-iterator" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" +[[package]] +name = "fastrand" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" + [[package]] name = "finl_unicode" version = "1.2.0" @@ -465,6 +523,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.0" @@ -480,6 +553,21 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.28" @@ -496,6 +584,23 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + [[package]] name = "futures-macro" version = "0.3.28" @@ -525,10 +630,13 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-macro", "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -684,6 +792,29 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -694,6 +825,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "ipnet" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" + [[package]] name = "iri-string" version = "0.4.1" @@ -730,6 +867,12 @@ version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +[[package]] +name = "linux-raw-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" + [[package]] name = "lock_api" version = "0.4.10" @@ -809,6 +952,24 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nom" version = "7.1.3" @@ -853,6 +1014,50 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "openssl" +version = "0.10.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" +dependencies = [ + "bitflags 2.4.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -932,6 +1137,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + [[package]] name = "postgres" version = "0.19.7" @@ -1091,7 +1302,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -1103,6 +1314,43 @@ dependencies = [ "bytecheck", ] +[[package]] +name = "reqwest" +version = "0.11.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" +dependencies = [ + "base64 0.21.4", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "rkyv" version = "0.7.42" @@ -1164,6 +1412,19 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustix" +version = "0.38.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "rustversion" version = "1.0.14" @@ -1176,6 +1437,15 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1188,6 +1458,29 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.188" @@ -1245,20 +1538,25 @@ dependencies = [ name = "server_metrics" version = "0.1.0" dependencies = [ + "async-trait", "axum", "clap", + "futures", "http", "http-body", "hyper", "prometheus", + "reqwest", "rust_decimal", "rust_decimal_macros", "serde", "serde_json", + "slog", "thiserror", "tokio", "tower", "tower-http", + "url", ] [[package]] @@ -1302,6 +1600,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slog" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06" + [[package]] name = "smallvec" version = "1.11.0" @@ -1385,6 +1689,19 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tempfile" +version = "3.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys", +] + [[package]] name = "thiserror" version = "1.0.48" @@ -1450,6 +1767,16 @@ dependencies = [ "syn 2.0.32", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-postgres" version = "0.7.10" @@ -1528,7 +1855,7 @@ checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858" dependencies = [ "async-compression", "base64 0.13.1", - "bitflags", + "bitflags 1.3.2", "bytes", "futures-core", "futures-util", @@ -1625,6 +1952,17 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "url" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "utf8parse" version = "0.2.1" @@ -1640,6 +1978,12 @@ dependencies = [ "getrandom", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" @@ -1686,6 +2030,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.87" @@ -1823,6 +2179,16 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys", +] + [[package]] name = "wyz" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index b766615..a7d15a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,17 +6,22 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +async-trait = "0.1.61" axum = "0.6.7" clap = { version = "4.4.2", features = ["derive"] } +futures = "0.3.28" http = "0.2.8" http-body = "0.4.5" -hyper = { version = "0.14.24", features = ["full"] } +hyper = { version = "0.14", features = ["full"] } prometheus = "0.13.3" +reqwest = { version = "0.11.14", features = ["json"] } rust_decimal = { version = "1.29.1", features = ["serde", "db-postgres"] } rust_decimal_macros = "1.27" serde = { version = "1.0.152", features = ["derive"] } serde_json = "1.0.91" +slog = "2.7.0" thiserror = "1.0.38" tokio = { version = "1.28.2", features = ["full"] } tower = { version = "0.4.13", features = ["full"] } -tower-http = { version = "0.3.5", features = ["full"] } \ No newline at end of file +tower-http = { version = "0.3.5", features = ["full"] } +url = "2.3.1" \ No newline at end of file diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..ab28068 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,22 @@ +use clap::Parser; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +pub struct Args { + /// The address to bind the metrics server on + #[arg(long, default_value = "0.0.0.0")] + pub bind_address: String, + /// The port to bind the metrics server to + #[arg(long, default_value_t = 45454)] + pub bind_port: u32, + /// The metrics registry prefix + #[arg(long, default_value = "system_exporter")] + pub metrics_prefix: String, + /// The interval to poll docker stats + #[arg(long, default_value_t = 1)] + pub docker_stats_seconds: u64, + /// The fqdn to the docker server (e.g: unix:/var/run/docker.sock, http://localhost:2375) + /// Note: unix sockets are not supported yet + #[arg(long, default_value = "http://localhost:2375")] + pub docker_server: String, +} diff --git a/src/docker/collector.rs b/src/docker/collector.rs index d9c9ed8..6ad062c 100644 --- a/src/docker/collector.rs +++ b/src/docker/collector.rs @@ -1,42 +1,58 @@ use std::sync::Arc; -use crate::{docker::DockerStats, error::SystemExporterResult, metrics::Metrics}; +use crate::{ + docker::{ + method::{ + get_containers::models::GetContainersRequest, + get_stats::models::{DockerStats, GetContainerStatsRequest}, + DockerClientContainersTrait, DockerClientStatsTrait, + }, + DockerClient, + }, + error::SystemExporterResult, + metrics::Metrics, +}; pub struct DockerCollector { metrics: Arc, + client: DockerClient, } impl DockerCollector { - pub fn new(metrics: Arc) -> Self { - Self { metrics } + pub fn new(client: DockerClient, metrics: Arc) -> Self { + Self { client, metrics } } - pub fn poll(&self) -> SystemExporterResult> { - let output = std::process::Command::new("sh") - .arg("-c") - .arg( - r#"docker stats --format='{ - "BlockIO": "{{.BlockIO}}", - "CPUPerc": "{{.CPUPerc}}", - "Container": "{{.Container}}", - "ID": "{{.ID}}", - "MemPerc": "{{.MemPerc}}", - "MemUsage": "{{.MemUsage}}", - "Name": "{{.Name}}", - "NetIO": "{{.NetIO}}", - "PIDs": "{{.PIDs}}" - }' --no-stream | jq -s '.'"#, - ) - .output() - .expect("Failed to execute command"); + pub async fn poll(&self) -> SystemExporterResult> { + let mut stats = vec![]; - let output_str = String::from_utf8(output.stdout).expect("Not UTF-8"); + let get_containers_response = self.client.get_containers(GetContainersRequest {}).await?; - let res = serde_json::from_str::>(&output_str)?; + let container_ids = get_containers_response + .containers + .into_iter() + .map(|e| e.id) + .collect::>(); - res.iter() - .for_each(|e| e.post_metrics(&self.metrics.docker)); + let futures = container_ids.iter().map(|container_id| { + self.client.get_container_stats(GetContainerStatsRequest { + container_id: container_id.clone(), + }) + }); - Ok(res) + // todo: should we batch these in case there are a lot of containers? + // todo: do we care about failures? could be the case that a container terminated between getting IDs and stats + for get_containers_response in futures::future::join_all(futures) + .await + .into_iter() + .flatten() + { + get_containers_response + .stats + .post_metrics(&self.metrics.docker); + stats.push(get_containers_response.stats); + } + + Ok(stats) } } diff --git a/src/docker/method/get_containers/implementation.rs b/src/docker/method/get_containers/implementation.rs new file mode 100644 index 0000000..74e403f --- /dev/null +++ b/src/docker/method/get_containers/implementation.rs @@ -0,0 +1,27 @@ +use crate::{ + docker::{ + method::{ + get_containers::models::{ + DockerContainer, GetContainersRequest, GetContainersResponse, + }, + DockerClientContainersTrait, + }, + DockerClient, + }, + error::SystemExporterResult, +}; +use async_trait::async_trait; + +#[async_trait] +impl DockerClientContainersTrait for DockerClient { + async fn get_containers( + &self, + _req: GetContainersRequest, + ) -> SystemExporterResult { + let containers: Vec = self + .send_request(http::Method::GET, "containers/json?all=false", _req) + .await?; + + Ok(GetContainersResponse { containers }) + } +} diff --git a/src/docker/method/get_containers/mod.rs b/src/docker/method/get_containers/mod.rs new file mode 100644 index 0000000..f25a37c --- /dev/null +++ b/src/docker/method/get_containers/mod.rs @@ -0,0 +1,2 @@ +mod implementation; +pub mod models; diff --git a/src/docker/method/get_containers/models.rs b/src/docker/method/get_containers/models.rs new file mode 100644 index 0000000..18a77b9 --- /dev/null +++ b/src/docker/method/get_containers/models.rs @@ -0,0 +1,98 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetContainersRequest {} + +#[derive(Debug, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetContainersResponse { + pub containers: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct DockerContainer { + pub id: String, + pub command: String, + pub created: i64, + pub host_config: HostConfig, + pub image: String, + #[serde(rename = "ImageID")] + pub image_id: String, + pub labels: HashMap, + pub mounts: Vec, + pub names: Vec, + pub network_settings: NetworkSettings, + pub ports: Vec, + pub state: String, + pub status: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct HostConfig { + pub network_mode: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct Mount { + pub destination: String, + pub mode: String, + pub propagation: String, + #[serde(rename(deserialize = "RW"))] + pub rw: bool, + pub source: String, + #[serde(rename(deserialize = "Type"))] + pub type_: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct NetworkSettings { + pub networks: Networks, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct Networks { + #[serde(rename(deserialize = "bridge"))] + pub bridge: Bridge, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct Bridge { + #[serde(rename(deserialize = "EndpointID"))] + pub endpoint_id: String, + pub gateway: String, + #[serde(rename(deserialize = "GlobalIPv6Address"))] + pub global_ipv6_address: String, + #[serde(rename(deserialize = "GlobalIPv6PrefixLen"))] + pub global_ipv6_prefix_len: i32, + #[serde(rename(deserialize = "IPAMConfig"))] + pub ipam_config: Option, // You might want to replace this with the appropriate type + #[serde(rename(deserialize = "IPAddress"))] + pub ip_address: String, + #[serde(rename(deserialize = "IPPrefixLen"))] + pub ip_prefix_len: i32, + #[serde(rename(deserialize = "IPv6Gateway"))] + pub ipv6_gateway: String, + pub links: Option, // You might want to replace this with the appropriate type + pub mac_address: String, + #[serde(rename(deserialize = "NetworkID"))] + pub network_id: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct Port { + #[serde(rename(deserialize = "IP"))] + pub ip: String, + pub private_port: i32, + pub public_port: i32, + #[serde(rename(deserialize = "Type"))] + pub type_: String, +} diff --git a/src/docker/method/get_stats/implementation.rs b/src/docker/method/get_stats/implementation.rs new file mode 100644 index 0000000..e4a2c97 --- /dev/null +++ b/src/docker/method/get_stats/implementation.rs @@ -0,0 +1,29 @@ +use crate::{ + docker::{ + method::{ + get_stats::models::{DockerStats, GetContainerStatsRequest, GetContainerStatsResponse}, + DockerClientStatsTrait, + }, + DockerClient, + }, + error::SystemExporterResult, +}; +use async_trait::async_trait; + +#[async_trait] +impl DockerClientStatsTrait for DockerClient { + async fn get_container_stats( + &self, + req: GetContainerStatsRequest, + ) -> SystemExporterResult { + let stats: DockerStats = self + .send_request( + http::Method::GET, + format!("containers/{}/stats?stream=false", req.container_id).as_str(), + req, + ) + .await?; + + Ok(GetContainerStatsResponse { stats }) + } +} diff --git a/src/docker/method/get_stats/mod.rs b/src/docker/method/get_stats/mod.rs new file mode 100644 index 0000000..f25a37c --- /dev/null +++ b/src/docker/method/get_stats/mod.rs @@ -0,0 +1,2 @@ +mod implementation; +pub mod models; diff --git a/src/docker/method/get_stats/models.rs b/src/docker/method/get_stats/models.rs new file mode 100644 index 0000000..c44e775 --- /dev/null +++ b/src/docker/method/get_stats/models.rs @@ -0,0 +1,190 @@ +use crate::docker::metrics::DockerMetrics; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetContainerStatsRequest { + pub container_id: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetContainerStatsResponse { + pub stats: DockerStats, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct DockerStats { + id: String, + name: String, + cpu_stats: CpuStats, + precpu_stats: CpuStats, + blkio_stats: BlkioStats, + memory_stats: MemoryStats, + pids_stats: PidsStats, + #[serde(default)] + networks: HashMap, + #[serde(default)] + num_procs: u64, + preread: String, + read: String, + // todo: add storage_stats type + storage_stats: HashMap, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct BlkioStats { + io_merged_recursive: Option, + io_queue_recursive: Option, + io_service_bytes_recursive: Option>, + io_service_time_recursive: Option, + io_serviced_recursive: Option, + io_time_recursive: Option, + io_wait_time_recursive: Option, + sectors_recursive: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all(deserialize = "lowercase"))] +pub enum IoDirection { + Read, + Write, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct IoServiceBytesRecursive { + major: u64, + minor: u64, + op: IoDirection, + value: u64, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct CpuStats { + cpu_usage: CpuUsage, + #[serde(default)] + online_cpus: u64, + #[serde(default)] + system_cpu_usage: u64, + throttling_data: ThrottlingData, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct CpuUsage { + total_usage: u64, + usage_in_kernelmode: u64, + usage_in_usermode: u64, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ThrottlingData { + periods: u64, + throttled_periods: u64, + throttled_time: u64, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct MemoryStats { + #[serde(default)] + limit: u64, + stats: Option, + #[serde(default)] + usage: u64, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct MemoryStatsDetails { + active_anon: u64, + active_file: u64, + // ... (Include all other fields in the memory_stats.stats here) + workingset_refault: u64, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct NetworkStats { + rx_bytes: u64, + rx_dropped: u64, + rx_errors: u64, + rx_packets: u64, + tx_bytes: u64, + tx_dropped: u64, + tx_errors: u64, + tx_packets: u64, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct PidsStats { + current: Option, + limit: Option, +} + +impl DockerStats { + pub fn post_metrics(&self, metrics: &DockerMetrics) { + let cpu_delta = + self.cpu_stats.cpu_usage.total_usage - self.precpu_stats.cpu_usage.total_usage; + let system_cpu_delta = self.cpu_stats.system_cpu_usage - self.precpu_stats.system_cpu_usage; + + let cpu_percent = if system_cpu_delta > 0 { + (cpu_delta as f64 / system_cpu_delta as f64) * self.cpu_stats.online_cpus as f64 * 100.0 + } else { + 0_f64 + }; + + metrics + .container_cpu_perc + .with_label_values(&[&self.name]) + .set(cpu_percent); + + let memoy_percent = if self.memory_stats.limit > 0 { + (self.memory_stats.usage as f64 / self.memory_stats.limit as f64) * 100.0 + } else { + 0_f64 + }; + + metrics + .container_mem_percentage + .with_label_values(&[&self.name]) + .set(memoy_percent); + + metrics + .container_mem_used + .with_label_values(&[&self.name]) + .set(self.memory_stats.usage as f64); + + metrics + .container_mem_available + .with_label_values(&[&self.name]) + .set(self.memory_stats.limit as f64); + + if let Some(io_service_bytes_recursive) = &self.blkio_stats.io_service_bytes_recursive { + let (read, written) = io_service_bytes_recursive.iter().fold( + (0, 0), + |(read, written), io_service_bytes_recursive| match &io_service_bytes_recursive.op { + IoDirection::Read => (read + io_service_bytes_recursive.value, written), + IoDirection::Write => (read, written + io_service_bytes_recursive.value), + }, + ); + + metrics + .container_block_io_read + .with_label_values(&[&self.name]) + .set(read as f64); + metrics + .container_block_io_write + .with_label_values(&[&self.name]) + .set(written as f64); + } + + self.networks.iter().for_each(|(iface, stats)| { + metrics + .container_net_io_received + .with_label_values(&[&self.name, iface]) + .set(stats.rx_bytes as f64); + metrics + .container_net_io_transmitted + .with_label_values(&[&self.name, iface]) + .set(stats.tx_bytes as f64); + }) + } +} diff --git a/src/docker/method/mod.rs b/src/docker/method/mod.rs new file mode 100644 index 0000000..033d197 --- /dev/null +++ b/src/docker/method/mod.rs @@ -0,0 +1,27 @@ +use crate::{ + docker::method::{ + get_containers::models::{GetContainersRequest, GetContainersResponse}, + get_stats::models::{GetContainerStatsRequest, GetContainerStatsResponse}, + }, + error::SystemExporterResult, +}; +use async_trait::async_trait; + +pub mod get_containers; +pub mod get_stats; + +#[async_trait] +pub trait DockerClientStatsTrait { + async fn get_container_stats( + &self, + req: GetContainerStatsRequest, + ) -> SystemExporterResult; +} + +#[async_trait] +pub trait DockerClientContainersTrait { + async fn get_containers( + &self, + req: GetContainersRequest, + ) -> SystemExporterResult; +} diff --git a/src/docker/metrics.rs b/src/docker/metrics.rs index 0d40960..8977707 100644 --- a/src/docker/metrics.rs +++ b/src/docker/metrics.rs @@ -39,14 +39,14 @@ impl DockerMetrics { "container_net_io_received", "Container Network I/O Received" ), - &["container"], + &["container", "interface"], )?; let container_net_io_transmitted = GaugeVec::new( opts!( "container_net_io_transmitted", "Container Network I/O Transmitted" ), - &["container"], + &["container", "interface"], )?; let container_cpu_perc = GaugeVec::new( opts!("container_cpu_perc", "Container CPU Percentage"), diff --git a/src/docker/mod.rs b/src/docker/mod.rs index f2cb2a2..7ae70f7 100644 --- a/src/docker/mod.rs +++ b/src/docker/mod.rs @@ -1,46 +1,64 @@ -use rust_decimal::Decimal; -use serde::Deserialize; +use crate::{ + docker::method::{DockerClientContainersTrait, DockerClientStatsTrait}, + error::{SystemExporterError, SystemExporterResult}, +}; +use async_trait::async_trait; +use http::Method; +use serde::{de::DeserializeOwned, Serialize}; +use url::Url; pub mod collector; +pub mod method; pub mod metrics; -mod util; -#[derive(Debug, Deserialize)] -pub struct DockerStats { - #[serde(rename = "BlockIO", with = "util::block_io_format")] - pub block_io: BlockIo, - #[serde(rename = "CPUPerc", with = "util::perc_format")] - pub cpu_perc: Decimal, - #[serde(rename = "Container")] - pub container: String, - #[serde(rename = "ID")] - pub id: String, - #[serde(rename = "MemPerc", with = "util::perc_format")] - pub mem_perc: Decimal, - #[serde(rename = "MemUsage", with = "util::mem_usage_format")] - pub mem_usage: MemUsage, - #[serde(rename = "Name")] - pub name: String, - #[serde(rename = "NetIO", with = "util::net_io_format")] - pub net_io: NetIo, - // #[serde(rename = "PIDs")] - // pub pids: String, +#[derive(Clone)] +pub struct DockerClient { + pub docker_daemon_host: String, } -#[derive(Debug, Deserialize)] -pub struct BlockIo { - pub read: Decimal, - pub write: Decimal, +impl DockerClient { + pub fn from_addr(docker_daemon_host: &str) -> Self { + DockerClient { + docker_daemon_host: docker_daemon_host.to_owned(), + } + } + + pub async fn send_request( + &self, + method: Method, + url: &str, + body: U, + ) -> SystemExporterResult + where + T: DeserializeOwned, + U: Serialize, + { + // TODO: support unix sockets + if self.docker_daemon_host.starts_with("unix:") { + return Err(SystemExporterError::UnsupportedProtocol( + self.docker_daemon_host.clone(), + )); + } + + let url = Url::parse(format!("{}/{url}", self.docker_daemon_host).as_str())?; + + let res = reqwest::Client::new() + .request(method, url) + .body(serde_json::to_string(&body)?) + .header(reqwest::header::CONTENT_TYPE, "application/json") + .send() + .await?; + + let json_value: serde_json::Value = res.json().await?; + + match serde_json::from_value(json_value.clone()) { + Ok(value) => Ok(value), + Err(_) => Err(SystemExporterError::RemoteServerError( + serde_json::from_value(json_value)?, + )), + } + } } -#[derive(Debug, Deserialize)] -pub struct MemUsage { - pub used: Decimal, - pub total: Decimal, -} - -#[derive(Debug, Deserialize)] -pub struct NetIo { - pub received: Decimal, - pub transmitted: Decimal, -} +#[async_trait] +pub trait DockerClientTrait: DockerClientStatsTrait + DockerClientContainersTrait {} diff --git a/src/docker/util.rs b/src/docker/util.rs deleted file mode 100644 index b0cf452..0000000 --- a/src/docker/util.rs +++ /dev/null @@ -1,193 +0,0 @@ -use crate::docker::{metrics::DockerMetrics, DockerStats}; -use rust_decimal::Decimal; -use std::str::FromStr; - -pub(crate) mod block_io_format { - use crate::docker::{util::parse_to_decimal, BlockIo}; - use serde::{self, Deserialize, Deserializer}; - - pub fn deserialize<'de, D>(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - let parts: Vec<&str> = s.split(" / ").collect(); - if parts.len() == 2 { - match (parse_to_decimal(parts[0]), parse_to_decimal(parts[1])) { - (Ok(read), Ok(write)) => Ok(BlockIo { read, write }), - _ => Err(serde::de::Error::custom("Invalid decimal format")), - } - } else { - Err(serde::de::Error::custom("Invalid format")) - } - } -} - -pub(crate) mod mem_usage_format { - use crate::docker::{util::parse_to_decimal, MemUsage}; - use serde::{self, Deserialize, Deserializer}; - - pub fn deserialize<'de, D>(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - let parts: Vec<&str> = s.split(" / ").collect(); - if parts.len() == 2 { - match (parse_to_decimal(parts[0]), parse_to_decimal(parts[1])) { - (Ok(used), Ok(total)) => Ok(MemUsage { used, total }), - _ => Err(serde::de::Error::custom("Invalid decimal format")), - } - } else { - Err(serde::de::Error::custom("Invalid format")) - } - } -} - -pub(crate) mod net_io_format { - use crate::docker::{util::parse_to_decimal, NetIo}; - use serde::{self, Deserialize, Deserializer}; - - pub fn deserialize<'de, D>(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - let parts: Vec<&str> = s.split(" / ").collect(); - if parts.len() == 2 { - match (parse_to_decimal(parts[0]), parse_to_decimal(parts[1])) { - (Ok(received), Ok(transmitted)) => Ok(NetIo { - received, - transmitted, - }), - _ => Err(serde::de::Error::custom("Invalid decimal format")), - } - } else { - Err(serde::de::Error::custom("Invalid format")) - } - } -} - -pub(crate) mod perc_format { - use rust_decimal::Decimal; - use serde::{self, Deserialize, Deserializer}; - use std::str::FromStr; - - pub fn deserialize<'de, D>(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - let number_part: &str = s.trim_end_matches('%'); - match Decimal::from_str(number_part) { - Ok(value) => Ok(value), - Err(_) => Err(serde::de::Error::custom("Invalid decimal format")), - } - } -} - -fn parse_to_decimal(input: &str) -> Result { - let multiplier = if input.ends_with("kB") { - 1_000 - } else if input.ends_with("MB") { - 1_000_000 - } else if input.ends_with("GB") { - 1_000_000_000 - } else if input.ends_with("KiB") { - 1_000 - } else if input.ends_with("MiB") { - 1_000_000 - } else if input.ends_with("GiB") { - 1_000_000_000 - } else { - 1 - }; - - let number_part: &str = input.trim_end_matches(|c: char| !c.is_numeric() && c != '.'); - let value = Decimal::from_str(number_part)?; - - Ok(value * Decimal::from(multiplier)) -} - -impl DockerStats { - pub fn post_metrics(&self, metrics: &DockerMetrics) { - metrics - .container_cpu_perc - .with_label_values(&[&self.name]) - .set( - self.cpu_perc - .to_string() - .parse::() - .expect("Failed to parse"), - ); - metrics - .container_mem_percentage - .with_label_values(&[&self.name]) - .set( - self.mem_perc - .to_string() - .parse::() - .expect("Failed to parse"), - ); - metrics - .container_mem_used - .with_label_values(&[&self.name]) - .set( - self.mem_usage - .used - .to_string() - .parse::() - .expect("Failed to parse"), - ); - metrics - .container_mem_available - .with_label_values(&[&self.name]) - .set( - self.mem_usage - .total - .to_string() - .parse::() - .expect("Failed to parse"), - ); - metrics - .container_block_io_read - .with_label_values(&[&self.name]) - .set( - self.block_io - .read - .to_string() - .parse::() - .expect("Failed to parse"), - ); - metrics - .container_block_io_write - .with_label_values(&[&self.name]) - .set( - self.block_io - .write - .to_string() - .parse::() - .expect("Failed to parse"), - ); - metrics - .container_net_io_received - .with_label_values(&[&self.name]) - .set( - self.net_io - .received - .to_string() - .parse::() - .expect("Failed to parse"), - ); - metrics - .container_net_io_transmitted - .with_label_values(&[&self.name]) - .set( - self.net_io - .transmitted - .to_string() - .parse::() - .expect("Failed to parse"), - ); - } -} diff --git a/src/error.rs b/src/error.rs index 38c06f6..de3c4a7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -10,6 +10,14 @@ pub enum SystemExporterError { Prometheus(#[from] prometheus::Error), #[error(transparent)] AddrParse(#[from] std::net::AddrParseError), + #[error(transparent)] + UrlParse(#[from] url::ParseError), + #[error(transparent)] + Reqwest(#[from] reqwest::Error), + #[error("The remote server responded with error body: {0}")] + RemoteServerError(serde_json::Value), + #[error("The specified protocol: {0} is not supported")] + UnsupportedProtocol(String), } pub type SystemExporterResult = Result; diff --git a/src/main.rs b/src/main.rs index 9ee6813..1c68588 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,42 +1,32 @@ -use crate::{error::SystemExporterResult, metrics::Metrics}; +use crate::{ + cli::Args, + docker::{collector::DockerCollector, DockerClient}, + error::SystemExporterResult, + metrics::Metrics, + server::Server, +}; use clap::Parser; use std::{sync::Arc, time::Duration}; -use crate::docker::collector::DockerCollector; -use crate::server::Server; +mod cli; mod docker; mod error; mod metrics; mod server; -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] -pub struct Args { - /// The address to bind the metrics server on - #[arg(long, default_value = "0.0.0.0")] - pub bind_address: String, - /// The port to bind the metrics server to - #[arg(long, default_value_t = 45454)] - pub bind_port: u32, - /// The metrics registry prefix - #[arg(long, default_value = "system_exporter")] - pub metrics_prefix: String, - /// The interval to poll docker stats - #[arg(long, default_value_t = 5)] - pub docker_stats_seconds: u64, -} - #[tokio::main] async fn main() -> SystemExporterResult<()> { let args = Args::parse(); + let client = DockerClient::from_addr(args.docker_server.as_ref()); + let metrics = Arc::new(Metrics::new(args.metrics_prefix.as_str())?); let server = Server::new(&args, metrics.clone())?; - let collector = DockerCollector::new(metrics); + let collector = DockerCollector::new(client, metrics); tokio::spawn(async move { loop { - let _ = collector.poll(); + let _ = collector.poll().await; tokio::time::sleep(Duration::from_secs(args.docker_stats_seconds)).await; } }); -- 2.49.1