iocaine 1.0.0

The deadliest poison known to AI
Documentation
// SPDX-FileCopyrightText: 2025 Gergely Nagy
// SPDX-FileContributor: Gergely Nagy
//
// SPDX-License-Identifier: MIT

use axum::{
    body::Body,
    extract::{Request, State},
    middleware::Next,
    response::Response,
    routing::get,
    Router,
};
use metrics_exporter_prometheus::PrometheusBuilder;
use std::time::{SystemTime, UNIX_EPOCH};

use crate::{
    app::{shutdown_signal, AppError, StatefulIocaine},
    config::MetricsLabel,
};

pub async fn track_metrics(
    State(iocaine): State<StatefulIocaine>,
    req: Request,
    next: Next,
) -> Result<Response<Body>, AppError> {
    let headers = req.headers().clone();
    let response = next.run(req).await;
    let cfg = &iocaine.config.metrics;

    let mut labels = Vec::new();
    if cfg.labels.contains(&MetricsLabel::Host) {
        if let Some(host) = headers.get("host") {
            let host = host.to_str()?.to_string();
            labels.push(("host", host));
        }
    }

    if cfg.labels.contains(&MetricsLabel::UserAgent)
        || cfg.labels.contains(&MetricsLabel::UserAgentGroup)
    {
        if let Some(ua) = headers.get("user-agent") {
            let user_agent = ua.to_str()?.to_string();

            if cfg.labels.contains(&MetricsLabel::UserAgent) {
                labels.push(("user-agent", user_agent.clone()));
            }

            if cfg.labels.contains(&MetricsLabel::UserAgentGroup) {
                if let Some(group) = cfg
                    .agent_group
                    .iter()
                    .find(|agent_group_config| agent_group_config.agent.is_match(&user_agent))
                {
                    labels.push(("user-agent-group", group.group.clone()));
                }
            }
        }
    }

    metrics::counter!("iocaine_requests_total", &labels).increment(1);

    Ok(response)
}

pub async fn start_metrics_server(metrics_bind: String) -> std::result::Result<(), AppError> {
    let metrics_listener = tokio::net::TcpListener::bind(metrics_bind).await?;
    let recorder_handle = PrometheusBuilder::new().install_recorder()?;
    let app = Router::new().route("/metrics", get(|| async move { recorder_handle.render() }));

    let ts = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("Time went backwards");
    let labels = [("service", "iocaine".to_string())];
    metrics::gauge!("process_start_time_seconds", &labels).set(ts);

    Ok(axum::serve(metrics_listener, app)
        .with_graceful_shutdown(shutdown_signal())
        .await?)
}