Praxis exposes Prometheus metrics, structured access logs, and health endpoints for monitoring proxy behavior. This guide covers setup, metric reference, logging configuration, and usage patterns.
All observability endpoints are served from a
dedicated admin listener. Enable it by setting
admin.address in your config:
admin:
address: "127.0.0.1:9901"The admin listener exposes three endpoints:
| Path | Purpose |
|---|---|
/healthy |
Liveness probe - returns 200 once the server is accepting connections |
/ready |
Readiness probe - returns cluster health status; 503 when any cluster has zero healthy endpoints |
/metrics |
Prometheus text exposition format |
Any other path returns 404. The admin listener
must bind to a loopback address by default. Binding
to a non-loopback address requires
insecure_options.allow_public_admin: true.
By default, /ready returns aggregate counts only
(total, healthy, degraded clusters) without cluster
names. Set verbose: true to include per-cluster
detail:
admin:
address: "127.0.0.1:9901"
verbose: trueNon-verbose response (default):
{
"status": "ok",
"clusters": {
"total": 2,
"healthy": 2,
"degraded": 0
}
}Verbose response:
{
"status": "ok",
"clusters": {
"total": 2,
"healthy": 2,
"degraded": 0,
"detail": {
"api": {
"healthy": 3,
"unhealthy": 0,
"total": 3
},
"web": {
"healthy": 2,
"unhealthy": 0,
"total": 2
}
}
}
}Verbose mode exposes internal topology (cluster names, endpoint counts). Keep it off in production unless the admin port is network-isolated.
Praxis records two categories of Prometheus metrics: HTTP request metrics (always on when admin is enabled) and per-filter duration histograms (opt-in).
These are recorded automatically for every proxied request when the admin endpoint is enabled.
Total completed HTTP requests.
| Label | Values |
|---|---|
method |
GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE, CONNECT, OTHER |
status_class |
1xx, 2xx, 3xx, 4xx, 5xx, unknown |
route |
Route name or "unknown" |
cluster |
Cluster name or "none" |
Non-standard HTTP methods (e.g. PURGE) are
collapsed to OTHER to bound cardinality. Status
code 0 (no response written) maps to unknown.
Wall-clock duration of completed HTTP requests in
seconds. Uses the same label set as
praxis_http_requests_total.
Per-filter hook timing is opt-in. Enable it in the
metrics section:
metrics:
filter_duration: trueWall-clock duration of a single filter hook invocation in seconds.
| Label | Values |
|---|---|
filter |
Filter name (e.g. router, rate_limiter, access_log) |
phase |
request or response |
stream |
headers or body |
The four hook combinations are:
| Phase + Stream | Hook |
|---|---|
request + headers |
on_request |
request + body |
on_request_body |
response + headers |
on_response |
response + body |
on_response_body |
Enabling filter_duration without admin.address
records metrics internally but does not expose them.
A startup warning is logged in this case.
The /metrics endpoint returns Prometheus text
exposition format with content type
text/plain; version=0.0.4; charset=utf-8.
Example prometheus.yml scrape config:
scrape_configs:
- job_name: praxis
scrape_interval: 15s
static_configs:
- targets:
- "127.0.0.1:9901"For Kubernetes deployments with multiple replicas, use service discovery:
scrape_configs:
- job_name: praxis
scrape_interval: 15s
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels:
- __meta_kubernetes_pod_label_app
regex: praxis
action: keep
- source_labels:
- __meta_kubernetes_pod_annotation_prometheus_io_port
target_label: __address__
regex: (.+)
replacement: "${1}"
action: replaceRequests per second by status class:
sum by (status_class) (
rate(praxis_http_requests_total[5m])
)
Percentage of 5xx responses:
sum(rate(praxis_http_requests_total{status_class="5xx"}[5m]))
/
sum(rate(praxis_http_requests_total[5m]))
p50, p95, and p99 latency:
histogram_quantile(0.50,
sum by (le) (
rate(praxis_http_request_duration_seconds_bucket[5m])
)
)
histogram_quantile(0.99,
sum by (le) (
rate(praxis_http_request_duration_seconds_bucket[5m])
)
)
p99 latency broken down by upstream cluster:
histogram_quantile(0.99,
sum by (le, cluster) (
rate(praxis_http_request_duration_seconds_bucket[5m])
)
)
p95 filter execution time, ranked:
topk(10,
histogram_quantile(0.95,
sum by (le, filter) (
rate(praxis_filter_duration_seconds_bucket[5m])
)
)
)
Compare request vs response processing time for a specific filter:
histogram_quantile(0.95,
sum by (le, phase) (
rate(
praxis_filter_duration_seconds_bucket{filter="router"}[5m]
)
)
)
Praxis uses the access_log filter for structured
request/response logging. Logs are emitted via the
tracing framework, not written to a separate file.
Add the access_log filter to your filter chain:
filter_chains:
- name: observability
filters:
- filter: request_id
- filter: access_logEach completed request emits a structured log entry with these fields:
| Field | Description |
|---|---|
method |
HTTP method |
path |
Request path (sanitized) |
client_ip |
Client IP address |
status |
Response status code |
duration_ms |
Request duration in milliseconds |
cluster |
Upstream cluster name or - |
upstream |
Upstream address or - |
request_id |
Correlation ID or - |
request_body_bytes |
Request body size |
response_body_bytes |
Response body size |
For high-traffic deployments, reduce log volume with
sample_rate:
filter_chains:
- name: observability
filters:
- filter: access_log
sample_rate: 0.1sample_rate accepts values in (0.0, 1.0]. The
value 0.1 logs approximately 10% of requests.
Sampling uses a deterministic counter (every Nth
request), not random selection.
Set PRAXIS_LOG_FORMAT=json for structured JSON
output suitable for log aggregation pipelines:
PRAXIS_LOG_FORMAT=json cargo run -p praxis-proxyThe default format is human-readable text. Both formats include the same structured fields.
Control per-module log verbosity via
runtime.log_overrides in your config:
runtime:
log_overrides:
praxis_filter::pipeline: trace
praxis_protocol: debugThe base log level comes from the RUST_LOG
environment variable (defaults to info). Overrides
are additive - they set the level for specific
modules without changing the base level. Valid
levels: error, warn, info, debug, trace.
A complete config enabling all observability features:
admin:
address: "127.0.0.1:9901"
verbose: true
metrics:
filter_duration: true
listeners:
- name: web
address: "0.0.0.0:8080"
filter_chains:
- observability
- routing
filter_chains:
- name: observability
filters:
- filter: request_id
- filter: access_log
- name: routing
filters:
- filter: router
routes:
- path_prefix: "/"
cluster: backend
- filter: load_balancer
clusters:
- name: backend
endpoints:
- "10.0.0.1:8080"
health_check:
type: http
interval_secs: 5
path: /healthzThis enables:
- Prometheus scraping on
127.0.0.1:9901/metrics - Liveness and readiness probes with verbose cluster detail
- Per-filter hook duration histograms
- Structured access logs with request correlation IDs
- Active health checks on the backend cluster