aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock820
-rw-r--r--Cargo.toml4
-rw-r--r--crates/api-base/Cargo.toml17
-rw-r--r--crates/api-base/src/health/apidoc.rs12
-rw-r--r--crates/api-base/src/health/mod.rs27
-rw-r--r--crates/api-base/src/lib.rs3
-rw-r--r--crates/api-base/src/version.rs45
-rw-r--r--crates/sellershut/Cargo.toml16
-rw-r--r--crates/sellershut/src/config/mod.rs2
-rw-r--r--crates/sellershut/src/config/server.rs10
-rw-r--r--crates/sellershut/src/main.rs48
-rw-r--r--crates/sellershut/src/server/api/mod.rs55
-rw-r--r--crates/sellershut/src/server/api/routes/logs/mod.rs54
-rw-r--r--crates/sellershut/src/server/api/routes/mod.rs38
-rw-r--r--crates/sellershut/src/server/logs.rs36
-rw-r--r--crates/sellershut/src/server/mod.rs2
-rw-r--r--crates/sellershut/src/state/mod.rs12
17 files changed, 1184 insertions, 17 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 979ef14..6022e70 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3,6 +3,12 @@
version = 4
[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -68,6 +74,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
+name = "api-base"
+version = "0.0.0"
+dependencies = [
+ "axum",
+ "serde",
+ "utoipa",
+]
+
+[[package]]
+name = "arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
+dependencies = [
+ "derive_arbitrary",
+]
+
+[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -138,6 +162,52 @@ dependencies = [
]
[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bon"
+version = "3.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f47dbe92550676ee653353c310dfb9cf6ba17ee70396e1f7cf0a2020ad49b2fe"
+dependencies = [
+ "bon-macros",
+ "rustversion",
+]
+
+[[package]]
+name = "bon-macros"
+version = "3.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "519bd3116aeeb42d5372c29d982d16d0170d3d4a5ed85fc7dd91642ffff3c67c"
+dependencies = [
+ "darling",
+ "ident_case",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "rustversion",
+ "syn",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
+
+[[package]]
name = "bytes"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -196,12 +266,140 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "darling"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
+dependencies = [
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+dependencies = [
+ "powerfmt",
+]
+
+[[package]]
+name = "derive_arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
+name = "flate2"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+dependencies = [
+ "miniz_oxide",
+ "zlib-rs",
+]
+
+[[package]]
name = "form_urlencoded"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -244,6 +442,16 @@ dependencies = [
]
[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -336,6 +544,115 @@ dependencies = [
]
[[package]]
+name = "icu_collections"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
+
+[[package]]
+name = "icu_properties"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
+
+[[package]]
+name = "icu_provider"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
name = "indexmap"
version = "2.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -343,6 +660,8 @@ checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff"
dependencies = [
"equivalent",
"hashbrown",
+ "serde",
+ "serde_core",
]
[[package]]
@@ -370,6 +689,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
[[package]]
+name = "litemap"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
+
+[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -403,6 +728,26 @@ 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.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
name = "mio"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -423,6 +768,12 @@ dependencies = [
]
[[package]]
+name = "num-conv"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
+
+[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -435,6 +786,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
name = "percent-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -447,6 +804,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
+name = "potential_utf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -465,6 +847,18 @@ dependencies = [
]
[[package]]
+name = "regex"
+version = "1.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -482,23 +876,81 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
+name = "rust-embed"
+version = "8.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27"
+dependencies = [
+ "rust-embed-impl",
+ "rust-embed-utils",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-impl"
+version = "8.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rust-embed-utils",
+ "syn",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-utils"
+version = "8.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1"
+dependencies = [
+ "sha2",
+ "walkdir",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
name = "sellershut"
version = "0.1.0"
dependencies = [
"anyhow",
+ "api-base",
"axum",
+ "bon",
"clap",
"serde",
"tokio",
"toml",
"tracing",
+ "tracing-appender",
"tracing-subscriber",
+ "utoipa",
+ "utoipa-axum",
+ "utoipa-rapidoc",
+ "utoipa-redoc",
+ "utoipa-scalar",
+ "utoipa-swagger-ui",
]
[[package]]
@@ -577,6 +1029,17 @@ dependencies = [
]
[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -586,6 +1049,12 @@ dependencies = [
]
[[package]]
+name = "simd-adler32"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
+
+[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -608,6 +1077,12 @@ dependencies = [
]
[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -631,6 +1106,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
name = "thread_local"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -640,6 +1146,47 @@ dependencies = [
]
[[package]]
+name = "time"
+version = "0.3.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
+dependencies = [
+ "deranged",
+ "itoa",
+ "num-conv",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
+
+[[package]]
+name = "time-macros"
+version = "0.2.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
name = "tokio"
version = "1.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -744,6 +1291,18 @@ dependencies = [
]
[[package]]
+name = "tracing-appender"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf"
+dependencies = [
+ "crossbeam-channel",
+ "thiserror",
+ "time",
+ "tracing-subscriber",
+]
+
+[[package]]
name = "tracing-attributes"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -794,30 +1353,176 @@ dependencies = [
]
[[package]]
+name = "typenum"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
+
+[[package]]
+name = "unicase"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
+
+[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
+name = "utoipa"
+version = "5.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_json",
+ "utoipa-gen",
+]
+
+[[package]]
+name = "utoipa-axum"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c25bae5bccc842449ec0c5ddc5cbb6a3a1eaeac4503895dc105a1138f8234a0"
+dependencies = [
+ "axum",
+ "paste",
+ "tower-layer",
+ "tower-service",
+ "utoipa",
+]
+
+[[package]]
+name = "utoipa-gen"
+version = "5.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "regex",
+ "syn",
+]
+
+[[package]]
+name = "utoipa-rapidoc"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5f8f5abd341cce16bb4f09a8bafc087d4884a004f25fb980e538d51d6501dab"
+dependencies = [
+ "axum",
+ "serde",
+ "serde_json",
+ "utoipa",
+]
+
+[[package]]
+name = "utoipa-redoc"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6427547f6db7ec006cbbef95f7565952a16f362e298b416d2d497d9706fef72d"
+dependencies = [
+ "axum",
+ "serde",
+ "serde_json",
+ "utoipa",
+]
+
+[[package]]
+name = "utoipa-scalar"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59559e1509172f6b26c1cdbc7247c4ddd1ac6560fe94b584f81ee489b141f719"
+dependencies = [
+ "axum",
+ "serde",
+ "serde_json",
+ "utoipa",
+]
+
+[[package]]
+name = "utoipa-swagger-ui"
+version = "9.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d047458f1b5b65237c2f6dc6db136945667f40a7668627b3490b9513a3d43a55"
+dependencies = [
+ "axum",
+ "base64",
+ "mime_guess",
+ "regex",
+ "rust-embed",
+ "serde",
+ "serde_json",
+ "url",
+ "utoipa",
+ "zip",
+]
+
+[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -839,7 +1544,122 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5"
[[package]]
+name = "writeable"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
+
+[[package]]
+name = "yoke"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zerotrie"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zip"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12598812502ed0105f607f941c386f43d441e00148fce9dec3ca5ffb0bde9308"
+dependencies = [
+ "arbitrary",
+ "crc32fast",
+ "flate2",
+ "indexmap",
+ "memchr",
+ "zopfli",
+]
+
+[[package]]
+name = "zlib-rs"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513"
+
+[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+
+[[package]]
+name = "zopfli"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
+dependencies = [
+ "bumpalo",
+ "crc32fast",
+ "log",
+ "simd-adler32",
+]
diff --git a/Cargo.toml b/Cargo.toml
index 6a6de8a..0326c37 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,6 +9,10 @@ documentation = "https://books.kanjala.com/sellershut"
homepage = "https://git.kanjala.com/sellershut"
[workspace.dependencies]
+api-base = { path = "./crates/api-base", version = "0.0.0" }
+async-trait = "0.1.89"
+axum = "0.8.8"
serde = "1.0.228"
thiserror = "2.0.18"
tracing = "0.1.44"
+utoipa = "5.4.0"
diff --git a/crates/api-base/Cargo.toml b/crates/api-base/Cargo.toml
new file mode 100644
index 0000000..e15c19b
--- /dev/null
+++ b/crates/api-base/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "api-base"
+version = "0.0.0"
+edition = "2024"
+license.workspace = true
+readme.workspace = true
+documentation.workspace = true
+homepage.workspace = true
+
+[dependencies]
+axum = { workspace = true, optional = true }
+serde.workspace = true
+utoipa = { workspace = true, optional = true }
+
+[features]
+axum = ["dep:axum"]
+utoipa = ["dep:utoipa", "serde/derive", "axum"]
diff --git a/crates/api-base/src/health/apidoc.rs b/crates/api-base/src/health/apidoc.rs
new file mode 100644
index 0000000..45b8754
--- /dev/null
+++ b/crates/api-base/src/health/apidoc.rs
@@ -0,0 +1,12 @@
+use utoipa::OpenApi;
+
+use crate::Version;
+
+#[derive(OpenApi)]
+#[openapi(
+ tags(
+ (name = "sellershut", description = "API health check"),
+ ),
+ components(schemas(Version))
+)]
+pub struct ApiDocBase;
diff --git a/crates/api-base/src/health/mod.rs b/crates/api-base/src/health/mod.rs
new file mode 100644
index 0000000..a84dc85
--- /dev/null
+++ b/crates/api-base/src/health/mod.rs
@@ -0,0 +1,27 @@
+#[cfg(feature = "utoipa")]
+mod apidoc;
+
+#[cfg(feature = "utoipa")]
+pub use apidoc::*;
+
+#[derive(Default)]
+pub struct BaseService;
+
+impl HealthDriver for BaseService {}
+
+pub trait HealthDriver: Send + Sync {
+ fn health(&self, app: &str, version: &str) -> String {
+ format!("{app} v{version} is live")
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::health::{BaseService, HealthDriver};
+
+ #[test]
+ fn health() {
+ let app = BaseService.health(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
+ assert!(app.contains("is live"));
+ }
+}
diff --git a/crates/api-base/src/lib.rs b/crates/api-base/src/lib.rs
new file mode 100644
index 0000000..9c632e0
--- /dev/null
+++ b/crates/api-base/src/lib.rs
@@ -0,0 +1,3 @@
+pub mod health;
+mod version;
+pub use version::*;
diff --git a/crates/api-base/src/version.rs b/crates/api-base/src/version.rs
new file mode 100644
index 0000000..0652c6e
--- /dev/null
+++ b/crates/api-base/src/version.rs
@@ -0,0 +1,45 @@
+#[derive(Debug)]
+#[cfg_attr(
+ feature = "utoipa",
+ derive(utoipa::ToSchema, serde::Deserialize, serde::Serialize),
+ schema(example = "v0"),
+ serde(rename_all = "lowercase")
+)]
+pub enum Version {
+ V0,
+}
+
+#[cfg(feature = "axum")]
+mod request {
+ use super::*;
+ use axum::RequestPartsExt;
+ use axum::extract::{FromRequestParts, Path};
+ use axum::http::StatusCode;
+ use axum::http::request::Parts;
+ use axum::response::{IntoResponse, Response};
+ use std::collections::HashMap;
+
+ impl<S> FromRequestParts<S> for Version
+ where
+ S: Send + Sync,
+ {
+ type Rejection = Response;
+
+ async fn from_request_parts(
+ parts: &mut Parts,
+ _state: &S,
+ ) -> Result<Self, Self::Rejection> {
+ let params: Path<HashMap<String, String>> =
+ parts.extract().await.map_err(IntoResponse::into_response)?;
+
+ let version = params
+ .get("apiVersion")
+ .ok_or_else(|| (StatusCode::NOT_FOUND, "version param missing").into_response())?;
+
+ match version.as_str() {
+ "v0" => Ok(Version::V0),
+ _ => Err((StatusCode::NOT_FOUND, "unknown version").into_response()),
+ }
+ }
+ }
+}
diff --git a/crates/sellershut/Cargo.toml b/crates/sellershut/Cargo.toml
index 9ded17b..f7cd15a 100644
--- a/crates/sellershut/Cargo.toml
+++ b/crates/sellershut/Cargo.toml
@@ -6,13 +6,29 @@ license.workspace = true
readme.workspace = true
documentation.workspace = true
homepage.workspace = true
+description = "A federated marketplace platform"
[dependencies]
anyhow = "1.0.102"
+api-base = { workspace = true, features = ["utoipa"] }
axum = { version = "0.8.8", features = ["macros"] }
+bon = "3.9.1"
clap = { version = "4.6.0", features = ["derive", "env"] }
serde = { workspace = true, features = ["derive"] }
tokio = { version = "1.51.0", features = ["macros", "rt", "rt-multi-thread"] }
toml = "1.1.2"
tracing.workspace = true
+tracing-appender = "0.2.4"
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
+utoipa = { workspace = true, features = ["axum_extras"] }
+utoipa-axum = "0.2.0"
+utoipa-rapidoc = { version = "6.0.0", features = ["axum"], optional = true }
+utoipa-redoc = { version = "6.0.0", features = ["axum"], optional = true }
+utoipa-scalar = { version = "0.3.0", features = ["axum"], optional = true }
+utoipa-swagger-ui = { version = "9.0.2", features = ["axum"], optional = true }
+
+[features]
+swagger = ["dep:utoipa-swagger-ui"]
+redoc = ["dep:utoipa-redoc"]
+rapidoc = ["dep:utoipa-rapidoc"]
+scalar = ["dep:utoipa-scalar"]
diff --git a/crates/sellershut/src/config/mod.rs b/crates/sellershut/src/config/mod.rs
index b7a6ba3..d35ba1e 100644
--- a/crates/sellershut/src/config/mod.rs
+++ b/crates/sellershut/src/config/mod.rs
@@ -16,7 +16,7 @@ pub struct Config {
config: Option<PathBuf>,
/// Server configuration.
#[command(flatten)]
- server: server::ServerConfig,
+ pub server: server::ServerConfig,
}
impl Config {
pub fn load(cli: Self) -> Result<Self> {
diff --git a/crates/sellershut/src/config/server.rs b/crates/sellershut/src/config/server.rs
index 08b7828..3680c16 100644
--- a/crates/sellershut/src/config/server.rs
+++ b/crates/sellershut/src/config/server.rs
@@ -8,18 +8,18 @@ use serde::{Deserialize, Serialize};
pub struct ServerConfig {
/// Port the application server listens on.
#[arg(short, long, env = "HUT_SERVER_PORT")]
- port: Option<u16>,
+ pub port: Option<u16>,
/// Request timeout duration
#[arg(long, env = "HUT_SERVER_TIMEOUT_SECS")]
- timeout_duration: Option<u64>,
+ pub timeout_duration: Option<u64>,
/// Log level for the application server.
- #[arg(long, env = "HUT_SERVER_LOG_LEVEL")]
- log_level: Option<String>,
+ #[arg(long, env = "HUT_LOG")]
+ pub log_level: Option<String>,
/// Directory where log files should be written.
#[arg(long, env = "HUT_SERVER_LOG_FILE_DIRECTORY")]
- log_directory: Option<PathBuf>,
+ pub log_directory: Option<PathBuf>,
}
impl ServerConfig {
diff --git a/crates/sellershut/src/main.rs b/crates/sellershut/src/main.rs
index 900d554..cb7be07 100644
--- a/crates/sellershut/src/main.rs
+++ b/crates/sellershut/src/main.rs
@@ -1,24 +1,50 @@
mod config;
+mod server;
+mod state;
-use anyhow::Result;
+use std::{
+ net::{Ipv6Addr, SocketAddr},
+ sync::Arc,
+};
+
+use anyhow::{Context, Result};
+use api_base::health::BaseService;
use clap::Parser;
+use tokio::net::TcpListener;
+use tracing::info;
-use crate::config::cli;
+use crate::{config::cli, state::AppState};
#[tokio::main]
async fn main() -> Result<()> {
let cli = cli::Cli::parse();
- match cli.command {
- Some(cli::Commands::GenerateConfig { dir, file_name }) => {
- let path = config::generate_config_file(&dir, &file_name)?;
- println!("Wrote {}", path.display());
- }
- None => {
- let cfg = config::Config::load(cli.config)?;
- println!("{cfg:#?}");
- }
+ if let Some(cli::Commands::GenerateConfig { dir, file_name }) = cli.command {
+ let path = config::generate_config_file(&dir, &file_name)?;
+ println!("Wrote {}", path.display());
+ return Ok(());
}
+ let cfg = config::Config::load(cli.config)?;
+ let (log_handle, _log_guard) = server::logs::initialise_logging(
+ cfg.server.log_level.as_deref(),
+ cfg.server.log_directory.as_ref(),
+ )?;
+
+ let state = AppState::builder()
+ .log_handle(log_handle)
+ .base_service(Arc::new(BaseService))
+ .build();
+ let addr = SocketAddr::from((
+ Ipv6Addr::UNSPECIFIED,
+ cfg.server.port.context("missing port")?,
+ ));
+
+ let app = server::api::router(state, cfg).await;
+
+ let listener = TcpListener::bind(addr).await?;
+ info!(addr = ?listener.local_addr().unwrap(), "starting server");
+
+ axum::serve(listener, app).await?;
Ok(())
}
diff --git a/crates/sellershut/src/server/api/mod.rs b/crates/sellershut/src/server/api/mod.rs
new file mode 100644
index 0000000..0fd48c6
--- /dev/null
+++ b/crates/sellershut/src/server/api/mod.rs
@@ -0,0 +1,55 @@
+use api_base::health::ApiDocBase;
+use axum::Router;
+use utoipa::OpenApi;
+use utoipa_axum::router::OpenApiRouter;
+
+use crate::{config::Config, server::api::routes::ServerConfigDoc, state::AppState};
+
+pub mod routes;
+
+#[derive(OpenApi)]
+#[openapi(
+ tags(
+ (name = "sellershut", description = env!("CARGO_PKG_DESCRIPTION")),
+ ),
+)]
+pub struct ApiDoc;
+
+pub async fn router(state: AppState, config: Config) -> Router<()> {
+ let mut doc = ApiDoc::openapi();
+
+ doc.merge(ApiDocBase::openapi());
+ doc.merge(ServerConfigDoc::openapi());
+
+ let stubs = OpenApiRouter::with_openapi(doc)
+ .routes(utoipa_axum::routes!(routes::health))
+ .nest("/api", routes::router(state.clone()))
+ .with_state(state);
+
+ let (router, _api) = stubs.split_for_parts();
+
+ #[cfg(feature = "swagger")]
+ let router = router.merge(
+ utoipa_swagger_ui::SwaggerUi::new("/swagger-ui")
+ .url("/api-docs/swaggerdoc.json", _api.clone()),
+ );
+
+ #[cfg(feature = "redoc")]
+ let router = {
+ use utoipa_redoc::Servable as _;
+ router.merge(utoipa_redoc::Redoc::with_url("/redoc", _api.clone()))
+ };
+
+ #[cfg(feature = "scalar")]
+ let router = {
+ use utoipa_scalar::Servable as _;
+ router.merge(utoipa_scalar::Scalar::with_url("/scalar", _api.clone()))
+ };
+
+ #[cfg(feature = "rapidoc")]
+ let router = router.merge(
+ utoipa_rapidoc::RapiDoc::with_openapi("/api-docs/rapidoc.json", _api).path("/rapidoc"),
+ );
+
+ router
+}
diff --git a/crates/sellershut/src/server/api/routes/logs/mod.rs b/crates/sellershut/src/server/api/routes/logs/mod.rs
new file mode 100644
index 0000000..8718d86
--- /dev/null
+++ b/crates/sellershut/src/server/api/routes/logs/mod.rs
@@ -0,0 +1,54 @@
+use axum::{Json, extract::State, http::StatusCode};
+use serde::Deserialize;
+use tracing::warn;
+use utoipa::ToSchema;
+
+use crate::state::AppState;
+
+#[derive(Deserialize, Debug, Clone, ToSchema)]
+/// Log level
+#[serde(rename_all = "camelCase")]
+pub struct LogLevel {
+ #[schema(examples("info", "trace", "warden=debug,tower_http=debug,axum::rejection=trace"))]
+ log_level: String,
+}
+
+/// Update log level
+#[utoipa::path(
+ patch,
+ responses(
+ (
+ status = 200,
+ description = "Server's log level has been updated",
+ headers(
+ ("x-request-id", description = "Request identifier")
+ )
+ ),
+ (
+ status = 400,
+ description = "Invalid log level",
+ headers(
+ ("x-request-id", description = "Request identifier")
+ )
+ ),
+ ),
+ operation_id = "log_update", // https://github.com/juhaku/utoipa/issues/1170
+ path = "/logging",
+ tag = super::CONFIG,
+ request_body(
+ content = LogLevel
+ )
+)]
+pub async fn reload(State(state): State<AppState>, Json(body): Json<LogLevel>) -> StatusCode {
+ if let Ok(value) = body.log_level.parse::<tracing_subscriber::EnvFilter>() {
+ match state.log_handle.reload(value) {
+ Ok(_) => StatusCode::OK,
+ Err(e) => {
+ warn!("{e:?}");
+ StatusCode::INTERNAL_SERVER_ERROR
+ }
+ }
+ } else {
+ StatusCode::BAD_REQUEST
+ }
+}
diff --git a/crates/sellershut/src/server/api/routes/mod.rs b/crates/sellershut/src/server/api/routes/mod.rs
new file mode 100644
index 0000000..f343742
--- /dev/null
+++ b/crates/sellershut/src/server/api/routes/mod.rs
@@ -0,0 +1,38 @@
+mod logs;
+
+use axum::{extract::State, response::IntoResponse};
+
+use crate::state::AppState;
+
+use utoipa::OpenApi;
+use utoipa_axum::router::OpenApiRouter;
+
+const CONFIG: &str = "Server configuration";
+
+#[derive(OpenApi)]
+#[openapi(tags((name = CONFIG, description = "Configuration endpoints")))]
+pub struct ServerConfigDoc;
+
+pub fn router(state: AppState) -> OpenApiRouter<AppState> {
+ OpenApiRouter::new()
+ .routes(utoipa_axum::routes!(logs::reload))
+ .with_state(state)
+}
+
+/// Health
+#[utoipa::path(
+ method(get, head),
+ path = "/api/health",
+ responses(
+ (
+ status = OK, description = "API is live",
+ body = Option<str>, content_type = "text/plain",
+ )
+ ),
+ tag = "sellershut"
+)]
+pub async fn health(State(state): State<AppState>) -> impl IntoResponse {
+ state
+ .base_service
+ .health(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
+}
diff --git a/crates/sellershut/src/server/logs.rs b/crates/sellershut/src/server/logs.rs
new file mode 100644
index 0000000..edb698b
--- /dev/null
+++ b/crates/sellershut/src/server/logs.rs
@@ -0,0 +1,36 @@
+use anyhow::{Context, Result};
+use std::{env, path::PathBuf};
+use tracing_appender::rolling::{RollingFileAppender, Rotation};
+use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
+
+pub type LogHandle = tracing_subscriber::reload::Handle<EnvFilter, tracing_subscriber::Registry>;
+
+pub fn initialise_logging(
+ level: Option<&str>,
+ log_dir: Option<&PathBuf>,
+) -> Result<(LogHandle, tracing_appender::non_blocking::WorkerGuard)> {
+ let level = level.context("missing log level")?;
+ let log_dir = log_dir.context("missing log dir")?;
+
+ let file_appender = RollingFileAppender::new(Rotation::DAILY, log_dir, env!("CARGO_PKG_NAME"));
+
+ let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
+
+ let env_filter =
+ tracing_subscriber::EnvFilter::try_from_env("HUT_LOG").unwrap_or_else(|_| level.into());
+
+ let (filter_layer, reload_handle) = tracing_subscriber::reload::Layer::new(env_filter);
+
+ let file_layer = tracing_subscriber::fmt::layer()
+ .with_writer(non_blocking)
+ .with_ansi(false)
+ .with_target(true);
+
+ tracing_subscriber::registry()
+ .with(filter_layer)
+ .with(tracing_subscriber::fmt::layer())
+ .with(file_layer)
+ .init();
+
+ Ok((reload_handle, guard))
+}
diff --git a/crates/sellershut/src/server/mod.rs b/crates/sellershut/src/server/mod.rs
new file mode 100644
index 0000000..f669af9
--- /dev/null
+++ b/crates/sellershut/src/server/mod.rs
@@ -0,0 +1,2 @@
+pub mod api;
+pub mod logs;
diff --git a/crates/sellershut/src/state/mod.rs b/crates/sellershut/src/state/mod.rs
new file mode 100644
index 0000000..067cc62
--- /dev/null
+++ b/crates/sellershut/src/state/mod.rs
@@ -0,0 +1,12 @@
+use std::sync::Arc;
+
+use api_base::health::HealthDriver;
+use bon::Builder;
+
+use crate::server::logs::LogHandle;
+
+#[derive(Clone, Builder)]
+pub struct AppState {
+ pub base_service: Arc<dyn HealthDriver>,
+ pub log_handle: LogHandle,
+}