From 5734b3190a820e208e428bd864ce85af8b348fb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20=C3=96sterberg?= Date: Wed, 4 Feb 2026 12:45:24 +0100 Subject: [PATCH 1/4] fix: Log messages to stdout instead of stderr --- src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.rs b/src/main.rs index 84fd5cf..c375eef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,6 +19,7 @@ async fn main() { let cfg: Config = Config::parse(); #[cfg(feature = "logging")] env_logger::Builder::new() + .target(env_logger::Target::Stdout) .filter_level(cfg.log_level().to_level_filter()) .init(); From 17272bc197c19e30d34d6c36432e4739a4803e3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20=C3=96sterberg?= Date: Wed, 4 Feb 2026 13:03:35 +0100 Subject: [PATCH 2/4] feat: Add JSON log format for proper Datadog parsing Configure env_logger to output structured JSON logs with timestamp, level, target, and message fields. This ensures Datadog correctly parses and categorizes log levels instead of treating all logs as a single severity level. Co-Authored-By: Claude --- Cargo.lock | 204 ++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 3 +- src/main.rs | 11 +++ 3 files changed, 217 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 5824cc2..c713ea3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,6 +24,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.21" @@ -85,6 +94,12 @@ dependencies = [ "syn", ] +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "axum" version = "0.6.20" @@ -162,18 +177,47 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + [[package]] name = "bytes" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cc" +version = "1.2.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "clap" version = "4.5.53" @@ -234,6 +278,12 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -297,11 +347,18 @@ dependencies = [ "log", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "flagpole" version = "0.1.0" dependencies = [ "axum", + "chrono", "clap", "env_logger", "http", @@ -478,6 +535,30 @@ dependencies = [ "want", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.1.1" @@ -616,6 +697,16 @@ dependencies = [ "syn", ] +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "libc" version = "0.2.177" @@ -672,6 +763,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -965,6 +1065,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" version = "1.4.6" @@ -1211,12 +1317,110 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index 4565bff..9574b6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ tokio = { version = "1", features = ["full"] } axum = { version = "0.6.18", features = ["headers", "http1", "json", "tokio", "query"] } log = { version = "0.4", optional = true } env_logger = { version = "0.11.8", optional = true } +chrono = { version = "0.4", optional = true } clap = { version = "4.5.53", features = ["color", "derive", "help", "usage", "std", "env"] } redis = { version = "0.25.4", features = [ "ahash", "aio", "tokio-comp" ] } sha2 = "0.10.9" @@ -23,5 +24,5 @@ tower = "0.4" [features] default = ["logging"] -logging = ["dep:log", "dep:env_logger"] +logging = ["dep:log", "dep:env_logger", "dep:chrono"] redis = [] diff --git a/src/main.rs b/src/main.rs index c375eef..c2f6f3c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,6 +21,17 @@ async fn main() { env_logger::Builder::new() .target(env_logger::Target::Stdout) .filter_level(cfg.log_level().to_level_filter()) + .format(|buf, record| { + use std::io::Write; + writeln!( + buf, + "{{\"timestamp\":\"{}\",\"level\":\"{}\",\"target\":\"{}\",\"message\":\"{}\"}}", + chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true), + record.level(), + record.target(), + record.args() + ) + }) .init(); if cfg.api_key().is_none() { From 801d1c0abad83bb645e47290f9928810a384074e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20=C3=96sterberg?= Date: Wed, 4 Feb 2026 13:54:27 +0100 Subject: [PATCH 3/4] Make JSON logging an optional compile feature --- Cargo.toml | 3 ++- src/db/mod.rs | 2 ++ src/main.rs | 48 +++++++++++++++++++++++++++++++++--------------- 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9574b6b..e37badb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,5 +24,6 @@ tower = "0.4" [features] default = ["logging"] -logging = ["dep:log", "dep:env_logger", "dep:chrono"] +logging = ["dep:log", "dep:env_logger"] +json-logging = ["dep:chrono", "logging"] redis = [] diff --git a/src/db/mod.rs b/src/db/mod.rs index bba432d..a2d7978 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -10,11 +10,13 @@ pub mod redis; pub async fn create_db(cfg: &Config) -> Arc>> { #[cfg(not(feature = "redis"))] + #[cfg(feature = "logging")] log::info!("Using InMemoryDb"); #[cfg(not(feature = "redis"))] let database = mem::InMemoryDb::new(); #[cfg(feature = "redis")] + #[cfg(feature = "logging")] log::info!("Using RedisDb"); #[cfg(feature = "redis")] let database = redis::RedisDb::new(cfg.redis_uri().to_string()); diff --git a/src/main.rs b/src/main.rs index c2f6f3c..7f58996 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,23 +18,10 @@ use db::Database; async fn main() { let cfg: Config = Config::parse(); #[cfg(feature = "logging")] - env_logger::Builder::new() - .target(env_logger::Target::Stdout) - .filter_level(cfg.log_level().to_level_filter()) - .format(|buf, record| { - use std::io::Write; - writeln!( - buf, - "{{\"timestamp\":\"{}\",\"level\":\"{}\",\"target\":\"{}\",\"message\":\"{}\"}}", - chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true), - record.level(), - record.target(), - record.args() - ) - }) - .init(); + init_logs(&cfg); if cfg.api_key().is_none() { + #[cfg(feature = "logging")] log::warn!("No API key is configured, authentication is disabled"); } @@ -49,6 +36,7 @@ async fn main() { .with_state(state); let addr: SocketAddr = cfg.address().unwrap(); + #[cfg(feature = "logging")] log::info!("Running flagpole on {:?}", addr); axum::Server::bind(&addr) @@ -57,9 +45,37 @@ async fn main() { .await .unwrap(); + #[cfg(feature = "logging")] log::info!("Server shutdown complete"); } +#[cfg(feature = "logging")] +#[cfg(not(feature = "json-logging"))] +fn init_logs(cfg: &Config) { + env_logger::Builder::new() + .filter_level(cfg.log_level().to_level_filter()) + .init(); +} + +#[cfg(feature = "json-logging")] +fn init_logs(cfg: &Config) { + env_logger::Builder::new() + .target(env_logger::Target::Stdout) + .filter_level(cfg.log_level().to_level_filter()) + .format(|buf, record| { + use std::io::Write; + writeln!( + buf, + "{{\"timestamp\":\"{}\",\"level\":\"{}\",\"target\":\"{}\",\"message\":\"{}\"}}", + chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true), + record.level(), + record.target(), + record.args() + ) + }) + .init(); +} + async fn shutdown_signal() { let ctrl_c = async { tokio::signal::ctrl_c().await.expect("failed to install CTRL+C signal handler"); @@ -78,9 +94,11 @@ async fn shutdown_signal() { tokio::select! { _ = ctrl_c => { + #[cfg(feature = "logging")] log::info!("Received SIGINT (CTRL+C), initiating graceful shutdown"); }, _ = terminate => { + #[cfg(feature = "logging")] log::info!("Received SIGTERM, initiating graceful shutdown"); }, } From 6f4308313728c2fb3bc3773dbe697da5010fcd18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20=C3=96sterberg?= Date: Wed, 4 Feb 2026 14:06:44 +0100 Subject: [PATCH 4/4] build: Support setting compile time features in Dockerfile --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index fcb6d78..5c4b1f7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,8 @@ COPY --from=planner /app/recipe.json recipe.json RUN cargo chef cook --release --recipe-path recipe.json # Build application COPY . . -RUN cargo build --release --bin flagpole +ARG FEATURES=logging +RUN cargo build --release --bin flagpole --features=$FEATURES # We do not need the Rust toolchain to run the binary! FROM debian:stable-slim@sha256:4448d44b91bf4a13cb1b4e02d9d5f87ed40621d6e33f0ae7b6ddf71d57e29364 AS runtime