HTTP reverse proxy for API audit logging. Provide full request/response/timing observability.
1.1K
A lightweight HTTP reverse proxy for API audit logging. Sits between your clients and backend, recording every request and response with precise timing — even when the client times out. Log everything, or only when errors occur. Traffic is never modified.
Built for teams that need full observability over third-party or internal APIs: who called what, when, what the backend returned, and how long each layer took.
zerolog, including body, headers, status codes, and timinglogged_endpoints for full logging, error_endpoints for logging only on selected HTTP error codes, proxied_endpoints for silent passthrough (health checks, static assets)dispatch_ms), pure backend time (backend_duration_ms), and total wall time (total_duration_ms)disconnectedGET /gate-health reports auditgate status and backend reachabilityAuthorization, Cookie) from logs without affecting proxied trafficlumberjack: size limit, backup count, age, optional gzip compressionsync.Pool for request/response body buffers to minimise allocations under loadconfig.json (see Configuration below)services:
auditgate:
image: ygolovnia/auditgate:latest
ports:
- "8080:8080"
volumes:
- ./config.json:/app/config.json:ro
environment:
- TZ=Europe/Kyiv
- LOG_TO_STDOUT=true
logging:
driver: "json-file"
options:
max-size: "50m"
max-file: "10"
restart: unless-stopped
curl http://localhost:8080/posts/1curl http://localhost:8080/gate-healthThe config follows the natural user story: where to proxy → what to log → how to log it → where to store → operations.
{
"target_url": "https://jsonplaceholder.typicode.com",
"logged_endpoints": [
"GET /posts",
"POST /posts"
],
"error_endpoints": [
"GET /posts/*",
"GET /products"
],
"log_error_codes": [400, 401, 403, 404, 500, 502, 503],
"proxied_endpoints": [
"GET /*"
],
"log_headers": false,
"forbidden_headers": ["Authorization", "Cookie", "X-Api-Key"],
"log_file": "./logs/proxy.log",
"log_level": "info",
"max_size_mb": 50,
"max_backups": 10,
"max_age_days": 30,
"compress": true,
"health_endpoint": "GET /health",
"allowed_cidrs": [],
"deny_by_default": false,
"drop_bad_connections": true,
"shutdown_timeout_seconds": 25,
"port": 8080
}
| Mode | Field | Behaviour |
|---|---|---|
| Full logging | logged_endpoints | Every request and response is logged |
| Error-only logging | error_endpoints | Silent on success; logs request + response only when backend returns a code from log_error_codes |
| Silent passthrough | proxied_endpoints | Proxied without any logging. Use GET /* as catch-all |
Priority when a request matches multiple lists: logged_endpoints → error_endpoints → proxied_endpoints.
| Pattern | Matches |
|---|---|
GET /posts | Only /posts (exact) |
GET /posts/* | /posts, /posts/1, /posts/1/comments, … |
GET / | Only / (exact) |
GET /* | Any path — catch-all |
| Field | Type | Description |
|---|---|---|
target_url | string | Backend base URL |
logged_endpoints | []string | METHOD /path — proxied with full logging |
error_endpoints | []string | METHOD /path — logged only on matching error codes |
log_error_codes | []int | HTTP status codes that trigger logging for error_endpoints |
proxied_endpoints | []string | METHOD /path — proxied silently, no logs |
log_level | string | debug, info, warn, error |
log_headers | bool | Include request headers in logs |
forbidden_headers | []string | Headers stripped from logs (traffic is unaffected) |
log_file | string | Path to log file |
max_size_mb | int | Max log file size before rotation |
max_backups | int | Number of rotated files to keep |
max_age_days | int | Max age of rotated files |
compress | bool | Gzip rotated log files |
health_endpoint | string | METHOD /path on the backend to probe for /gate-health. Omit to skip backend check |
allowed_cidrs | []string | Allowlisted IP ranges. Empty = allow all |
deny_by_default | bool | Block all IPs not in allowed_cidrs |
drop_bad_connections | bool | Log count of TCP-dropped connections every 10s |
shutdown_timeout_seconds | int | Graceful shutdown window |
port | int | Port auditgate listens on |
Each request produces two log lines — request on arrival, response on completion:
{"level":"info","hostname":"346acb56e7ca","service":"auditgate","version":"dev","env":"local","request_id":"d17060f0-8e6f-4ece-adff-6b7cdf06cf46","method":"GET","path":"/posts/1","remote_addr":"172.19.0.1:56188","time":"2026-07-04T13:26:14.274182644+03:00","event":"request"}
{"level":"info","hostname":"346acb56e7ca","service":"auditgate","version":"dev","env":"local","request_id":"d17060f0-8e6f-4ece-adff-6b7cdf06cf46","method":"GET","path":"/posts/1","backend_status_code":200,"dispatch_ms":0,"backend_duration_ms":467,"total_duration_ms":467,"proxy_overhead_ms":0,"client_status_code":200,"backend_response_body":"{...}","time":"2026-07-04T13:26:14.742045074+03:00","event":"response"}
On client disconnect (-m 0.1 adds 100ms timeout) — curl -m 0.1 http://localhost:8080/posts/1:
{"level":"info","hostname":"346acb56e7ca","service":"auditgate","version":"dev","env":"local","request_id":"949fe0f0-6619-49b3-b623-4481ca779fc5","method":"GET","path":"/posts/1","remote_addr":"172.19.0.1:35132","time":"2026-07-04T13:26:50.429669751+03:00","event":"request"}
{"level":"warn","hostname":"346acb56e7ca","service":"auditgate","version":"dev","env":"local","client_status":"disconnected","client_alive_ms":99,"request_id":"949fe0f0-6619-49b3-b623-4481ca779fc5","method":"GET","path":"/posts/1","backend_status_code":200,"dispatch_ms":0,"backend_duration_ms":135,"total_duration_ms":135,"proxy_overhead_ms":0,"backend_response_body":"{...}","time":"2026-07-04T13:26:50.565285618+03:00","event":"response"}
| Field | Description |
|---|---|
dispatch_ms | Time from request arrival to the moment the backend call is fired — proxy-only cost |
backend_duration_ms | Pure backend latency (network + server processing) |
total_duration_ms | Wall time for the full round trip |
proxy_overhead_ms | total - backend — everything the proxy added |
GET /gate-health is always available and never proxied or logged.
curl http://localhost:8080/gate-health
When backend is reachable — HTTP 200:
{"status":"ok","backend":"ok"}
When backend is down or returns 5xx — HTTP 503:
{"status":"degraded","backend":"unreachable"}
Backend is probed using the method and path defined in health_endpoint. If health_endpoint is omitted, only auditgate itself is checked and backend is always "ok".
Auditgate JSON logs can be shipped to Loki via the Loki Docker Driver and visualised in Grafana with zero configuration. A ready-to-use Docker Compose stack with a pre-built Grafana dashboard is available in the auditgate-stack repository.
The dashboard includes request rate, error rate, backend latency percentiles (p50/p95/p99), proxy overhead, top paths by request count, slowest paths by average latency, and a live log stream.
Content type
Image
Digest
sha256:449bdea45…
Size
9.1 MB
Last updated
3 months ago
docker pull ygolovnia/auditgate