A high-performance Axum middleware for relaying axum requests & responses to a background thread for asynchronous processing, with full streaming support, and minimal performance overhead.
- Stream-aware: Handles streaming request and response bodies without blocking
- Configurable body capture: Control whether to capture request/response bodies
- Background processing: All capture and processing happens asynchronously
- Extensible: Custom handlers for processing captured data
Add to your Cargo.toml:
[dependencies]
outlet = "0.3.0"
axum = "0.8"
tokio = { version = "1.0", features = ["full"] }
tower = "0.5"Here's a practical example that tracks API usage metrics:
use axum::{routing::get, Router, Json};
use outlet::{RequestLoggerLayer, RequestLoggerConfig, RequestHandler, RequestData, ResponseData};
use tower::ServiceBuilder;
use std::sync::{Arc, Mutex};
use std::collections::HashMap;
use serde_json::json;
// Track API usage metrics
#[derive(Debug, Default)]
struct MetricsHandler {
stats: Arc<Mutex<HashMap<String, u64>>>,
}
impl RequestHandler for MetricsHandler {
fn handle_request(&self, data: RequestData, _correlation_id: u64) {
// Count requests by endpoint
let endpoint = data.uri.path().to_string();
let mut stats = self.stats.lock().unwrap();
*stats.entry(endpoint).or_insert(0) += 1;
}
fn handle_response(&self, data: ResponseData, _correlation_id: u64) {
// Log slow requests for monitoring
if data.duration.as_millis() > 1000 {
println!("SLOW REQUEST: {} took {}ms",
data.status, data.duration.as_millis());
}
}
}
async fn hello() -> &'static str {
"Hello, World!"
}
async fn stats(metrics: axum::extract::State<Arc<Mutex<HashMap<String, u64>>>>) -> Json<serde_json::Value> {
let stats = metrics.lock().unwrap().clone();
Json(json!({ "request_counts": stats }))
}
#[tokio::main]
async fn main() {
let metrics = Arc::new(Mutex::new(HashMap::new()));
let handler = MetricsHandler { stats: metrics.clone() };
let layer = RequestLoggerLayer::new(RequestLoggerConfig::default(), handler);
let app = Router::new()
.route("/hello", get(hello))
.route("/stats", get(stats))
.with_state(metrics)
.layer(ServiceBuilder::new().layer(layer));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}Now your API automatically tracks request counts and identifies slow requests!
Customize the middleware behavior:
use outlet::{RequestLoggerConfig, RequestLoggerLayer, LoggingHandler};
let config = RequestLoggerConfig {
capture_request_body: true, // Whether to capture request bodies
capture_response_body: true, // Whether to capture response bodies
};
let handler = LoggingHandler;
let layer = RequestLoggerLayer::new(config, handler);Implement the [RequestHandler] trait to create custom processing logic:
use outlet::{RequestHandler, RequestData, ResponseData};
#[derive(Debug)]
struct CustomHandler;
impl RequestHandler for CustomHandler {
fn handle_request(&self, data: RequestData, correlation_id: u64) {
println!("Request: {} {}", data.method, data.uri);
// Custom processing logic here
}
fn handle_response(&self, data: ResponseData, correlation_id: u64) {
println!("Response: {} ({}ms)", data.status, data.duration.as_millis());
// Custom processing logic here
}
}cd outlet
cargo run --example demo
# In another terminal:
curl http://localhost:3000/hello
curl -X POST -d 'Hello there!' http://localhost:3000/echo
curl http://localhost:3000/streamingcargo testGenerate and view the full API documentation:
cargo doc --openThis project is licensed under the MIT License.