arcly-http 0.4.0

Enterprise-grade NestJS-inspired web framework on axum: zero-lock DI, declarative controllers, multi-tenant data routing, transactional outbox, ABAC, and a self-documenting OpenAPI surface
Documentation
//! Prometheus metrics — install once, record per-request RED metrics.
//!
//! Call `init_metrics()` from `ArclyObservabilityPlugin::on_init`.
//! The returned `PrometheusHandle` is provided into the DI container so the
//! `/metrics` route handler can call `handle.render()` to produce the scrape body.

use std::sync::OnceLock;

use metrics_exporter_prometheus::{PrometheusBuilder, PrometheusHandle};

static HANDLE: OnceLock<PrometheusHandle> = OnceLock::new();

/// Install the Prometheus metrics recorder globally and return a render handle.
///
/// Idempotent: subsequent calls return the same handle without re-installing
/// the recorder (which would panic). Safe to call from tests that launch the
/// app multiple times in the same process.
pub fn init_metrics() -> PrometheusHandle {
    HANDLE
        .get_or_init(|| {
            let handle = PrometheusBuilder::new()
                .install_recorder()
                .expect("Prometheus metrics recorder");

            // Pre-register counters/histogram so they appear in the first scrape even
            // if no requests have arrived yet.
            metrics::describe_counter!(
                "http_requests_total",
                "Total number of HTTP requests handled."
            );
            metrics::describe_histogram!(
                "http_request_duration_seconds",
                "HTTP request duration in seconds."
            );
            metrics::describe_gauge!(
                "http_requests_in_flight",
                "Number of HTTP requests currently being processed."
            );

            handle
        })
        .clone()
}

/// Record one completed HTTP request into the global metrics recorder.
///
/// `route` must be the matched **pattern** (e.g. `/users/:id`), never the raw
/// path — using raw paths produces unbounded cardinality and will OOM the
/// Prometheus storage in production.
pub fn record_request(route: &str, method: &str, status: u16, duration_secs: f64) {
    let route = if route.is_empty() { "plugin" } else { route };
    let status = status.to_string();

    metrics::counter!(
        "http_requests_total",
        "method" => method.to_owned(),
        "route"  => route.to_owned(),
        "status" => status.clone(),
    )
    .increment(1);

    metrics::histogram!(
        "http_request_duration_seconds",
        "method" => method.to_owned(),
        "route"  => route.to_owned(),
        "status" => status,
    )
    .record(duration_secs);
}

/// Build the axum route handler for `GET /metrics`.
///
/// Returns the Prometheus text-format scrape body with `Content-Type:
/// text/plain; version=0.0.4`.
pub fn metrics_route_handler(
    handle: PrometheusHandle,
) -> impl Fn(
    crate::web::context::RequestContext,
) -> futures::future::BoxFuture<'static, axum::response::Response>
       + Send
       + Sync
       + Clone
       + 'static {
    move |_ctx| {
        let body = handle.render();
        Box::pin(async move {
            axum::response::Response::builder()
                .status(200)
                .header("Content-Type", "text/plain; version=0.0.4; charset=utf-8")
                .body(axum::body::Body::from(body))
                .unwrap()
        })
    }
}