Microservice to bring 2FA to self hosted PDSes

feat: add opt-in per-request logging #3

closed opened by eriko.eurosky.social targeting main from feat/structured-request-logging

Summary

  • Add GATEKEEPER_REQUEST_LOGGING=true to log every request with method, path, client_ip, status, latency_ms
  • Disabled by default — existing behavior unchanged (errors only)
  • Client IP from x-forwarded-for / x-real-ip / socket — same IP traceable across LB, gatekeeper, PDS

Test plan

  • cargo test — 26 tests pass
  • Without env var: no change in log output
  • GATEKEEPER_REQUEST_LOGGING=true: request logs appear
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:autcqcg4hsvgdf3hwt4cvci3/sh.tangled.repo.pull/3mfm3psk6y422
+40 -3
Diff #0
+1
Cargo.lock
··· 4202 4202 "tower", 4203 4203 "tower-layer", 4204 4204 "tower-service", 4205 + "tracing", 4205 4206 ] 4206 4207 4207 4208 [[package]]
+1 -1
Cargo.toml
··· 14 14 tracing = "0.1" 15 15 tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } 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"] }
+38 -2
src/main.rs
··· 33 33 use tower_http::{ 34 34 compression::CompressionLayer, 35 35 cors::{Any, CorsLayer}, 36 + trace::TraceLayer, 36 37 }; 37 38 use tracing::log; 38 39 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 client_ip = req.headers() 397 + .get("x-forwarded-for") 398 + .and_then(|v| v.to_str().ok()) 399 + .and_then(|v| v.split(',').next()) 400 + .map(|s| s.trim().to_string()) 401 + .or_else(|| req.headers() 402 + .get("x-real-ip") 403 + .and_then(|v| v.to_str().ok()) 404 + .map(|s| s.trim().to_string())) 405 + .or_else(|| req.extensions() 406 + .get::<std::net::SocketAddr>() 407 + .map(|a| a.ip().to_string())) 408 + .unwrap_or_else(|| "-".to_string()); 409 + 410 + tracing::info_span!("request", 411 + method = %req.method(), 412 + path = %req.uri().path(), 413 + client_ip = %client_ip, 414 + ) 415 + }) 416 + .on_response(|resp: &axum::http::Response<Body>, latency: Duration, _span: &tracing::Span| { 417 + tracing::info!(status = resp.status().as_u16(), latency_ms = latency.as_millis() as u64, "response"); 418 + }) 419 + ) 389 420 .layer(CompressionLayer::new()) 390 421 .layer(cors) 391 - .with_state(state); 422 + .with_state(state) 423 + } else { 424 + app.layer(CompressionLayer::new()) 425 + .layer(cors) 426 + .with_state(state) 427 + }; 392 428 393 429 let host = env::var("GATEKEEPER_HOST").unwrap_or_else(|_| "0.0.0.0".to_string()); 394 430 let port: u16 = env::var("GATEKEEPER_PORT")

History

1 round 0 comments
sign up or login to add to the discussion
1 commit
expand
feat: add opt-in per-request logging
expand 0 comments
closed without merging