Microservice to bring 2FA to self hosted PDSes

feat: add opt-in per-request logging

Add GATEKEEPER_REQUEST_LOGGING=true to enable per-request tracing
with method, path, client_ip, status, and latency_ms. Disabled by
default — existing behavior unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

authored by

Enrico Graziani
Claude Opus 4.6
and committed by tangled.org 70f45371 e48cb5ee

+69 -9
+14
Cargo.lock
··· 4457 4457 "tower", 4458 4458 "tower-layer", 4459 4459 "tower-service", 4460 + "tracing", 4460 4461 ] 4461 4462 4462 4463 [[package]] ··· 4533 4534 ] 4534 4535 4535 4536 [[package]] 4537 + name = "tracing-serde" 4538 + version = "0.2.0" 4539 + source = "registry+https://github.com/rust-lang/crates.io-index" 4540 + checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" 4541 + dependencies = [ 4542 + "serde", 4543 + "tracing-core", 4544 + ] 4545 + 4546 + [[package]] 4536 4547 name = "tracing-subscriber" 4537 4548 version = "0.3.22" 4538 4549 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4542 4553 "nu-ansi-term", 4543 4554 "once_cell", 4544 4555 "regex-automata", 4556 + "serde", 4557 + "serde_json", 4545 4558 "sharded-slab", 4546 4559 "smallvec", 4547 4560 "thread_local", 4548 4561 "tracing", 4549 4562 "tracing-core", 4550 4563 "tracing-log", 4564 + "tracing-serde", 4551 4565 ] 4552 4566 4553 4567 [[package]]
+2 -2
Cargo.toml
··· 12 12 serde = { version = "1.0", features = ["derive"] } 13 13 serde_json = "1.0" 14 14 tracing = "0.1" 15 - tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } 15 + tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "json"] } 16 16 hyper-util = { version = "0.1.19", features = ["client", "client-legacy"] } 17 - tower-http = { version = "0.6", features = ["cors", "compression-zstd"] } 17 + tower-http = { version = "0.6", features = ["cors", "compression-zstd", "trace"] } 18 18 tower_governor = { version = "0.8.0", features = ["axum", "tracing"] } 19 19 hex = "0.4" 20 20 jwt-compact = { version = "0.8.0", features = ["es256k"] }
+53 -7
src/main.rs
··· 33 33 }; 34 34 use tower_http::{ 35 35 compression::CompressionLayer, 36 - cors::{AllowHeaders, Any, CorsLayer}, 36 + cors::{Any, CorsLayer}, 37 + trace::TraceLayer, 37 38 }; 38 39 use tracing::log; 39 40 use tracing_subscriber::{EnvFilter, fmt, prelude::*}; ··· 385 386 ); 386 387 } 387 388 388 - let app = app 389 + let request_logging = env::var("GATEKEEPER_REQUEST_LOGGING") 390 + .map(|v| v.eq_ignore_ascii_case("true") || v == "1") 391 + .unwrap_or(false); 392 + 393 + let app = if request_logging { 394 + app.layer(TraceLayer::new_for_http() 395 + .make_span_with(|req: &axum::http::Request<Body>| { 396 + let headers: std::collections::HashMap<&str, Vec<&str>> = req.headers() 397 + .keys() 398 + .map(|k| { 399 + let vals: Vec<&str> = req.headers() 400 + .get_all(k) 401 + .iter() 402 + .filter_map(|v| v.to_str().ok()) 403 + .collect(); 404 + (k.as_str(), vals) 405 + }) 406 + .collect(); 407 + let headers_json = serde_json::to_string(&headers).unwrap_or_default(); 408 + 409 + tracing::info_span!("request", 410 + method = %req.method(), 411 + path = %req.uri().path(), 412 + headers = %headers_json, 413 + ) 414 + }) 415 + .on_response(|resp: &axum::http::Response<Body>, latency: Duration, _span: &tracing::Span| { 416 + tracing::info!(status = resp.status().as_u16(), latency_ms = latency.as_millis() as u64, "response"); 417 + }) 418 + ) 389 419 .layer(CompressionLayer::new()) 390 420 .layer(cors) 391 - .with_state(state); 421 + .with_state(state) 422 + } else { 423 + app.layer(CompressionLayer::new()) 424 + .layer(cors) 425 + .with_state(state) 426 + }; 392 427 393 428 let host = env::var("GATEKEEPER_HOST").unwrap_or_else(|_| "0.0.0.0".to_string()); 394 429 let port: u16 = env::var("GATEKEEPER_PORT") ··· 416 451 417 452 fn setup_tracing() { 418 453 let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); 419 - tracing_subscriber::registry() 420 - .with(env_filter) 421 - .with(fmt::layer()) 422 - .init(); 454 + let json = env::var("GATEKEEPER_LOG_FORMAT") 455 + .map(|v| v.eq_ignore_ascii_case("json")) 456 + .unwrap_or(false); 457 + 458 + if json { 459 + tracing_subscriber::registry() 460 + .with(env_filter) 461 + .with(fmt::layer().json()) 462 + .init(); 463 + } else { 464 + tracing_subscriber::registry() 465 + .with(env_filter) 466 + .with(fmt::layer()) 467 + .init(); 468 + } 423 469 } 424 470 425 471 async fn shutdown_signal() {