Get Stats From Docker Daemon via HTTP #1

Merged
stphnsmpsn merged 1 commits from feature/get_stats_from_docker_daemon into master 2023-09-12 16:58:53 -03:00
16 changed files with 893 additions and 286 deletions

372
Cargo.lock generated
View File

@@ -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"

View File

@@ -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"] }
tower-http = { version = "0.3.5", features = ["full"] }
url = "2.3.1"

22
src/cli.rs Normal file
View File

@@ -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,
}

View File

@@ -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<Metrics>,
client: DockerClient,
}
impl DockerCollector {
pub fn new(metrics: Arc<Metrics>) -> Self {
Self { metrics }
pub fn new(client: DockerClient, metrics: Arc<Metrics>) -> Self {
Self { client, metrics }
}
pub fn poll(&self) -> SystemExporterResult<Vec<DockerStats>> {
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<Vec<DockerStats>> {
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::<Vec<DockerStats>>(&output_str)?;
let container_ids = get_containers_response
.containers
.into_iter()
.map(|e| e.id)
.collect::<Vec<String>>();
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)
}
}

View File

@@ -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<GetContainersResponse> {
let containers: Vec<DockerContainer> = self
.send_request(http::Method::GET, "containers/json?all=false", _req)
.await?;
Ok(GetContainersResponse { containers })
}
}

View File

@@ -0,0 +1,2 @@
mod implementation;
pub mod models;

View File

@@ -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<DockerContainer>,
}
#[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<String, String>,
pub mounts: Vec<Mount>,
pub names: Vec<String>,
pub network_settings: NetworkSettings,
pub ports: Vec<Port>,
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<String>, // 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<String>, // 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,
}

View File

@@ -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<GetContainerStatsResponse> {
let stats: DockerStats = self
.send_request(
http::Method::GET,
format!("containers/{}/stats?stream=false", req.container_id).as_str(),
req,
)
.await?;
Ok(GetContainerStatsResponse { stats })
}
}

View File

@@ -0,0 +1,2 @@
mod implementation;
pub mod models;

View File

@@ -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<String, NetworkStats>,
#[serde(default)]
num_procs: u64,
preread: String,
read: String,
// todo: add storage_stats type
storage_stats: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct BlkioStats {
io_merged_recursive: Option<serde_json::Value>,
io_queue_recursive: Option<serde_json::Value>,
io_service_bytes_recursive: Option<Vec<IoServiceBytesRecursive>>,
io_service_time_recursive: Option<serde_json::Value>,
io_serviced_recursive: Option<serde_json::Value>,
io_time_recursive: Option<serde_json::Value>,
io_wait_time_recursive: Option<serde_json::Value>,
sectors_recursive: Option<serde_json::Value>,
}
#[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<MemoryStatsDetails>,
#[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<u64>,
limit: Option<u64>,
}
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);
})
}
}

27
src/docker/method/mod.rs Normal file
View File

@@ -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<GetContainerStatsResponse>;
}
#[async_trait]
pub trait DockerClientContainersTrait {
async fn get_containers(
&self,
req: GetContainersRequest,
) -> SystemExporterResult<GetContainersResponse>;
}

View File

@@ -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"),

View File

@@ -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<T, U>(
&self,
method: Method,
url: &str,
body: U,
) -> SystemExporterResult<T>
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 {}

View File

@@ -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<BlockIo, D::Error>
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<MemUsage, D::Error>
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<NetIo, D::Error>
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<Decimal, D::Error>
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<Decimal, rust_decimal::Error> {
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::<f64>()
.expect("Failed to parse"),
);
metrics
.container_mem_percentage
.with_label_values(&[&self.name])
.set(
self.mem_perc
.to_string()
.parse::<f64>()
.expect("Failed to parse"),
);
metrics
.container_mem_used
.with_label_values(&[&self.name])
.set(
self.mem_usage
.used
.to_string()
.parse::<f64>()
.expect("Failed to parse"),
);
metrics
.container_mem_available
.with_label_values(&[&self.name])
.set(
self.mem_usage
.total
.to_string()
.parse::<f64>()
.expect("Failed to parse"),
);
metrics
.container_block_io_read
.with_label_values(&[&self.name])
.set(
self.block_io
.read
.to_string()
.parse::<f64>()
.expect("Failed to parse"),
);
metrics
.container_block_io_write
.with_label_values(&[&self.name])
.set(
self.block_io
.write
.to_string()
.parse::<f64>()
.expect("Failed to parse"),
);
metrics
.container_net_io_received
.with_label_values(&[&self.name])
.set(
self.net_io
.received
.to_string()
.parse::<f64>()
.expect("Failed to parse"),
);
metrics
.container_net_io_transmitted
.with_label_values(&[&self.name])
.set(
self.net_io
.transmitted
.to_string()
.parse::<f64>()
.expect("Failed to parse"),
);
}
}

View File

@@ -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<T, E = SystemExporterError> = Result<T, E>;

View File

@@ -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;
}
});