Microservice to bring 2FA to self hosted PDSes

feat: add opt-in per-request logging with JSON format support #10

Summary

  • Add GATEKEEPER_REQUEST_LOGGING=true to log every request with method, path, headers (as JSON), status, and latency_ms
  • Add GATEKEEPER_LOG_FORMAT=json to switch all log output (requests, errors, rate limits, everything) to JSON — consistent with PDS logs for cross-service tracing
  • Both are off by default — existing behavior unchanged

Env vars

┌────────────────────────────┬─────────┬────────────────────────────────────────────────────────┐ │ Var │ Default │ Description │ ├────────────────────────────┼─────────┼────────────────────────────────────────────────────────┤ │ GATEKEEPER_REQUEST_LOGGING │ false │ Enable per-request logging │ ├────────────────────────────┼─────────┼────────────────────────────────────────────────────────┤ │ GATEKEEPER_LOG_FORMAT │ fmt │ Set to json for structured JSON output across all logs │ └────────────────────────────┴─────────┴────────────────────────────────────────────────────────┘

Test plan

  • cargo test — 26 tests pass
  • Without env vars: no change in log output
  • GATEKEEPER_REQUEST_LOGGING=true: request logs appear in current format
  • GATEKEEPER_LOG_FORMAT=json: all logs switch to JSON, headers parseable with jq
  • Both combined: per-request JSON logs with searchable IP across LB/gatekeeper/PDS
Labels

None yet.

Participants 2
AT URI
at://did:plc:autcqcg4hsvgdf3hwt4cvci3/sh.tangled.repo.pull/3mfm56kkxs322
+68 -8
Diff #0
+14
Cargo.lock
··· 4202 "tower", 4203 "tower-layer", 4204 "tower-service", 4205 ] 4206 4207 [[package]] ··· 4277 "tracing-core", 4278 ] 4279 4280 [[package]] 4281 name = "tracing-subscriber" 4282 version = "0.3.22" ··· 4287 "nu-ansi-term", 4288 "once_cell", 4289 "regex-automata", 4290 "sharded-slab", 4291 "smallvec", 4292 "thread_local", 4293 "tracing", 4294 "tracing-core", 4295 "tracing-log", 4296 ] 4297 4298 [[package]]
··· 4202 "tower", 4203 "tower-layer", 4204 "tower-service", 4205 + "tracing", 4206 ] 4207 4208 [[package]] ··· 4278 "tracing-core", 4279 ] 4280 4281 + [[package]] 4282 + name = "tracing-serde" 4283 + version = "0.2.0" 4284 + source = "registry+https://github.com/rust-lang/crates.io-index" 4285 + checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" 4286 + dependencies = [ 4287 + "serde", 4288 + "tracing-core", 4289 + ] 4290 + 4291 [[package]] 4292 name = "tracing-subscriber" 4293 version = "0.3.22" ··· 4298 "nu-ansi-term", 4299 "once_cell", 4300 "regex-automata", 4301 + "serde", 4302 + "serde_json", 4303 "sharded-slab", 4304 "smallvec", 4305 "thread_local", 4306 "tracing", 4307 "tracing-core", 4308 "tracing-log", 4309 + "tracing-serde", 4310 ] 4311 4312 [[package]]
+2 -2
Cargo.toml
··· 12 serde = { version = "1.0", features = ["derive"] } 13 serde_json = "1.0" 14 tracing = "0.1" 15 - tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } 16 hyper-util = { version = "0.1.19", features = ["client", "client-legacy"] } 17 - tower-http = { version = "0.6", features = ["cors", "compression-zstd"] } 18 tower_governor = { version = "0.8.0", features = ["axum", "tracing"] } 19 hex = "0.4" 20 jwt-compact = { version = "0.8.0", features = ["es256k"] }
··· 12 serde = { version = "1.0", features = ["derive"] } 13 serde_json = "1.0" 14 tracing = "0.1" 15 + tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "json"] } 16 hyper-util = { version = "0.1.19", features = ["client", "client-legacy"] } 17 + tower-http = { version = "0.6", features = ["cors", "compression-zstd", "trace"] } 18 tower_governor = { version = "0.8.0", features = ["axum", "tracing"] } 19 hex = "0.4" 20 jwt-compact = { version = "0.8.0", features = ["es256k"] }
+52 -6
src/main.rs
··· 33 use tower_http::{ 34 compression::CompressionLayer, 35 cors::{Any, CorsLayer}, 36 }; 37 use tracing::log; 38 use tracing_subscriber::{EnvFilter, fmt, prelude::*}; ··· 385 ); 386 } 387 388 - let app = app 389 .layer(CompressionLayer::new()) 390 .layer(cors) 391 - .with_state(state); 392 393 let host = env::var("GATEKEEPER_HOST").unwrap_or_else(|_| "0.0.0.0".to_string()); 394 let port: u16 = env::var("GATEKEEPER_PORT") ··· 416 417 fn setup_tracing() { 418 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(); 423 } 424 425 async fn shutdown_signal() {
··· 33 use tower_http::{ 34 compression::CompressionLayer, 35 cors::{Any, CorsLayer}, 36 + trace::TraceLayer, 37 }; 38 use tracing::log; 39 use tracing_subscriber::{EnvFilter, fmt, prelude::*}; ··· 386 ); 387 } 388 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 + ) 419 .layer(CompressionLayer::new()) 420 .layer(cors) 421 + .with_state(state) 422 + } else { 423 + app.layer(CompressionLayer::new()) 424 + .layer(cors) 425 + .with_state(state) 426 + }; 427 428 let host = env::var("GATEKEEPER_HOST").unwrap_or_else(|_| "0.0.0.0".to_string()); 429 let port: u16 = env::var("GATEKEEPER_PORT") ··· 451 452 fn setup_tracing() { 453 let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); 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 + } 469 } 470 471 async fn shutdown_signal() {

History

1 round 1 comment
sign up or login to add to the discussion
1 commit
expand
feat: add opt-in per-request logging
expand 1 comment

Had some changes but closed here https://tangled.org/baileytownsend.dev/pds-gatekeeper/pulls/15/round/1

Thank you for the PR! The headers are a JSON string, could not work around that but should be able to parse with JQ and should hopefully be better than nothing

closed without merging