summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorToby Vincent <tobyv@tobyvin.dev>2024-09-26 17:31:16 -0500
committerToby Vincent <tobyv@tobyvin.dev>2024-09-26 17:31:16 -0500
commitfd992d7e3c03f37fbcafe9d3f26c72a2ead3b2a7 (patch)
treef3e29427d1bbe4a8d6e050abbd9f66afb5fa2152
parentcbfca14b38806798847e3f2008038b25194a9b8b (diff)
feat!: impl full api
-rw-r--r--Cargo.lock448
-rw-r--r--Cargo.toml10
-rw-r--r--assets/index.css61
-rw-r--r--assets/index.html30
-rw-r--r--assets/index.js87
-rw-r--r--config.toml25
-rw-r--r--index.html109
-rw-r--r--src/api.rs73
-rw-r--r--src/api/services.rs40
-rw-r--r--src/error.rs22
-rw-r--r--src/lib.rs82
-rw-r--r--src/main.rs93
-rw-r--r--src/service.rs203
-rw-r--r--src/service/http.rs49
-rw-r--r--src/service/systemd.rs33
-rw-r--r--src/service/tcp.rs28
16 files changed, 1024 insertions, 369 deletions
diff --git a/Cargo.lock b/Cargo.lock
index da7d5be..db02f18 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -18,6 +18,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "async-trait"
+version = "0.1.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -30,6 +50,83 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
[[package]]
+name = "axum"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f43644eed690f5374f1af436ecd6aea01cd201f6fbdf0178adaf6907afb2cec"
+dependencies = [
+ "async-trait",
+ "axum-core",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustversion",
+ "serde",
+ "serde_json",
+ "serde_path_to_error",
+ "serde_urlencoded",
+ "sync_wrapper 1.0.1",
+ "tokio",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e6b8ba012a258d63c9adfa28b9ddcf66149da6f986c5b5452e629d5ee64bf00"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "rustversion",
+ "sync_wrapper 1.0.1",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-extra"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73c3220b188aea709cf1b6c5f9b01c3bd936bb08bd2b5184a12b35ac8131b1f9"
+dependencies = [
+ "axum",
+ "axum-core",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "serde",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
name = "backtrace"
version = "0.3.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -161,6 +258,21 @@ dependencies = [
]
[[package]]
+name = "futures"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
name = "futures-channel"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -177,12 +289,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
[[package]]
+name = "futures-executor"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
name = "futures-io"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
[[package]]
+name = "futures-macro"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
name = "futures-sink"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -200,8 +334,10 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [
+ "futures-channel",
"futures-core",
"futures-io",
+ "futures-macro",
"futures-sink",
"futures-task",
"memchr",
@@ -293,12 +429,24 @@ dependencies = [
]
[[package]]
+name = "http-range-header"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ce4ef31cda248bbdb6e6820603b82dfcd9e833db65a43e997a0ccec777d11fe"
+
+[[package]]
name = "httparse"
version = "1.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9"
[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
name = "hyper"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -311,6 +459,7 @@ dependencies = [
"http",
"http-body",
"httparse",
+ "httpdate",
"itoa",
"pin-project-lite",
"smallvec",
@@ -353,9 +502,9 @@ dependencies = [
[[package]]
name = "hyper-util"
-version = "0.1.8"
+version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da62f120a8a37763efb0cf8fdf264b884c7b8b9ac8660b900c8661030c00e6ba"
+checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b"
dependencies = [
"bytes",
"futures-channel",
@@ -366,7 +515,6 @@ dependencies = [
"pin-project-lite",
"socket2",
"tokio",
- "tower",
"tower-service",
"tracing",
]
@@ -413,6 +561,12 @@ dependencies = [
]
[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
name = "libc"
version = "0.2.158"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -437,6 +591,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "155db5e86c6e45ee456bf32fad5a290ee1f7151c2faca27ea27097568da67d1a"
[[package]]
+name = "matchers"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
+dependencies = [
+ "regex-automata 0.1.10",
+]
+
+[[package]]
+name = "matchit"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
+
+[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -449,6 +618,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
+name = "mime_guess"
+version = "2.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
name = "miniz_oxide"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -487,6 +666,16 @@ dependencies = [
]
[[package]]
+name = "nu-ansi-term"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+dependencies = [
+ "overload",
+ "winapi",
+]
+
+[[package]]
name = "object"
version = "0.36.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -546,30 +735,16 @@ dependencies = [
]
[[package]]
-name = "percent-encoding"
-version = "2.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
-
-[[package]]
-name = "pin-project"
-version = "1.1.5"
+name = "overload"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
-dependencies = [
- "pin-project-internal",
-]
+checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
-name = "pin-project-internal"
-version = "1.1.5"
+name = "percent-encoding"
+version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pin-project-lite"
@@ -608,6 +783,50 @@ dependencies = [
]
[[package]]
+name = "regex"
+version = "1.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata 0.4.7",
+ "regex-syntax 0.8.4",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
+dependencies = [
+ "regex-syntax 0.6.29",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax 0.8.4",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
+
+[[package]]
name = "reqwest"
version = "0.12.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -639,7 +858,7 @@ dependencies = [
"serde",
"serde_json",
"serde_urlencoded",
- "sync_wrapper",
+ "sync_wrapper 1.0.1",
"system-configuration",
"tokio",
"tokio-native-tls",
@@ -726,6 +945,12 @@ dependencies = [
]
[[package]]
+name = "rustversion"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
+
+[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -796,6 +1021,16 @@ dependencies = [
]
[[package]]
+name = "serde_path_to_error"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6"
+dependencies = [
+ "itoa",
+ "serde",
+]
+
+[[package]]
name = "serde_spanned"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -817,6 +1052,15 @@ dependencies = [
]
[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -857,11 +1101,21 @@ checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
name = "statsrv"
version = "0.1.0"
dependencies = [
+ "axum",
+ "axum-extra",
+ "futures",
+ "futures-util",
+ "hyper-util",
"main_error",
"reqwest",
"serde",
+ "serde_json",
"thiserror",
+ "tokio",
"toml",
+ "tower-http",
+ "tracing",
+ "tracing-subscriber",
]
[[package]]
@@ -883,6 +1137,12 @@ dependencies = [
[[package]]
name = "sync_wrapper"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
+
+[[package]]
+name = "sync_wrapper"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
@@ -945,6 +1205,16 @@ dependencies = [
]
[[package]]
+name = "thread_local"
+version = "1.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+]
+
+[[package]]
name = "tinyvec"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -971,10 +1241,22 @@ dependencies = [
"mio",
"pin-project-lite",
"socket2",
+ "tokio-macros",
"windows-sys 0.52.0",
]
[[package]]
+name = "tokio-macros"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1044,17 +1326,43 @@ dependencies = [
[[package]]
name = "tower"
-version = "0.4.13"
+version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
+checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f"
dependencies = [
"futures-core",
"futures-util",
- "pin-project",
+ "pin-project-lite",
+ "sync_wrapper 0.1.2",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8437150ab6bbc8c5f0f519e3d5ed4aa883a83dd4cdd3d1b21f9482936046cb97"
+dependencies = [
+ "bitflags",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "http-range-header",
+ "httpdate",
+ "mime",
+ "mime_guess",
+ "percent-encoding",
"pin-project-lite",
"tokio",
+ "tokio-util",
"tower-layer",
"tower-service",
+ "tracing",
]
[[package]]
@@ -1075,17 +1383,60 @@ version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
dependencies = [
+ "log",
"pin-project-lite",
+ "tracing-attributes",
"tracing-core",
]
[[package]]
+name = "tracing-attributes"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
name = "tracing-core"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
dependencies = [
"once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
]
[[package]]
@@ -1095,6 +1446,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
+name = "unicase"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
name = "unicode-bidi"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1133,12 +1493,24 @@ dependencies = [
]
[[package]]
+name = "valuable"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
+
+[[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.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
name = "want"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1231,6 +1603,28 @@ dependencies = [
]
[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
name = "windows-registry"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 0a558a2..69f7eeb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,8 +4,18 @@ version = "0.1.0"
edition = "2021"
[dependencies]
+axum = "0.7.6"
+axum-extra = "0.9.4"
+futures = "0.3.30"
+futures-util = "0.3.30"
+hyper-util = { version = "0.1.9", features = ["tokio"] }
main_error = "0.1.2"
reqwest = { version = "0.12.7", features = ["blocking"] }
serde = { version = "1.0.210", features = ["derive"] }
+serde_json = "1.0.128"
thiserror = "1.0.63"
+tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread", "net"] }
toml = "0.8.19"
+tower-http = { version = "0.6.1", features = ["fs", "trace"] }
+tracing = "0.1.40"
+tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
diff --git a/assets/index.css b/assets/index.css
new file mode 100644
index 0000000..9c170e4
--- /dev/null
+++ b/assets/index.css
@@ -0,0 +1,61 @@
+html {
+ background: var(--bg);
+ color: var(--fg);
+ font-size: 12pt;
+ font-family: monospace;
+ display: flex;
+ flex-direction: column;
+}
+body {
+ height: calc(100vh - 1rem);
+ width: calc(100vw - 4rem);
+ max-width: 640px;
+ margin: 0 2rem 1rem;
+ line-height: 1.4;
+ align-self: center;
+}
+main {
+ display: flex;
+ flex-direction: column;
+}
+hgroup {
+ text-align: center;
+}
+table {
+ width: 100%;
+ max-width: 500px;
+ align-self: center;
+ border-collapse: collapse;
+}
+tr {
+ border: 2px solid #ddd;
+}
+td {
+ padding: 5px;
+}
+td:first-of-type + td {
+ text-align: right;
+}
+.ok {
+ color: #228b22;
+}
+.warning {
+ color: #ff8c00;
+}
+.error {
+ color: #dc143c;
+}
+
+#status {
+ color: #fff;
+ background: #228b22;
+}
+#status.ok {
+ background: #228b22;
+}
+#status.warning {
+ background: #ff8c00;
+}
+#status.error {
+ background: #dc143c;
+}
diff --git a/assets/index.html b/assets/index.html
new file mode 100644
index 0000000..c168318
--- /dev/null
+++ b/assets/index.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>status.tobyvin.dev</title>
+ <link rel="stylesheet" type="text/css" href="index.css" />
+ <script src="index.js"></script>
+ </head>
+ <body>
+ <main>
+ <header>
+ <hgroup>
+ <h1>tobyvin.dev Status</h1>
+ </hgroup>
+ </header>
+
+ <table>
+ <tr id="status">
+ <td><b>Status</b></td>
+ <td id="issues">No issues detected</td>
+ </tr>
+ </table>
+
+ <hr></hr>
+
+ <table id="services"></table>
+ </main>
+ </body>
+</html>
diff --git a/assets/index.js b/assets/index.js
new file mode 100644
index 0000000..173ed72
--- /dev/null
+++ b/assets/index.js
@@ -0,0 +1,87 @@
+/**
+ * @typedef {Object} Check
+ * @property {String} status - 'pass'|'fail'|'warn'
+ * @property {String} output - Details. Not present if 'pass'
+ */
+
+/**
+ * @typedef {Check} HealthCheck
+ * @property {Map<String, Check>} checks
+ */
+
+async function getHealthCheck() {
+ const url = "api/healthcheck";
+ try {
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(`Response status: ${response.status}`);
+ }
+
+ const json = await response.json();
+ return json;
+ } catch (error) {
+ console.error(error.message);
+ }
+}
+
+function updateStatus(check) {
+ const statusElm = document.getElementById("status");
+ const issuesElm = document.getElementById("issues");
+ switch (check.status) {
+ case "pass":
+ issuesElm.textContent = "No issues detected";
+ statusElm.setAttribute("class", "ok");
+ break;
+ case "fail":
+ issuesElm.textContent = `${check.output} issues detected`;
+ statusElm.setAttribute("class", "error");
+ break;
+ case "warn":
+ issuesElm.textContent = `${check.output} warnings detected`;
+ statusElm.setAttribute("class", "warning");
+ break;
+ default:
+ issuesElm.textContent = "Unknown";
+ statusElm.setAttribute("class", "warning");
+ }
+}
+
+getHealthCheck().then((healthCheck) => {
+ const table = document.getElementById("services");
+ const evtSource = new EventSource("sse");
+ updateStatus(healthCheck);
+
+ for (const [service, check] of Object.entries(healthCheck.checks)) {
+ const row = table.insertRow();
+
+ const nameNode = row.insertCell();
+ nameNode.textContent = service;
+
+ const stateNode = row.insertCell();
+ switch (check.status) {
+ case "pass":
+ stateNode.textContent = "Operational";
+ stateNode.setAttribute("class", "ok");
+ break;
+ case "fail":
+ stateNode.textContent = "Down";
+ stateNode.title = check.output;
+ stateNode.setAttribute("class", "error");
+ break;
+ case "warn":
+ stateNode.textContent = "Warning";
+ stateNode.title = check.output;
+ stateNode.setAttribute("class", "warning");
+ break;
+ default:
+ stateNode.textContent = "Unknown";
+ statusElm.setAttribute("class", "warning");
+ }
+
+ evtSource.addEventListener(service, (event) => {
+ const status = JSON.parse(event.data);
+ stateNode.textContent = status.state;
+ stateNode.title = status.output;
+ });
+ }
+});
diff --git a/config.toml b/config.toml
index 867ee91..815ae94 100644
--- a/config.toml
+++ b/config.toml
@@ -1,20 +1,7 @@
-title = "tobyvin.dev"
-template_path = "./index.html"
-output_dir = "/tmp/out"
+root = "assets"
+address = "127.0.0.1:8080"
-[[services]]
-name = "tobyvin.dev"
-type = "http"
-url = "https://tobyvin.dev"
-method = "GET"
-status = 200
-
-[[services]]
-name = "ollama"
-type = "systemd"
-service = "ollama.service"
-
-[[services]]
-name = "DNS"
-type = "tcp"
-address = "10.42.0.1:53"
+[services]
+"tobyvin.dev" = { url = "https://tobyvin.dev" }
+ollama = { service = "ollama.service" }
+dns = { address = "10.42.0.1:53" }
diff --git a/index.html b/index.html
deleted file mode 100644
index 0b0254b..0000000
--- a/index.html
+++ /dev/null
@@ -1,109 +0,0 @@
-<!doctype html>
-<html lang="en">
- <head>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <title>status.tobyvin.dev</title>
- <link rel="stylesheet" type="text/css" href="index.css" />
- <style>
- html {
- background: var(--bg);
- color: var(--fg);
- font-size: 12pt;
- font-family: monospace;
- display: flex;
- flex-direction: column;
- }
- body {
- height: calc(100vh - 1rem);
- width: calc(100vw - 4rem);
- max-width: 640px;
- display: flex;
- flex-direction: column;
- margin: 0 2rem 1rem;
- line-height: 1.4;
- align-self: center;
- }
- main {
- display: flex;
- flex-direction: column;
- }
- hgroup {
- display: flex;
- flex-direction: column;
- text-align: center;
- }
- hgroup > p {
- align-self: center;
- width: 100%;
- max-width: 400px;
- padding: 5px;
- border: 2px solid #ddd;
- }
- dl {
- width: 100%;
- max-width: 500px;
- align-self: center;
- display: grid;
- grid-template-columns: max-content auto;
- border: 2px solid #ddd;
- border-bottom: 0;
- }
- dt {
- padding: 5px;
- grid-column-start: 1;
- border-bottom: 2px solid #ddd;
- }
- dd {
- text-align: right;
- grid-column-start: 2;
- margin-left: 0;
- padding: 5px;
- border-bottom: 2px solid #ddd;
- }
- .ok {
- color: #228b22;
- }
- .warning {
- color: #ff8c00;
- }
- .error {
- color: #dc143c;
- }
-
- .status {
- color: #fff;
- }
- .status.ok {
- background: #228b22;
- }
- .status.warning {
- background: #ff8c00;
- }
- .status.error {
- background: #dc143c;
- }
- </style>
- </head>
- <body>
- <main>
- <header>
- <hgroup>
- <h1>{title} Status</h1>
- </hgroup>
- </header>
-
- <dl class="status {status}">
- <dt><b>Status</b></dt>
- <dd>{down} issue(s) detected</dd>
- </dl>
-
- <dl>
- {{services}}
- <dt title="{title}">{name}</dt>
- <dd class="{level}">{state}</dd>
- {{end}}
- </dl>
- </main>
- </body>
-</html>
diff --git a/src/api.rs b/src/api.rs
new file mode 100644
index 0000000..e6a91ba
--- /dev/null
+++ b/src/api.rs
@@ -0,0 +1,73 @@
+use std::collections::HashMap;
+
+use axum::{extract::State, response::IntoResponse, Json};
+use serde::{Deserialize, Serialize};
+
+use crate::{service::Services, Check, Error, Status};
+
+pub mod services;
+
+pub fn router() -> axum::Router<Services> {
+ use axum::routing::get;
+
+ axum::Router::new()
+ .route("/healthcheck", get(healthcheck))
+ .merge(services::router())
+ .fallback(fallback)
+}
+
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+pub struct Health {
+ pub status: Status,
+ pub output: Option<String>,
+ pub checks: HashMap<String, Check>,
+}
+
+impl<T: std::error::Error> From<T> for Health {
+ fn from(value: T) -> Self {
+ Health {
+ status: Status::Fail,
+ output: Some(value.to_string()),
+ ..Default::default()
+ }
+ }
+}
+
+impl IntoResponse for Health {
+ fn into_response(self) -> axum::response::Response {
+ Json(self).into_response()
+ }
+}
+
+pub async fn healthcheck(State(services): State<Services>) -> Health {
+ let checks = match services.check().await {
+ Ok(c) => c,
+ Err(err) => {
+ return Health {
+ status: Status::Fail,
+ output: Some(err.to_string()),
+ ..Default::default()
+ }
+ }
+ };
+
+ let (status, output) = match checks
+ .values()
+ .filter(|s| !matches!(s.status, Status::Pass))
+ .count()
+ {
+ 0 => (Status::Pass, None),
+ 1 => (Status::Fail, Some("1 issue detected".to_string())),
+ n => (Status::Fail, Some(format!("{n} issues detected"))),
+ };
+
+ Health {
+ status,
+ output,
+ checks,
+ }
+}
+
+pub async fn fallback(uri: axum::http::Uri) -> Error {
+ Error::RouteNotFound(uri)
+}
diff --git a/src/api/services.rs b/src/api/services.rs
new file mode 100644
index 0000000..59e891f
--- /dev/null
+++ b/src/api/services.rs
@@ -0,0 +1,40 @@
+use std::collections::HashMap;
+
+use axum::{
+ extract::{Path, Query, State},
+ Json, Router,
+};
+use axum_extra::routing::Resource;
+use serde::{Deserialize, Serialize};
+
+use crate::{service::Services, Check, Error, Status};
+
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+pub struct ServiceQuery {
+ pub name: Option<String>,
+ pub state: Option<Status>,
+}
+
+pub fn router() -> Router<Services> {
+ Resource::named("services").index(index).show(show).into()
+}
+
+pub async fn index(
+ Query(query): Query<ServiceQuery>,
+ State(services): State<Services>,
+) -> Result<Json<HashMap<String, Check>>, Error> {
+ services
+ .check_filtered(|name| (!query.name.as_ref().is_some_and(|s| s != name)))
+ .await
+ .map(Json)
+}
+
+pub async fn show(
+ Path(name): Path<String>,
+ State(services): State<Services>,
+) -> Result<Check, Error> {
+ services
+ .check_one(&name)
+ .await
+ .ok_or_else(|| Error::ServiceNotFound(name))?
+}
diff --git a/src/error.rs b/src/error.rs
index ef30b97..109c944 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -13,4 +13,26 @@ pub enum Error {
#[error("Invalid HTTP method")]
Method,
+
+ #[error("Axum error: {0}")]
+ Axum(#[from] axum::Error),
+
+ #[error("Route not found: {0}")]
+ RouteNotFound(axum::http::Uri),
+
+ #[error("Service not found: {0}")]
+ ServiceNotFound(String),
+}
+
+impl axum::response::IntoResponse for Error {
+ fn into_response(self) -> axum::response::Response {
+ use axum::http::StatusCode;
+
+ let status = match self {
+ Self::RouteNotFound(_) | Self::ServiceNotFound(_) => StatusCode::NOT_FOUND,
+ _ => StatusCode::INTERNAL_SERVER_ERROR,
+ };
+
+ (status, self.to_string()).into_response()
+ }
}
diff --git a/src/lib.rs b/src/lib.rs
index 3437dca..2c9fa91 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,61 +1,39 @@
-use std::ops::Range;
+use std::path::PathBuf;
-pub use crate::{
- error::{Error, Result},
- service::{Service, Status},
-};
+use serde::{Deserialize, Serialize};
+use service::Services;
+use tower_http::services::ServeDir;
+pub use crate::error::{Error, Result};
+
+pub mod api;
pub mod error;
pub mod service;
-pub fn generate(title: String, mut services: Vec<Service>, template: String) -> String {
- let client = reqwest::blocking::Client::new();
-
- let [up, down, unknown] = std::thread::scope(|s| {
- let mut handles = Vec::new();
- for service in services.iter_mut() {
- handles.push(s.spawn(|| service.check(client.clone())));
- }
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum Status {
+ #[default]
+ Pass,
+ Fail,
+ Warn,
+}
- handles
- .into_iter()
- .map(|h| h.join().expect("Joining thread"))
- .fold([0, 0, 0], |[up, down, unknown], res| match res {
- Ok(true) => [up + 1, down, unknown],
- Ok(false) => [up, down + 1, unknown],
- Err(_) => [up, down, unknown + 1],
- })
- });
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+pub struct Check {
+ pub status: Status,
+ pub output: Option<String>,
+}
- template
- .match_indices("{{services}}")
- .zip(template.match_indices("{{end}}"))
- .map(|(start, stop)| {
- (
- start.0 + start.1.len()..stop.0,
- start.0..stop.0 + stop.1.len(),
- )
- })
- .collect::<Vec<_>>()
- .into_iter()
- .fold(template, |mut template, (Range { start, end }, outer)| {
- let replace_with = services
- .iter()
- .map(|service| {
- template[start..end]
- .replace("{name}", &service.name)
- .replace("{title}", &service.kind.to_string())
- .replace("{state}", &service.state.to_string())
- .replace("{level}", &service.state.as_level())
- })
- .collect::<String>();
+impl axum::response::IntoResponse for Check {
+ fn into_response(self) -> axum::response::Response {
+ axum::Json(self).into_response()
+ }
+}
- template.replace_range(outer, &replace_with);
- template
- })
- .replace("{title}", &title)
- .replace("{status}", if down > 0 { "error" } else { "ok" })
- .replace("{up}", &up.to_string())
- .replace("{down}", &down.to_string())
- .replace("{unknown}", &unknown.to_string())
+pub fn router(root: PathBuf) -> axum::Router<Services> {
+ axum::Router::new()
+ .nest_service("/", ServeDir::new(root))
+ .nest("/api", api::router())
+ .layer(tower_http::trace::TraceLayer::new_for_http())
}
diff --git a/src/main.rs b/src/main.rs
index 2ff9fd3..97ed111 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,42 +1,69 @@
-use std::{fs::File, io::Write, path::PathBuf};
+use std::{fs::File, path::PathBuf};
+use tracing::level_filters::LevelFilter;
+use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
-use statsrv::Service;
+use statsrv::service::Services;
+
+#[cfg(not(debug_assertions))]
+const DEFAULT_CONFIG: &str = "/etc/statsrv.toml";
+#[cfg(debug_assertions)]
+const DEFAULT_CONFIG: &str = "./config.toml";
+
+#[tokio::main]
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
+ tracing_subscriber::registry()
+ .with(
+ EnvFilter::builder()
+ .with_default_directive(LevelFilter::INFO.into())
+ .from_env_lossy(),
+ )
+ .with(tracing_subscriber::fmt::layer())
+ .init();
+
+ let config = match Config::parse() {
+ Ok(c) => c,
+ Err(err) => {
+ tracing::debug!("Failed to read config file: `{err}`");
+ tracing::debug!("Using default config values");
+ Default::default()
+ }
+ };
+
+ let router = statsrv::router(config.root).with_state(config.services);
+
+ let listener = tokio::net::TcpListener::bind(config.address).await.unwrap();
+ tracing::info!("listening on {}", listener.local_addr().unwrap());
+
+ axum::serve(listener, router).await.map_err(Into::into)
+}
#[derive(Debug, Clone, serde::Deserialize)]
+#[serde(default)]
pub struct Config {
- pub title: String,
- pub template_path: PathBuf,
- pub output_dir: Option<PathBuf>,
- pub address: Option<String>,
- pub services: Vec<Service>,
+ pub root: PathBuf,
+ pub address: String,
+ pub services: Services,
}
-fn main() -> Result<(), main_error::MainError> {
- let mut args = std::env::args().skip(1);
-
- let config_path = args
- .next()
- .unwrap_or_else(|| "/etc/statsrv.toml".to_string());
- let config_file = File::open(config_path)?;
- let config_toml = std::io::read_to_string(config_file)?;
- let Config {
- title,
- template_path: template,
- output_dir,
- address: _,
- services,
- } = toml::from_str(&config_toml)?;
-
- let template_file = File::open(template)?;
- let template = std::io::read_to_string(template_file)?;
- let status_page = statsrv::generate(title, services, template);
-
- if let Some(output_dir) = output_dir {
- std::fs::create_dir_all(&output_dir)?;
- let mut html_writer = File::create(output_dir.join("index.html"))?;
-
- html_writer.write_all(status_page.as_bytes())?;
+impl Config {
+ fn parse() -> Result<Self, Box<dyn std::error::Error>> {
+ let config_path = std::env::args().nth(1).unwrap_or_else(|| {
+ tracing::debug!("Falling back to default config location");
+ DEFAULT_CONFIG.to_string()
+ });
+
+ let config_file = File::open(&config_path)?;
+ let config_toml = std::io::read_to_string(config_file)?;
+ toml::from_str(&config_toml).map_err(Into::into)
}
+}
- Ok(())
+impl Default for Config {
+ fn default() -> Self {
+ Self {
+ root: PathBuf::from("./"),
+ address: String::from("127.0.0.1:8080"),
+ services: Services::new(Default::default()),
+ }
+ }
}
diff --git a/src/service.rs b/src/service.rs
index c5eb0d7..677db17 100644
--- a/src/service.rs
+++ b/src/service.rs
@@ -1,160 +1,105 @@
-use std::{fmt::Display, process::Command};
+use std::{collections::HashMap, fmt::Display};
-use reqwest::blocking::Client;
+use futures::{stream::FuturesOrdered, TryStreamExt};
+use http::Http;
use serde::Deserialize;
+use systemd::Systemd;
+use tcp::Tcp;
-use crate::Error;
+use crate::{Check, Error};
+
+pub mod http;
+pub mod systemd;
+pub mod tcp;
#[derive(Debug, Clone, Deserialize)]
-pub struct Service {
- pub name: String,
+pub struct Services {
#[serde(flatten)]
- pub kind: Kind,
- #[serde(skip)]
- pub state: State,
+ inner: HashMap<String, Service>,
+ #[serde(skip, default = "Services::default_client")]
+ client: reqwest::Client,
}
-impl Service {
- pub fn check(&mut self, client: Client) -> Result<bool, Error> {
- self.state = self.kind.get_state(client)?;
- Ok(self.state.is_operational())
+impl Services {
+ pub fn new(services: HashMap<String, Service>) -> Self {
+ let client = reqwest::Client::new();
+ Self {
+ inner: services,
+ client,
+ }
}
-}
-
-#[derive(Debug, Clone, Deserialize)]
-#[serde(tag = "type", rename_all = "lowercase")]
-pub enum Kind {
- Tcp {
- address: String,
- },
- Http {
- url: String,
- #[serde(default = "Kind::default_method")]
- method: String,
- #[serde(default = "Kind::default_code")]
- status_code: u16,
- },
- Systemd {
- service: String,
- },
-}
-impl Kind {
- fn default_method() -> String {
- "GET".to_string()
+ fn default_client() -> reqwest::Client {
+ reqwest::Client::new()
}
- fn default_code() -> u16 {
- 200
+ pub async fn check(&self) -> Result<HashMap<String, Check>, Error> {
+ let checks = self
+ .inner
+ .values()
+ .map(|service| service.check(self.client.clone()))
+ .collect::<FuturesOrdered<_>>()
+ .try_collect::<Vec<_>>()
+ .await?;
+
+ Ok(self
+ .inner
+ .keys()
+ .cloned()
+ .zip(checks)
+ .collect::<HashMap<_, _>>())
}
- pub fn get_state(&self, client: Client) -> Result<State, Error> {
- let state = match self {
- Kind::Tcp { address } => {
- if std::net::TcpStream::connect(address).is_ok() {
- State::Operational
- } else {
- State::Down("Unreachable".to_string())
- }
- }
- Kind::Http {
- method,
- url,
- status_code,
- } => {
- match client
- .request(method.parse().map_err(|_| Error::Method)?, url)
- .send()?
- .status()
- {
- s if s.as_u16() == *status_code => State::Operational,
- s => State::Down(s.to_string()),
- }
- }
- Kind::Systemd { service } => {
- let output = Command::new("systemctl")
- .arg("is-active")
- .arg(service)
- .output()?;
-
- if output.status.success() {
- State::Operational
- } else {
- State::Down(String::from_utf8_lossy(&output.stdout).to_string())
- }
- }
- };
-
- Ok(state)
+ pub async fn check_one(&self, name: &str) -> Option<Result<Check, Error>> {
+ Some(self.inner.get(name)?.check(self.client.clone()).await)
}
-}
-impl Display for Kind {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Kind::Tcp { address } => write!(f, "tcp://{address}"),
- Kind::Http { method, url, .. } => write!(f, "{method} {url}"),
- Kind::Systemd { service } => write!(f, "{service}"),
- }
+ pub async fn check_filtered<P>(&self, mut predicate: P) -> Result<HashMap<String, Check>, Error>
+ where
+ P: FnMut(&String) -> bool,
+ {
+ let checks = self
+ .inner
+ .iter()
+ .filter_map(|(s, service)| predicate(s).then_some(service))
+ .map(|service| service.check(self.client.clone()))
+ .collect::<FuturesOrdered<_>>()
+ .try_collect::<Vec<_>>()
+ .await?;
+
+ Ok(self
+ .inner
+ .keys()
+ .cloned()
+ .zip(checks)
+ .collect::<HashMap<_, _>>())
}
}
-#[derive(Debug, Clone, Default)]
-pub struct Status {
- pub info: String,
- pub state: State,
-}
-
-#[derive(Debug, Clone, Default)]
-pub enum State {
- #[default]
- Unknown,
- Operational,
- Down(String),
+#[derive(Debug, Clone, Deserialize)]
+#[serde(untagged)]
+pub enum Service {
+ Http(Http),
+ Tcp(Tcp),
+ Systemd(Systemd),
}
-impl State {
- /// Returns `true` if this is a `Unknown` variant.
- pub fn is_unknown(&self) -> bool {
- matches!(self, Self::Unknown)
- }
-
- /// Returns `true` if this is a `Operational` variant.
- pub fn is_operational(&self) -> bool {
- matches!(self, Self::Operational)
- }
-
- /// Returns `true` if this is a `Down` variant.
- pub fn is_down(&self) -> bool {
- matches!(self, Self::Down(_))
- }
-
- /// Converts the `State` into an `Option` containing `String` description if the `State` was
- /// `Down` and `None` otherwise.
- pub fn down_value(self) -> Option<String> {
- match self {
- State::Unknown => None,
- State::Operational => None,
- State::Down(s) => Some(s),
- }
- }
-
- pub fn as_level(&self) -> String {
+impl Service {
+ pub async fn check(&self, client: reqwest::Client) -> Result<Check, Error> {
match self {
- State::Unknown => "warning",
- State::Operational => "ok",
- State::Down(_) => "error",
+ Service::Http(http) => http.check(client).await,
+ Service::Tcp(tcp) => tcp.check().await,
+ Service::Systemd(systemd) => systemd.check().await,
}
- .to_string()
}
}
-impl Display for State {
+impl Display for Service {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
- State::Unknown => write!(f, "Unknown"),
- State::Operational => write!(f, "Operational"),
- State::Down(s) => write!(f, "{s}"),
+ Service::Http(http) => http.fmt(f),
+ Service::Tcp(tcp) => tcp.fmt(f),
+ Service::Systemd(systemd) => systemd.fmt(f),
}
}
}
diff --git a/src/service/http.rs b/src/service/http.rs
new file mode 100644
index 0000000..15696a1
--- /dev/null
+++ b/src/service/http.rs
@@ -0,0 +1,49 @@
+use std::fmt::Display;
+
+use serde::Deserialize;
+
+use crate::{Check, Error, Status};
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct Http {
+ pub url: String,
+ #[serde(default = "Http::default_method")]
+ pub method: String,
+ #[serde(default = "Http::default_code")]
+ pub status_code: u16,
+}
+
+impl Http {
+ fn default_method() -> String {
+ "GET".to_string()
+ }
+
+ fn default_code() -> u16 {
+ 200
+ }
+}
+
+impl Display for Http {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{} {}", self.method, self.url)
+ }
+}
+
+impl Http {
+ pub async fn check(&self, client: reqwest::Client) -> Result<Check, Error> {
+ let status_code = client
+ .request(self.method.parse().map_err(|_| Error::Method)?, &self.url)
+ .send()
+ .await?
+ .status()
+ .as_u16();
+
+ match status_code == self.status_code {
+ true => Ok(Check::default()),
+ false => Ok(Check {
+ status: Status::Fail,
+ output: Some(format!("Status code: {status_code}")),
+ }),
+ }
+ }
+}
diff --git a/src/service/systemd.rs b/src/service/systemd.rs
new file mode 100644
index 0000000..2e3b74c
--- /dev/null
+++ b/src/service/systemd.rs
@@ -0,0 +1,33 @@
+use std::{fmt::Display, process::Command};
+
+use serde::Deserialize;
+
+use crate::{Check, Error, Status};
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct Systemd {
+ pub service: String,
+}
+
+impl Display for Systemd {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}.service", self.service.trim_end_matches(".service"))
+ }
+}
+
+impl Systemd {
+ pub async fn check(&self) -> Result<Check, Error> {
+ let output = Command::new("systemctl")
+ .arg("is-active")
+ .arg(&self.service)
+ .output()?;
+
+ let stdout = String::from_utf8_lossy(&output.stdout).to_string();
+ Ok((!output.status.success())
+ .then(|| Check {
+ status: Status::Fail,
+ output: Some(format!("Service state: {}", stdout.trim())),
+ })
+ .unwrap_or_default())
+ }
+}
diff --git a/src/service/tcp.rs b/src/service/tcp.rs
new file mode 100644
index 0000000..5f55091
--- /dev/null
+++ b/src/service/tcp.rs
@@ -0,0 +1,28 @@
+use std::fmt::Display;
+
+use serde::Deserialize;
+
+use crate::{Check, Error, Status};
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct Tcp {
+ pub address: String,
+}
+
+impl Display for Tcp {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "tcp://{}", self.address)
+ }
+}
+
+impl Tcp {
+ pub async fn check(&self) -> Result<Check, Error> {
+ Ok(std::net::TcpStream::connect(&self.address)
+ .err()
+ .map(|err| Check {
+ status: Status::Fail,
+ output: Some(format!("error: {err}")),
+ })
+ .unwrap_or_default())
+ }
+}