xtrace 0.0.15

An AI observability service for collecting, storing, and querying traces, spans, and metrics across LLM and agent workflows.
Documentation
use axum::{
    extract::State,
    http::{header, HeaderMap, StatusCode},
    middleware::Next,
    response::IntoResponse,
    Json,
};
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use base64::Engine;
use chrono::Utc;

use crate::{
    http::common::ApiResponse,
    state::{mask_client_key, AppState},
};

enum AuthHeader {
    Bearer(String),
    Basic { username: String, password: String },
}

fn extract_auth(headers: &HeaderMap) -> Result<AuthHeader, ()> {
    let value = headers
        .get(header::AUTHORIZATION)
        .ok_or(())
        .and_then(|v| v.to_str().map_err(|_| ()))?
        .trim();

    if let Some(rest) = value.strip_prefix("Bearer ") {
        return Ok(AuthHeader::Bearer(rest.trim().to_string()));
    }

    if let Some(rest) = value.strip_prefix("Basic ") {
        let decoded = BASE64_STANDARD
            .decode(rest.trim().as_bytes())
            .map_err(|_| ())?;
        let decoded = std::str::from_utf8(&decoded).map_err(|_| ())?;
        let (username, password) = decoded.split_once(':').ok_or(())?;
        return Ok(AuthHeader::Basic {
            username: username.to_string(),
            password: password.to_string(),
        });
    }

    Err(())
}

fn extract_client_key(headers: &HeaderMap) -> String {
    if let Ok(auth) = extract_auth(headers) {
        match auth {
            AuthHeader::Bearer(token) => return format!("bearer:{token}"),
            AuthHeader::Basic { username, .. } => return format!("basic:{username}"),
        }
    }
    "anonymous".to_string()
}

pub(crate) async fn auth(
    State(state): State<AppState>,
    headers: HeaderMap,
    request: axum::extract::Request,
    next: Next,
) -> impl IntoResponse {
    let path = request.uri().path();
    let is_langfuse_compat = matches!(path, "/api/public/projects" | "/api/public/otel/v1/traces");
    let langfuse_auth_not_configured =
        state.langfuse_public_key.is_none() && state.langfuse_secret_key.is_none();

    match extract_auth(&headers) {
        Ok(AuthHeader::Bearer(token)) if token == state.api_bearer_token.as_ref() => {
            next.run(request).await
        }
        Ok(AuthHeader::Basic { username, password })
            if state
                .langfuse_public_key
                .as_deref()
                .is_some_and(|k| k == username)
                && state
                    .langfuse_secret_key
                    .as_deref()
                    .is_some_and(|k| k == password) =>
        {
            next.run(request).await
        }
        Err(()) if is_langfuse_compat && langfuse_auth_not_configured => next.run(request).await,
        Ok(AuthHeader::Basic { .. }) if is_langfuse_compat && langfuse_auth_not_configured => {
            next.run(request).await
        }
        _ => (
            StatusCode::UNAUTHORIZED,
            Json(ApiResponse::<serde_json::Value> {
                message: "Unauthorized".to_string(),
                code: Some("UNAUTHORIZED"),
                data: None,
            }),
        )
            .into_response(),
    }
}

pub(crate) async fn rate_limit(
    State(state): State<AppState>,
    headers: HeaderMap,
    request: axum::extract::Request,
    next: Next,
) -> axum::response::Response {
    let key = extract_client_key(&headers);

    match state.query_limiter.check_key(&key) {
        Ok(_) => {
            state.rate_limit_stats.record_allowed();
            next.run(request).await
        }
        Err(not_until) => {
            let masked = mask_client_key(&key);
            state.rate_limit_stats.record_rejected(&masked);
            let wait =
                not_until.wait_time_from(governor::clock::Clock::now(state.query_limiter.clock()));
            let retry_after_secs = wait.as_secs().max(1);
            let reset_at = Utc::now() + chrono::Duration::seconds(retry_after_secs as i64);

            let body = serde_json::json!({
                "message": "Too Many Requests",
                "code": "TOO_MANY_REQUESTS",
                "data": null,
                "meta": {
                    "rate_limit": {
                        "remaining": 0,
                        "reset_at": reset_at.to_rfc3339(),
                    }
                }
            });

            (
                StatusCode::TOO_MANY_REQUESTS,
                [(
                    header::RETRY_AFTER,
                    axum::http::HeaderValue::from_str(&retry_after_secs.to_string())
                        .unwrap_or_else(|_| axum::http::HeaderValue::from_static("1")),
                )],
                Json(body),
            )
                .into_response()
        }
    }
}