chore(logs): add explicit http logs
Default is now RUST_LOG=info,http=warn. Effect of each level on the access log: ┌────────────────────┬────────────────────────┐ │ Level on http │ Status classes emitted │ ├────────────────────┼────────────────────────┤ │ info │ 2xx/3xx + 4xx + 5xx │ ├────────────────────┼────────────────────────┤ │ warn (default) │ 4xx + 5xx │ ├────────────────────┼────────────────────────┤ │ error │ 5xx only │ ├────────────────────┼────────────────────────┤ │ off │ nothing │ └────────────────────┴────────────────────────┘ Target mapping: ┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬─────────────────┐ │ Routes │ Target │ ├────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────────────┤ │ health_routes │ http::probe │ ├────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────────────┤ │ magic_link_router │ http::web │ ├────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────────────┤ │ All /api/auth/* sub-routers (login, register, refresh, public, protected, app_pw, device_public, device_protected) │ http::api::auth │ ├────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────────────┤ │ setup_router, public_api_routes, protected_api, wopi_api_protected │ http::api │ ├────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────────────┤ │ well_known_router, caldav_protected, carddav_protected, webdav_protected │ http::dav │ ├────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────────────┤ │ nc_router │ http::nextcloud │ ├────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────────────┤ │ wopi_protocol │ http::wopi │ ├────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────────────┤ │ web_routes (+ ServeDir fallback) │ http::web │ └────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┴─────────────────┘ # Default value: - **http=warn** if target http not specified - **http::web=error** if target http::web not specified Common operator overrides: # Server-error-only access logs (the new default) unset RUST_LOG # See login failures and other client errors on auth RUST_LOG=info,http=warn,http::api::auth=info # which is similar to RUST_LOG=info,http::api::auth=info # Full access log everywhere (heavy) RUST_LOG=info,http=info # Silence everything except errors RUST_LOG=warn
This commit is contained in:
+115
-48
@@ -12,9 +12,8 @@ use socket2::{Domain, Protocol, Socket, TcpKeepalive, Type};
|
||||
|
||||
use axum::Router;
|
||||
use axum::extract::DefaultBodyLimit;
|
||||
use oxicloud::interfaces::middleware::trace_span::{
|
||||
ClientIpMakeSpan, LogBadRequest, UuidRequestId,
|
||||
};
|
||||
use oxicloud::access_log;
|
||||
use oxicloud::interfaces::middleware::trace_span::{ClientIpMakeSpan, UuidRequestId};
|
||||
use tower_http::limit::RequestBodyLimitLayer;
|
||||
use tower_http::request_id::{PropagateRequestIdLayer, SetRequestIdLayer};
|
||||
use tower_http::set_header::SetResponseHeaderLayer;
|
||||
@@ -178,11 +177,44 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize tracing
|
||||
// Initialize tracing.
|
||||
//
|
||||
// Default access-log policy — two independent directives are
|
||||
// injected unless the operator has already named them:
|
||||
//
|
||||
// `http=warn` (4xx + 5xx for every access-log target)
|
||||
// `http::web=error` (5xx only for static / ServeDir / catch-all)
|
||||
//
|
||||
// `http::web` is pulled down to ERROR because it's the noisiest
|
||||
// surface (every CSS/JS/img/favicon request hits it) and its
|
||||
// 4xx are almost always "browser asked for a file we don't ship",
|
||||
// not a real signal. Operators investigating a 404 storm can
|
||||
// promote it back: `RUST_LOG=info,http::web=warn`.
|
||||
//
|
||||
// The detection is substring-based:
|
||||
// - `http=` in RUST_LOG → operator owns the http baseline.
|
||||
// - `http::web=` in RUST_LOG → operator owns the web subtarget.
|
||||
// The two are independent — supplying `http=info` still gets a
|
||||
// free `http::web=error` unless the operator named that too.
|
||||
//
|
||||
// Empty / unset / no http directives → both defaults applied.
|
||||
// Note that `http::web=…` does NOT contain `http=` as a substring
|
||||
// (different characters around the `:`), so the two checks don't
|
||||
// alias each other.
|
||||
let rust_log = match std::env::var("RUST_LOG").ok().filter(|s| !s.is_empty()) {
|
||||
None => "info,http=warn,http::web=error".to_string(),
|
||||
Some(mut s) => {
|
||||
if !s.contains("http=") {
|
||||
s.push_str(",http=warn");
|
||||
}
|
||||
if !s.contains("http::web=") {
|
||||
s.push_str(",http::web=error");
|
||||
}
|
||||
s
|
||||
}
|
||||
};
|
||||
tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::EnvFilter::new(
|
||||
std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into()),
|
||||
))
|
||||
.with(tracing_subscriber::EnvFilter::new(rust_log))
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
|
||||
@@ -487,38 +519,68 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let magic_link_router = interfaces::api::handlers::magic_link_handler::magic_link_routes()
|
||||
.with_state(app_state.clone());
|
||||
|
||||
// Access-log targets are declared per-mount via `access_log!(…)`
|
||||
// — see `interfaces/middleware/trace_span.rs` for the catalogue.
|
||||
app = Router::new()
|
||||
// Health / readiness probes — no auth, mounted at root
|
||||
.merge(health_routes)
|
||||
.merge(health_routes.layer(access_log!("http::probe")))
|
||||
// Magic-link redemption — top-level, no `/api/` prefix
|
||||
.merge(magic_link_router)
|
||||
.merge(magic_link_router.layer(access_log!("http::web")))
|
||||
// Rate-limited auth endpoints (login, register, refresh)
|
||||
.nest("/api/auth", auth_login)
|
||||
.nest("/api/auth", auth_register)
|
||||
.nest("/api/auth", auth_refresh)
|
||||
.nest(
|
||||
"/api/auth",
|
||||
auth_login.layer(access_log!("http::api::auth")),
|
||||
)
|
||||
.nest(
|
||||
"/api/auth",
|
||||
auth_register.layer(access_log!("http::api::auth")),
|
||||
)
|
||||
.nest(
|
||||
"/api/auth",
|
||||
auth_refresh.layer(access_log!("http::api::auth")),
|
||||
)
|
||||
// Public auth endpoints (status, OIDC)
|
||||
.nest("/api/auth", auth_public)
|
||||
.nest(
|
||||
"/api/auth",
|
||||
auth_public.layer(access_log!("http::api::auth")),
|
||||
)
|
||||
// Protected auth endpoints (/me, /change-password, /logout)
|
||||
.nest("/api/auth", auth_protected)
|
||||
.nest(
|
||||
"/api/auth",
|
||||
auth_protected.layer(access_log!("http::api::auth")),
|
||||
)
|
||||
// App password management (create, list, revoke)
|
||||
.nest("/api/auth", app_pw_protected)
|
||||
.nest(
|
||||
"/api/auth",
|
||||
app_pw_protected.layer(access_log!("http::api::auth")),
|
||||
)
|
||||
// One-time setup endpoint — public, rate-limited
|
||||
.nest("/api", setup_router)
|
||||
.nest("/api", setup_router.layer(access_log!("http::api")))
|
||||
// Device Auth Grant public endpoints (authorize + token polling)
|
||||
.nest("/api/auth/device", device_public)
|
||||
.nest(
|
||||
"/api/auth/device",
|
||||
device_public.layer(access_log!("http::api::auth")),
|
||||
)
|
||||
// Device Auth Grant protected endpoints (verify + device management)
|
||||
.nest("/api/auth/device", device_protected)
|
||||
.nest(
|
||||
"/api/auth/device",
|
||||
device_protected.layer(access_log!("http::api::auth")),
|
||||
)
|
||||
// Public API routes (share access, i18n) — no auth required
|
||||
.nest("/api", public_api_routes)
|
||||
.nest("/api", public_api_routes.layer(access_log!("http::api")))
|
||||
// All other API routes are protected by auth middleware
|
||||
.nest("/api", protected_api)
|
||||
.nest("/api", protected_api.layer(access_log!("http::api")))
|
||||
// RFC 6764 well-known discovery (public, no auth — just redirects)
|
||||
.merge(well_known_router.clone())
|
||||
.merge(well_known_router.clone().layer(access_log!("http::dav")))
|
||||
// CalDAV/CardDAV/WebDAV protocols merged at top-level for client compatibility
|
||||
.merge(caldav_protected)
|
||||
.merge(carddav_protected)
|
||||
.merge(webdav_protected)
|
||||
.merge(web_routes);
|
||||
.merge(caldav_protected.layer(access_log!("http::dav")))
|
||||
.merge(carddav_protected.layer(access_log!("http::dav")))
|
||||
.merge(webdav_protected.layer(access_log!("http::dav")))
|
||||
// Web (HTML pages) — also the ServeDir fallback root, so
|
||||
// static asset hits land here. We keep them on the `web`
|
||||
// target for simplicity; switch to `http::static` when
|
||||
// the static surface is split into its own router.
|
||||
.merge(web_routes.layer(access_log!("http::web")));
|
||||
|
||||
// Mount Nextcloud routes (uses its own Basic Auth middleware).
|
||||
// **Merged BEFORE the trace + request-id layers** so NC requests
|
||||
@@ -526,7 +588,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// fields as every other surface — see
|
||||
// `interfaces/middleware/trace_span.rs::ClientIpMakeSpan`.
|
||||
if let Some(nc_router) = nextcloud_router {
|
||||
app = app.merge(nc_router.with_state(app_state.clone()));
|
||||
app = app.merge(
|
||||
nc_router
|
||||
.with_state(app_state.clone())
|
||||
.layer(access_log!("http::nextcloud")),
|
||||
);
|
||||
}
|
||||
|
||||
// Mount WOPI routes (protocol routes use own token auth, API routes behind auth middleware).
|
||||
@@ -540,8 +606,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
auth_middleware,
|
||||
));
|
||||
app = app
|
||||
.nest("/wopi", wopi_protocol)
|
||||
.nest("/api/wopi", wopi_api_protected);
|
||||
.nest("/wopi", wopi_protocol.layer(access_log!("http::wopi")))
|
||||
.nest(
|
||||
"/api/wopi",
|
||||
wopi_api_protected.layer(access_log!("http::api")),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Trace + request-id layers applied LAST so every route
|
||||
@@ -550,11 +619,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// only have to be merged before this point to get tracing
|
||||
// for free — no second site to remember to update.
|
||||
app = app
|
||||
.layer(
|
||||
TraceLayer::new_for_http()
|
||||
.make_span_with(ClientIpMakeSpan)
|
||||
.on_response(LogBadRequest),
|
||||
)
|
||||
.layer(TraceLayer::new_for_http().make_span_with(ClientIpMakeSpan))
|
||||
.layer(PropagateRequestIdLayer::x_request_id())
|
||||
.layer(SetRequestIdLayer::x_request_id(UuidRequestId));
|
||||
} else {
|
||||
@@ -562,38 +627,40 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing::warn!("Authentication is DISABLED — all API routes are publicly accessible");
|
||||
app = Router::new()
|
||||
// Health / readiness probes — no auth, mounted at root
|
||||
.merge(health_routes)
|
||||
.nest("/api", public_api_routes)
|
||||
.nest("/api", api_routes)
|
||||
.merge(health_routes.layer(access_log!("http::probe")))
|
||||
.nest("/api", public_api_routes.layer(access_log!("http::api")))
|
||||
.nest("/api", api_routes.layer(access_log!("http::api")))
|
||||
// RFC 6764 well-known discovery (just redirects)
|
||||
.merge(well_known_router)
|
||||
.merge(well_known_router.layer(access_log!("http::dav")))
|
||||
// CalDAV/CardDAV/WebDAV protocols merged at top-level
|
||||
.merge(caldav_router)
|
||||
.merge(carddav_router)
|
||||
.merge(webdav_router)
|
||||
.merge(web_routes);
|
||||
.merge(caldav_router.layer(access_log!("http::dav")))
|
||||
.merge(carddav_router.layer(access_log!("http::dav")))
|
||||
.merge(webdav_router.layer(access_log!("http::dav")))
|
||||
.merge(web_routes.layer(access_log!("http::web")));
|
||||
|
||||
// Mount Nextcloud routes — merged BEFORE the trace + request-id
|
||||
// layers so NC requests get the same span fields as every
|
||||
// other surface (matches the auth-enabled branch above).
|
||||
if let Some(nc_router) = nextcloud_router {
|
||||
app = app.merge(nc_router.with_state(app_state.clone()));
|
||||
app = app.merge(
|
||||
nc_router
|
||||
.with_state(app_state.clone())
|
||||
.layer(access_log!("http::nextcloud")),
|
||||
);
|
||||
}
|
||||
|
||||
// Mount WOPI routes (no auth middleware when auth is disabled).
|
||||
// Same reasoning: merge before the trace layer.
|
||||
if let Some((wopi_protocol, wopi_api)) = wopi_routes {
|
||||
app = app.nest("/wopi", wopi_protocol).nest("/api/wopi", wopi_api);
|
||||
app = app
|
||||
.nest("/wopi", wopi_protocol.layer(access_log!("http::wopi")))
|
||||
.nest("/api/wopi", wopi_api.layer(access_log!("http::api")));
|
||||
}
|
||||
|
||||
// ── Trace + request-id layers applied LAST. See the
|
||||
// auth-enabled branch above for the rationale.
|
||||
app = app
|
||||
.layer(
|
||||
TraceLayer::new_for_http()
|
||||
.make_span_with(ClientIpMakeSpan)
|
||||
.on_response(LogBadRequest),
|
||||
)
|
||||
.layer(TraceLayer::new_for_http().make_span_with(ClientIpMakeSpan))
|
||||
.layer(PropagateRequestIdLayer::x_request_id())
|
||||
.layer(SetRequestIdLayer::x_request_id(UuidRequestId));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user