Skip to content

Axum (Rust)

The agent observes an Axum service via two channels: stdout for logs, and a Prometheus /metrics endpoint for HTTP metrics. No application-side Lumin SDK — the agent reads what your service already exposes.

services:
api:
image: my-axum-api:latest
labels:
lumin.logs: "true"
lumin.service: "api"
lumin.metrics: "true"
lumin.metrics.port: "3000"
lumin.metrics.framework: "axum"

Use axum-prometheus. Its defaults are exactly what the axum adapter looks for, so no configuration is needed.

Terminal window
cargo add axum-prometheus
use axum::{routing::get, Router};
use axum_prometheus::PrometheusMetricLayer;
#[tokio::main]
async fn main() {
let (prometheus_layer, metric_handle) = PrometheusMetricLayer::pair();
let app = Router::new()
.route("/", get(|| async { "ok" }))
.route("/metrics", get(|| async move { metric_handle.render() }))
.layer(prometheus_layer);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}

Apply the layer to your routes, not to the /metrics route itself, or your charts count the agent’s own scrapes as application traffic.

With lumin.metrics.framework=axum the agent reads these series and re-keys them onto the canonical schema:

axum-prometheus series Becomes
axum_http_requests_total http.requests
axum_http_requests_duration_seconds_bucket http.latency_ms
axum_http_requests_duration_seconds_count http.latency_ms count
axum_http_requests_duration_seconds_sum http.latency_ms sum

Note requests_duration, not request_duration. That is the crate’s own spelling, and the adapter matches it exactly.

Labels map from the axum-prometheus defaults: endpoint becomes route, status becomes the status code, and method passes through. axum_http_response_body_size is dropped — it charts in bytes and would sit misleadingly beside the http. series.

PrometheusMetricLayerBuilder and the AXUM_HTTP_REQUESTS_TOTAL / AXUM_HTTP_REQUESTS_DURATION_SECONDS compile-time environment variables let you rename the series to the conventional Prometheus shape. If you did that, set lumin.metrics.framework=gin instead.

The two adapters share one mapping. The framework label only picks which series names the agent looks for, and the gin set is the conventional shape.

Write JSON to stdout and label the container with lumin.logs=true. The agent parses a level field directly and does not have to guess.

tracing_subscriber::fmt().json().init();

Without a level field the agent sniffs the first 30 characters of the line for error, warn, info, debug, fatal, panic, or trace, and defaults to INFO when nothing matches.

An unrecognised lumin.metrics.framework value is not an error. The agent falls back to the generic pass-through and keeps shipping metrics under their original names, so the symptom is a Metrics page whose KPI cards stay on their empty state rather than a log line. Check the spelling first, then confirm curl http://localhost:3000/metrics returns axum_http_requests_total.