af-web 0.2.0

Reusable axum web infrastructure: request-id + latency-SLO middleware, JSON error envelope, health. The cross-cutting half of agent_core/api/.
Documentation
//! `af-web` — reusable axum web infrastructure (the cross-cutting half of
//! `agent_core/api/`). Provides:
//!
//! - [`ApiError`] — JSON error envelope carrying the request id.
//! - [`middleware::request_id`] / [`middleware::latency_slo`] — request-id
//!   propagation and the API latency SLO (warn past [`middleware::SLO_BUDGET_MS`]).
//! - [`with_standard_middleware`] — wrap any `Router` with the standard stack.
//! - [`health`] — a ready-made health handler.
//! - [`metrics`] — a minimal Prometheus-compatible liveness metric.
//!
//! A service composes its product routes and wraps them:
//!
//! ```ignore
//! let app = with_standard_middleware(
//!     Router::new().route("/health", get(health)).merge(product_routes),
//! );
//! ```

pub mod error;
pub mod middleware;

use axum::routing::get;
use axum::{Json, Router};
use serde_json::{json, Value};

pub use error::ApiError;
pub use middleware::{latency_slo, request_id, RequestId, SLO_BUDGET_MS};

/// Standard health endpoint payload.
pub async fn health() -> Json<Value> {
    Json(json!({ "status": "ok" }))
}

/// Baseline metric shared by reference hosts. Products merge their own metric
/// families into the same endpoint when they need richer observability.
pub async fn metrics() -> ([(&'static str, &'static str); 1], &'static str) {
    (
        [("content-type", "text/plain; version=0.0.4")],
        "agent_factory_up 1\n",
    )
}

/// Wrap a router with the standard middleware stack: latency SLO (outermost,
/// so it times everything) over request-id (so the id exists for SLO logs).
pub fn with_standard_middleware<S>(router: Router<S>) -> Router<S>
where
    S: Clone + Send + Sync + 'static,
{
    router
        .layer(axum::middleware::from_fn(latency_slo))
        .layer(axum::middleware::from_fn(request_id))
}

/// A `Router` preseeded with `GET /health`. Services merge their routes onto it.
pub fn base_router<S>() -> Router<S>
where
    S: Clone + Send + Sync + 'static,
{
    Router::new()
        .route("/health", get(health))
        .route("/metrics", get(metrics))
}