What if you could audit every HTTP request automatically without touching a single line of service code? That's exactly what AuditLayer does — like Express middleware or a Spring interceptor, it wraps the actual handler and records events before and after the request arrives.
It sits in the same role as Express's app.use(auditMiddleware) or Spring's HandlerInterceptor. Auditing happens in the outer shell — business logic is never touched.
tower Layer = the decorator pattern
Rust's tower library abstracts middleware through the concept of a Layer. A Layer takes an inner service, wraps it, and returns a new service. Stack multiple layers and they nest like an onion.
EndpointRule: deciding which endpoints to audit
You can audit every request, but usually you'll narrow the scope with EndpointRule. Rules match on path prefix and HTTP method, and also control whether the request and response bodies are captured.
examples/axum-demo/src/main.rslet audit_layer = AuditLayer::new(handle.clone())
.rules(vec![
EndpointRule::prefix("/api/")
.method(Method::PUT)
.capture_request() // capture request body
.capture_response() // capture response body
])
.filters(FilterSet::new().pre(|req| {
// health checks don't need auditing → Skip
if req.uri.path() == "/api/health" { FilterDecision::Skip }
else { FilterDecision::Audit }
}))
.target_extractor(|req, _res, _req_body, _res_body| {
// derive the target entity from the URL path
req.uri.path().strip_prefix("/api/docs/")
.map(|id| vec![TargetSpec::new("default_target", EntityInput::new(id)...)])
.unwrap_or_default()
});
Attaching the layer to an axum router takes exactly one line. Existing handler code is untouched.
examples/axum-demo/src/main.rslet app = Router::new()
.route("/api/docs/{id}", put(my_handler))
.layer(audit_layer) // this one line auto-audits /api/**
.route("/audit/logs", get(query_logs))
.with_state(handle);
Inside the flow: what call() does
When a request arrives, AuditService::call runs through these steps in order.
- EndpointRule matching — checks whether this request is in scope for auditing.
- pre-filter — checks exemption conditions (health checks, static files, etc.). A
Skipdecision proxies the request straight through. - Request body capture — if
capture_request_bodyis set, reads the body and reassembles the same bytes to pass on to the inner service. - Inner service call — the real handler runs.
- Response body capture — if
capture_response_bodyis set, reads the response bytes. - target_extractor — derives the target entity from the request and response information.
- post-filter — looks at the response to make the final exemption decision (e.g., skip 304s).
emit_unchecked— pushes the event into the pipeline. Non-blocking, so it never stalls the response path.
Enabling body capture loads the entire body into memory. Bodies larger than max_capture_bytes (default 1 MiB) are not captured — only a size note is stored. For endpoints that accept large file uploads, turn capture off or lower the limit.
① Explain what "tower Layer is the decorator pattern" means, using the onion analogy.
② What is the difference between a pre-filter and a post-filter? When would you use each?
③ Think about what type transformations happen internally when you write .layer(audit_layer).