jacquardSnapshot

← snapshot

2272 bytes
//! HTTP request telemetry: one span per request, constant name, templated
//! route — the shape the s10 console's `/perf` page recognises.

use axum::extract::{MatchedPath, Request, State};
use axum::middleware::Next;
use axum::response::Response;
use jac_telemetry::{Envelope, SpanStatus};

use crate::error::ErrorSignal;
use crate::state::SharedState;

/// Time source for span boundaries. Not `SystemClock` (that's a jacquard
/// engine port, one per repo); this is the surface's own wall clock, the
/// same allowance `SystemClock`'s own adapter takes.
#[expect(
    clippy::disallowed_types,
    reason = "the one place jac-serve reads wall time directly, for span timestamps outside any repo"
)]
fn now_ms() -> i64 {
    use std::time::{SystemTime, UNIX_EPOCH};
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map_or(0, |d| i64::try_from(d.as_millis()).unwrap_or(i64::MAX))
}

/// Wraps every request in a `http.server.request` span. `/api/health` is
/// excluded — a liveness probe is not a signal worth a tenant's attention.
pub(crate) async fn span_middleware(
    State(state): State<SharedState>,
    matched: Option<MatchedPath>,
    req: Request,
    next: Next,
) -> Response {
    let method = req.method().to_string();
    let route = matched.map_or_else(|| "unmatched".to_owned(), |m| m.as_str().to_owned());
    if route == "/api/health" {
        return next.run(req).await;
    }

    let start = now_ms();
    let response = next.run(req).await;
    let end = now_ms();
    let status = response.status().as_u16();
    let span_status = if status >= 500 {
        SpanStatus::Error
    } else {
        SpanStatus::Ok
    };
    let fault = response.extensions().get::<ErrorSignal>().cloned();
    state.telemetry.emit(
        Envelope::span("http.server.request", start, end, span_status)
            .attr("http.method", method.clone())
            .attr("http.route", route.clone())
            .attr("http.status_code", status.to_string()),
    );
    if status >= 500
        && let Some(ErrorSignal(message)) = fault
    {
        state.telemetry.emit(
            Envelope::bug(message, None, end)
                .attr("http.method", method)
                .attr("http.route", route),
        );
    }
    response
}