PostgreSQL logging handler for the
outlet HTTP request/response
middleware. This crate implements the RequestHandler
trait from outlet to log
HTTP requests and responses to PostgreSQL with JSONB serialization for bodies.
Features high-performance async logging with automatic table creation and structured query support.
Add this to your Cargo.toml
:
[dependencies]
outlet = "0.3.0"
outlet-postgres = "0.3.1"
axum = "0.8"
tokio = { version = "1.0", features = ["full"] }
tower = "0.5"
Basic usage:
use outlet::{RequestLoggerLayer, RequestLoggerConfig};
use outlet_postgres::PostgresHandler;
use axum::{routing::get, Router};
use tower::ServiceBuilder;
async fn hello() -> &'static str {
"Hello, World!"
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let database_url = "postgresql://user:password@localhost/dbname";
let handler = PostgresHandler::new(database_url).await?;
let layer = RequestLoggerLayer::new(RequestLoggerConfig::default(), handler);
let app = Router::new()
.route("/hello", get(hello))
.layer(ServiceBuilder::new().layer(layer));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app).await?;
Ok(())
}
The handler automatically creates two tables:
-
id
- Primary key -
correlation_id
- Links to corresponding response -
timestamp
- When the request was received -
method
- HTTP method (GET, POST, etc.) -
uri
- Full request URI -
headers
- Request headers as JSONB -
body
- Request body as JSONB (optional) -
body_parsed
- Whether the body was parsed as the supplied JSON-serde type (defaultserde_json::Value
) or not. If not, thebody
field is the base64-encoded binary data. -
created_at
- When the record was inserted
-
id
- Primary key -
correlation_id
- Links to corresponding request -
timestamp
- When the response was sent -
status_code
- HTTP status code -
headers
- Response headers as JSONB -
body
- Response body as JSONB (optional) -
body_parsed
- Whether the body was parsed as the supplied JSON-serde type (defaultserde_json::Value
) or not. If not, thebody
field is the base64-encoded binary data. -
duration_ms
- Request processing time in milliseconds -
created_at
- When the record was inserted
You can control what data is captured using RequestLoggerConfig
:
use outlet::RequestLoggerConfig;
// Capture everything (default)
let config = RequestLoggerConfig::default();
// Only capture requests, not responses
let config = RequestLoggerConfig {
capture_request_body: true,
capture_response_body: false,
};
// Headers only, no bodies
let config = RequestLoggerConfig {
capture_request_body: false,
capture_response_body: false,
};
Once you're logging requests, you can query the data:
-- Find all POST requests
SELECT method, uri, timestamp
FROM http_requests
WHERE method = 'POST'
ORDER BY timestamp DESC;
-- Find slow requests (> 1 second)
SELECT r.method, r.uri, s.status_code, s.duration_ms
FROM http_requests r
JOIN http_responses s ON r.correlation_id = s.correlation_id
WHERE s.duration_ms > 1000
ORDER BY s.duration_ms DESC;
-- Search request bodies for specific content
SELECT r.uri, r.body, s.status_code
FROM http_requests r
JOIN http_responses s ON r.correlation_id = s.correlation_id
WHERE r.body @> '{"user_id": 123}';
-- Get response statistics by endpoint
SELECT
r.uri,
COUNT(*) as request_count,
AVG(s.duration_ms) as avg_duration_ms,
MIN(s.duration_ms) as min_duration_ms,
MAX(s.duration_ms) as max_duration_ms
FROM http_requests r
JOIN http_responses s ON r.correlation_id = s.correlation_id
GROUP BY r.uri
ORDER BY request_count DESC;
-
Set up PostgreSQL and create a database
-
Set the
DATABASE_URL
environment variable:export DATABASE_URL="postgresql://user:password@localhost/outlet_demo"
-
Run the example:
cargo run --example basic_usage
-
Test the endpoints:
curl http://localhost:3000/ curl http://localhost:3000/users/42 curl -X POST http://localhost:3000/users -H "Content-Type: application/json" -d '{"name":"Alice","email":"alice@example.com"}' curl http://localhost:3000/large
MIT