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 4202 "tower", 4203 4203 "tower-layer", 4204 4204 "tower-service", 4205 + "tracing", 4205 4206 ] 4206 4207 4207 4208 [[package]] ··· 4277 4278 "tracing-core", 4278 4279 ] 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 + 4280 4291 [[package]] 4281 4292 name = "tracing-subscriber" 4282 4293 version = "0.3.22" ··· 4287 4298 "nu-ansi-term", 4288 4299 "once_cell", 4289 4300 "regex-automata", 4301 + "serde", 4302 + "serde_json", 4290 4303 "sharded-slab", 4291 4304 "smallvec", 4292 4305 "thread_local", 4293 4306 "tracing", 4294 4307 "tracing-core", 4295 4308 "tracing-log", 4309 + "tracing-serde", 4296 4310 ] 4297 4311 4298 4312 [[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"] }
+52 -6
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 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() {

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