tandem-server 0.6.2

HTTP server for Tandem engine APIs
use axum::extract::State;
use axum::http::header;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};

use crate::app::state::automation::scheduler::AutomationSchedulerMetrics;
use crate::AppState;

pub(super) async fn prometheus_metrics(State(state): State<AppState>) -> Response {
    if !crate::config::env::prometheus_metrics_enabled() {
        return StatusCode::NOT_FOUND.into_response();
    }

    let scheduler = state.automation_scheduler.read().await.metrics();
    let mut body = render_scheduler_metrics(&scheduler);
    body.push_str(&tandem_observability::render_observability_metrics_prometheus());

    (
        [(
            header::CONTENT_TYPE,
            "text/plain; version=0.0.4; charset=utf-8",
        )],
        body,
    )
        .into_response()
}

fn render_scheduler_metrics(metrics: &AutomationSchedulerMetrics) -> String {
    let mut out = String::new();
    render_metric_line(
        &mut out,
        "tandem_scheduler_active_runs",
        &[],
        metrics.active_runs as u64,
    );
    render_metric_line(
        &mut out,
        "tandem_scheduler_admitted_total",
        &[],
        metrics.admitted_total,
    );
    render_metric_line(
        &mut out,
        "tandem_scheduler_completed_total",
        &[],
        metrics.completed_total,
    );
    render_metric_line(
        &mut out,
        "tandem_scheduler_queue_wait_ms_avg",
        &[],
        metrics.avg_wait_ms,
    );
    render_metric_line(
        &mut out,
        "tandem_scheduler_queue_wait_ms_p95",
        &[],
        metrics.p95_wait_ms,
    );
    for (reason, count) in &metrics.queued_runs_by_reason {
        render_metric_line(
            &mut out,
            "tandem_scheduler_queued_runs",
            &[("reason", reason.as_str())],
            *count as u64,
        );
    }
    out
}

fn render_metric_line(out: &mut String, name: &str, labels: &[(&str, &str)], value: u64) {
    out.push_str(name);
    if !labels.is_empty() {
        out.push('{');
        for (idx, (key, value)) in labels.iter().enumerate() {
            if idx > 0 {
                out.push(',');
            }
            out.push_str(key);
            out.push_str("=\"");
            out.push_str(&escape_label_value(value));
            out.push('"');
        }
        out.push('}');
    }
    out.push(' ');
    out.push_str(&value.to_string());
    out.push('\n');
}

fn escape_label_value(value: &str) -> String {
    value
        .replace('\\', "\\\\")
        .replace('\n', "\\n")
        .replace('"', "\\\"")
}