Cookbook / Production
Application metrics
Counters, gauges and histograms on the same Prometheus endpoint as the HTTP metrics.
Metrics already records request counts and latencies. Register your own counters, gauges and histograms on the same registry and they appear on the same /metrics endpoint. Handles are cheap to clone and update with atomics, so recording a value in a hot path costs almost nothing.
use axeon::metrics::{Counter, CounterVec, GaugeVec, Histogram, Metrics, DEFAULT_BUCKETS};
use axeon::{Json, Server, ServerError};
use axeon::json;
use serde::Deserialize;
/// Handles for the app's own metrics, kept in shared state.
struct AppMetrics {
signups: Counter,
tokens: CounterVec,
queue_depth: GaugeVec,
step_seconds: Histogram,
}
#[derive(Deserialize)]
struct Usage {
model: String,
tokens: f64,
}
fn main() {
let metrics = Metrics::new();
let app_metrics = AppMetrics {
signups: metrics.counter("signups_total", "Accounts created."),
tokens: metrics.counter_vec("llm_tokens_total", "LLM tokens used.", &["model"]),
queue_depth: metrics.gauge_vec("queue_depth", "Jobs waiting.", &["queue"]),
step_seconds: metrics.histogram("workflow_step_seconds", "Step duration.", &DEFAULT_BUCKETS),
};
let mut app = Server::new();
app.middleware(metrics.clone()).mount("/", metrics.router()); // GET /metrics
app.state(app_metrics);
app.post("/signup", |req| async move {
req.state::<AppMetrics>().unwrap().signups.inc();
Ok::<_, ServerError>((201, "welcome"))
});
app.post("/usage", |req| async move {
let usage: Usage = req.json()?;
let m = req.state::<AppMetrics>().unwrap();
m.tokens.with(&[usage.model.as_str()]).inc_by(usage.tokens);
m.queue_depth.with(&["onboarding"]).set(17.0);
Ok::<_, ServerError>(Json(json!({ "recorded": usage.tokens })))
});
app.get("/work", |req| async move {
// Records the elapsed time when the timer is observed or dropped.
let _timer = req.state::<AppMetrics>().unwrap().step_seconds.start_timer();
tokio::time::sleep(std::time::Duration::from_millis(120)).await;
"done"
});
app.listen("127.0.0.1:3000").expect("server failed to start");
}
curl -X POST http://127.0.0.1:3000/signup
curl -X POST http://127.0.0.1:3000/usage -H 'content-type: application/json' \
-d '{"model":"qwen-max","tokens":812}'
curl http://127.0.0.1:3000/work
curl -s http://127.0.0.1:3000/metrics | grep -E 'signups|llm_tokens|workflow_step'
Notes#
- Keep label values to a small, fixed set, such as a model or a queue name, and never IDs or user input. Every distinct label combination is a separate time series that Prometheus has to store.
- Names and labels are validated when you register them, and registering a name again returns the same metric.
- The job and client crates plug into the same registry:
Worker::metrics(&metrics)in background jobs and.metrics(&metrics)on the outbound client.