apigate 1.0.0

Macro-driven API gateway for Rust: declarative routing, request transformation, and reverse proxying built on axum
Documentation
//! Runtime observability: the built-in tracing adapter plus a custom observer
//! for ApiGate runtime events.

use std::net::SocketAddr;

use serde::{Deserialize, Serialize};
use tracing_subscriber::{EnvFilter, fmt};

#[derive(Debug, Deserialize, Serialize)]
struct BuyInput {
    sale_id: String,
}

#[apigate::hook]
async fn require_auth(ctx: &mut apigate::PartsCtx) -> apigate::HookResult {
    if ctx.header("authorization").is_none() {
        return Err(apigate::ApigateError::unauthorized("missing authorization"));
    }
    Ok(())
}

#[apigate::map]
async fn passthrough_buy(input: BuyInput) -> apigate::MapResult<BuyInput> {
    Ok(input)
}

#[apigate::service(name = "sales", prefix = "/sales")]
mod sales {
    use super::*;

    #[apigate::get("/ping")]
    async fn ping() {}

    #[apigate::post("/buy", json = BuyInput, before = [require_auth], map = passthrough_buy)]
    async fn buy() {}
}

fn init_tracing() {
    let filter = EnvFilter::try_from_default_env()
        .unwrap_or_else(|_| EnvFilter::new("info,apigate=debug,apigate::proxy=trace"));

    fmt()
        .with_env_filter(filter)
        .with_target(true)
        .compact()
        .init();
}

fn observe(event: apigate::RuntimeEvent<'_>) {
    // Keep ApiGate's default structured tracing output.
    apigate::default_tracing_observer(event);

    // Add application-specific observability on top, for example audit logs.
    if let apigate::RuntimeEventKind::UpstreamSucceeded {
        backend_index,
        status,
        upstream_latency,
    } = event.kind
    {
        tracing::info!(
            target: "app::audit",
            service = event.service,
            route = event.route_path,
            backend_index,
            status = status.as_u16(),
            latency = ?upstream_latency,
            "gateway request completed"
        );
    }
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    init_tracing();

    let listen: SocketAddr = "127.0.0.1:8080".parse()?;

    let app = apigate::App::builder()
        .mount_service(sales::routes(), ["http://127.0.0.1:8081"])
        .runtime_observer(observe)
        .build()?;

    print!(
        "\
logging - http://{listen}

RUST_LOG=debug,apigate=trace cargo run --example logging

Ping:
  curl http://{listen}/sales/ping

Pipeline error (no auth):
  curl -X POST -H 'content-type: application/json' \
    -d '{{\"sale_id\":\"111\"}}' http://{listen}/sales/buy

Success:
  curl -X POST -H 'authorization: Bearer t' -H 'content-type: application/json' \
    -d '{{\"sale_id\":\"111\"}}' http://{listen}/sales/buy

Upstream: caddy run --config apigate/examples/upstream/Caddyfile
"
    );

    apigate::run(listen, app).await?;
    Ok(())
}