# Axum (Rust)

> HTTP metrics from axum-prometheus, structured logs to stdout, both observed by the agent.

Canonical: https://docs.getlumin.dev/stacks/axum/

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.

## Labels on your container

```yaml
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"
```

## Emit Prometheus metrics

Use [`axum-prometheus`](https://github.com/Ptrskay3/axum-prometheus). Its defaults are exactly what the `axum` adapter looks for, so no configuration is needed.

```sh
cargo add axum-prometheus
```

```rust
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.

## What the agent maps

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.

### If you renamed the metrics

`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.

## Structured logs

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.

```rust
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.

## Nothing shows up

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`.
