Observability
Metrics, structured logs, distributed tracing, and health endpoints for rust-web-server.
Metrics
Built-in Prometheus endpoint
GET /metrics is always available and returns metrics in the Prometheus text exposition format. No configuration is required.
Server-wide counters updated by the request loop:
| Metric | Type | Description |
|---|---|---|
rws_requests_total |
Counter | Total HTTP requests received |
rws_errors_total |
Counter | Connections whose handling ended in an error (I/O or application error, or a panicked handler) — not a count of 5xx responses |
rws_active_connections |
Gauge | Number of currently open connections |
Per-route metrics with MetricsLayer
Add MetricsLayer to any application to record per-route counters and latency histograms:
use rust_web_server::app::App;
use rust_web_server::core::New;
use rust_web_server::metrics::MetricsLayer;
let app = App::new().wrap(MetricsLayer);MetricsLayer adds:
| Metric | Type | Labels | Description |
|---|---|---|---|
rws_route_requests_total |
Counter | method, path, status |
Requests per route and status code |
rws_route_duration_seconds |
Histogram | method, path |
Response time distribution |
Circuit breaker state
Present automatically — no MetricsLayer or other opt-in needed — for every backend known to circuit_breaker::global():
| Metric | Type | Labels | Description |
|---|---|---|---|
rws_circuit_breaker_state |
Gauge | backend |
0=closed, 1=half_open, 2=open |
Wire ReverseProxy::with_circuit_breaker(Arc::new(circuit_breaker::global())) to get this metric for free alongside automatic breaker integration.
Grafana / PromQL queries
# Request rate (requests per second over 1 m)
rate(rws_requests_total[1m])
# Error rate
rate(rws_errors_total[1m])
# p99 latency per route
histogram_quantile(0.99, rate(rws_route_duration_seconds_bucket[5m]))
# Top routes by request count
topk(10, sum by (path) (rate(rws_route_requests_total[5m])))
# Backends currently tripped open
rws_circuit_breaker_state == 2Logs
JSON structured logs (default)
By default the server writes one newline-delimited JSON object per request to stdout:
{"time":"2026-07-02T12:00:00Z","remote_addr":"127.0.0.1","method":"GET","path":"/healthz","protocol":"HTTP/1.1","status":200,"bytes":2}JSON logs are well-suited for ingestion by Loki, Fluentd, Datadog, and other structured-log collectors.
Combined Log Format
Set RWS_CONFIG_LOG_FORMAT=combined (via environment variable or rws.config.toml) for Apache Combined Log Format (CLF):
127.0.0.1 - - [02/Jul/2026:12:00:00 +0000] "GET /healthz HTTP/1.1" 200 2This format is compatible with GoAccess, AWStats, and most log-analysis tools.
To produce the same line yourself — in your own middleware or a custom server loop — call Log::combined:
use rust_web_server::log::Log;
// request: &Request, response: &Response, peer_addr: &SocketAddr
let line = Log::combined(&request, &response, &peer_addr);
println!("{}", line);
// 192.168.1.1 - - [29/Jun/2026:14:23:05 +0000] "GET /index.html HTTP/1.1" 200 1234RWS_CONFIG_LOG_FORMAT=off writes no access log at all.
Buffered writes
Access-log lines are buffered per worker thread and written to stdout by a
background thread every 100 ms — at once when a thread's buffer passes
32 KiB, when a thread exits, and when the process exits normally (including
a graceful shutdown on SIGTERM/SIGINT). Writing each line with its own
write under the process-wide stdout lock cost about a sixth of a small
request's CPU. A line can therefore appear up to 100 ms after its request;
lines from one thread keep their order, and every line carries its own
timestamp. Code that ends the process some other way can call
Log::flush_access_log() first.
Log aggregation in Kubernetes
The server writes all logs to stdout. Kubernetes log aggregation pipelines pick them up automatically:
# View logs in real time
kubectl logs -f deployment/my-rws-app
# Ship to Loki via Promtail — no application config required; just label the podAccess log rotation. Currently, redirect stdout to a log file and use an external tool such as logrotate if you need rotation outside of Kubernetes.
Tracing
OtelLayer
OtelLayer implements Middleware and propagates W3C traceparent headers. Add it to your application and configure an OTLP exporter:
use rust_web_server::app::App;
use rust_web_server::core::New;
use rust_web_server::otel::{self, ExporterConfig, OtelLayer, TracingConfig};
otel::setup(TracingConfig {
service_name: "my-service".to_string(),
exporter: ExporterConfig::Otlp {
endpoint: "http://tempo:4318".to_string(),
},
..TracingConfig::default()
});
let app = App::new().wrap(OtelLayer);The layer reads the incoming traceparent header, creates a span for each request (a child of the caller's span when one was sent), and exports completed spans to the configured OTLP endpoint. Start nested spans inside a request with otel::span(name) / otel::client_span(name) — see Distributed Tracing.
Supported backends
- Grafana Tempo — point
endpointat the OTLP HTTP port (default4318) - Jaeger — enable the OTLP receiver and point at port
4318 - OpenTelemetry Collector — use as a fan-out proxy to multiple backends
Distributed tracing across services
Include the traceparent header in outbound requests using the synchronous HTTP client:
use rust_web_server::http_client::Client;
let response = Client::new()
.get("http://downstream-service/api/data")
.header("traceparent", incoming_traceparent)
.send()?;Baggage propagation, and spans created automatically for database queries and outbound calls.
Health endpoints
Two health endpoints are built into every server instance without any configuration:
GET /healthz — liveness
Returns 200 OK as long as the binary is running. Use this as the Kubernetes liveness probe:
livenessProbe:
httpGet:
path: /healthz
port: 7878
initialDelaySeconds: 5
periodSeconds: 10GET /readyz — readiness
Returns 200 OK during normal operation. Returns 503 Service Unavailable during graceful shutdown (after SIGTERM is received, while in-flight requests are being drained). Use as the Kubernetes readiness probe:
readinessProbe:
httpGet:
path: /readyz
port: 7878
initialDelaySeconds: 3
periodSeconds: 5The readiness gate ensures the load balancer stops routing new traffic to a pod as soon as it begins draining, giving in-flight requests time to complete before the pod terminates.