use std::sync::Arc;
use axum::response::IntoResponse;
use crate::core::ocla::OclaRegistry;
pub const ADMIN_TOKEN_ENV: &str = "LEAN_CTX_GATEWAY_ADMIN_TOKEN";
pub const DATABASE_URL_ENV: &str = "DATABASE_URL";
pub(crate) fn gateway_ocla_context() -> crate::core::ocla::types::OclaRequestContext {
crate::core::ocla::types::OclaRequestContext::new(
format!("gw-{}", uuid_short()),
"gateway".to_string(),
"gateway-server".to_string(),
String::new(),
None,
None,
)
}
fn uuid_short() -> String {
let mut bytes = [0u8; 8];
getrandom::fill(&mut bytes).unwrap_or_default();
hex::encode(bytes)
}
#[derive(Debug, Clone)]
pub struct ServeOptions {
pub port: u16,
pub admin_port: Option<u16>,
}
pub async fn serve(opts: ServeOptions) -> anyhow::Result<()> {
let cfg = crate::core::config::Config::load();
let admin_port = opts
.admin_port
.unwrap_or_else(|| opts.port.saturating_add(1));
crate::core::gain::live_pricing::spawn_background_refresh();
let pool = match std::env::var(DATABASE_URL_ENV) {
Ok(url) if !url.trim().is_empty() => match super::store::pool_from_database_url(&url) {
Ok(pool) => {
match super::store::init_schema(&pool).await {
Ok(()) => println!(" Store: usage_events ready (Postgres)"),
Err(e) => {
println!(
" Store: ⚠ Postgres unreachable at startup (fail-open): {e:#}"
);
}
}
if super::store::spawn_writer(pool.clone()) {
Some(pool)
} else {
tracing::warn!("usage sink already installed — store writer not started twice");
Some(pool)
}
}
Err(e) => {
println!(
" Store: ⚠ invalid {DATABASE_URL_ENV} (fail-open, metering off): {e:#}"
);
None
}
},
_ => {
println!(
" Store: off — set {DATABASE_URL_ENV} to enable org-wide usage_events metering"
);
None
}
};
if let Err(e) =
OclaRegistry::global()
.observation_hook
.observe(crate::core::ocla::types::Observation {
context: gateway_ocla_context(),
name: "gateway.started".to_string(),
attributes: std::collections::BTreeMap::from([(
"port".to_string(),
opts.port.to_string(),
)]),
})
{
tracing::debug!("OCLA observation: {e}");
}
if let Some(pool) = pool.clone() {
super::user_api::install_pool(pool);
println!(
" Me-View: http://<gateway-host>:{}/me — personal usage, sign in with your own gateway key",
opts.port
);
}
let mcp_registered = !cfg
.gateway_server
.resolve_mcp_servers(cfg.proxy.allows_insecure_http_upstream())
.is_empty();
if mcp_registered && let Some(pool) = pool.clone() {
match super::mcp::store::init_schema(&pool).await {
Ok(()) => println!(" MCP-Store: mcp_events + tool inventory ready (Postgres)"),
Err(e) => {
println!(" MCP-Store: ⚠ Postgres unreachable at startup (fail-open): {e:#}");
}
}
if !super::mcp::metering::spawn_writer(pool) {
tracing::warn!("mcp metering sink already installed — writer not started twice");
}
} else if mcp_registered {
println!(" MCP-Store: off — set {DATABASE_URL_ENV} to meter MCP tool calls (mcp_events)");
}
match (pool.clone(), admin_token()) {
(Some(pool), Some(token)) => {
let state = super::admin_api::AdminState {
pool,
seats: cfg.gateway_server.seats,
org_label: cfg.gateway_server.org_label.clone(),
started_at: std::time::Instant::now(),
providers: super::admin_status::provider_statuses(&cfg.proxy.resolve_providers()),
routing_enabled: cfg.proxy.routing.is_active(),
routing_aliases: cfg.proxy.routing.aliases.clone(),
reference_model: cfg.proxy.baseline.reference_model.clone(),
local_shadow_rate: cfg.proxy.baseline.effective_local_shadow_rate(),
mcp_servers: cfg
.gateway_server
.resolve_mcp_servers(cfg.proxy.allows_insecure_http_upstream()),
};
let router = admin_router(state, token);
let bind_host = cfg.gateway_server.resolved_admin_bind_host();
let addr = std::net::SocketAddr::new(bind_host, admin_port);
let listener = tokio::net::TcpListener::bind(addr).await?;
let exposure = if bind_host.is_loopback() {
"host-local"
} else {
"network-exposed — front with TLS"
};
println!(
" Admin: http://{addr}/ ({exposure}) — dashboard + /api/admin/* + /metrics (Bearer via {ADMIN_TOKEN_ENV})"
);
tokio::spawn(async move {
if let Err(e) = axum::serve(
listener,
router.into_make_service_with_connect_info::<std::net::SocketAddr>(),
)
.await
{
tracing::warn!("admin listener terminated (proxy unaffected): {e:#}");
}
});
}
(Some(_), None) => {
println!(
" Admin: off — set {ADMIN_TOKEN_ENV} to serve the dashboard + /api/admin/*"
);
}
(None, _) => {
println!(" Admin: off — requires the usage store ({DATABASE_URL_ENV})");
}
}
if let Some(pool) = pool.clone() {
tokio::spawn(async move {
loop {
match super::store::budget_window_sums(&pool).await {
Ok((person_day, project_month)) => {
crate::proxy::policy_gate::seed_from_store(person_day, project_month);
}
Err(e) => {
tracing::debug!("budget seed skipped (store unreachable): {e:#}");
}
}
tokio::time::sleep(std::time::Duration::from_secs(30)).await;
}
});
}
let retention_days = crate::core::config::Config::load()
.gateway_server
.usage_retention_days
.unwrap_or(0);
if retention_days > 0
&& let Some(pool) = pool.clone()
{
println!(
" Retention: usage_events + mcp_events kept {retention_days} days (purge every 6h)"
);
tokio::spawn(async move {
loop {
match super::store::purge_events_older_than(&pool, retention_days).await {
Ok(0) => {}
Ok(purged) => {
tracing::info!(
"usage retention: purged {purged} events older than {retention_days} days"
);
}
Err(e) => {
tracing::debug!("usage retention purge skipped: {e:#}");
}
}
match super::mcp::store::purge_events_older_than(&pool, retention_days).await {
Ok(0) | Err(_) => {}
Ok(purged) => {
tracing::info!(
"mcp retention: purged {purged} events older than {retention_days} days"
);
}
}
tokio::time::sleep(std::time::Duration::from_hours(6)).await;
}
});
}
println!("lean-ctx gateway: starting proxy on port {} …", opts.port);
let result = crate::proxy::start_proxy(opts.port).await;
if pool.is_some() {
drain_usage_queue(std::time::Duration::from_secs(5)).await;
}
result
}
async fn drain_usage_queue(max_wait: std::time::Duration) {
let started = std::time::Instant::now();
let pending =
|| crate::proxy::usage_sink::pending_count() + super::mcp::metering::pending_count();
let mut left = pending();
while left > 0 && started.elapsed() < max_wait {
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
left = pending();
}
if left > 0 {
tracing::warn!("shutdown drain window elapsed with {left} event(s) unflushed");
} else {
println!(" Store: usage queue drained.");
}
}
fn admin_token() -> Option<String> {
std::env::var(ADMIN_TOKEN_ENV)
.ok()
.map(|t| t.trim().to_string())
.filter(|t| !t.is_empty())
}
fn admin_router(state: super::admin_api::AdminState, token: String) -> axum::Router {
let token = Arc::new(token);
let throttle = Arc::new(super::security::AuthThrottle::default());
super::admin_api::router(state)
.route("/metrics", axum::routing::get(metrics_handler))
.layer(axum::middleware::from_fn(move |req, next| {
let token = token.clone();
let throttle = throttle.clone();
admin_auth_guard(req, next, token, throttle)
}))
.route("/healthz", axum::routing::get(|| async { "ok" }))
.merge(super::admin_ui::router())
.layer(axum::middleware::from_fn(super::security::security_headers))
}
async fn admin_auth_guard(
req: axum::extract::Request,
next: axum::middleware::Next,
expected: Arc<String>,
throttle: Arc<super::security::AuthThrottle>,
) -> Result<axum::response::Response, axum::response::Response> {
let client_ip = req
.extensions()
.get::<axum::extract::ConnectInfo<std::net::SocketAddr>>()
.map_or(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), |c| {
c.0.ip()
});
if throttle.is_blocked(client_ip) {
tracing::warn!("admin auth throttled: {client_ip} exceeded the failed-attempt budget");
return Err((
axum::http::StatusCode::TOO_MANY_REQUESTS,
[(axum::http::header::RETRY_AFTER, "60")],
axum::Json(serde_json::json!({"error": "too many failed attempts — retry later"})),
)
.into_response());
}
let ok = req
.headers()
.get("authorization")
.and_then(|v| v.to_str().ok())
.and_then(|auth| auth.strip_prefix("Bearer "))
.is_some_and(|token| constant_time_eq(token.as_bytes(), expected.as_bytes()));
if ok {
throttle.record_success(client_ip);
Ok(next.run(req).await)
} else {
let failures = throttle.record_failure(client_ip);
let path = req.uri().path();
tracing::warn!("admin auth failed: ip={client_ip} path={path} window_failures={failures}");
Err((
axum::http::StatusCode::UNAUTHORIZED,
axum::Json(
serde_json::json!({"error": format!("Bearer token required ({ADMIN_TOKEN_ENV})")}),
),
)
.into_response())
}
}
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
let ha = blake3::hash(a);
let hb = blake3::hash(b);
ha.as_bytes()
.iter()
.zip(hb.as_bytes())
.fold(0u8, |acc, (x, y)| acc | (x ^ y))
== 0
}
async fn metrics_handler() -> axum::response::Response {
let mut out = String::with_capacity(2048);
render_metrics(&mut out);
let metrics = vec![crate::core::ocla::types::MetricPoint {
context: gateway_ocla_context(),
name: "gateway.admin.metrics_query".to_string(),
value_milli: 1_000,
dimensions: std::collections::BTreeMap::new(),
}];
if let Err(e) = OclaRegistry::global()
.metrics_exporter
.export_metrics(metrics)
{
tracing::debug!("OCLA metrics export: {e}");
}
(
[(
axum::http::header::CONTENT_TYPE,
"text/plain; version=0.0.4",
)],
out,
)
.into_response()
}
fn render_metrics(out: &mut String) {
use std::fmt::Write as _;
let mut spend = crate::proxy::usage_meter::snapshot();
spend.sort_by(|a, b| a.model.cmp(&b.model));
let _ = writeln!(
out,
"# HELP leanctx_model_requests_total Measured requests per served model.\n# TYPE leanctx_model_requests_total counter"
);
for m in &spend {
let _ = writeln!(
out,
"leanctx_model_requests_total{{model=\"{}\"}} {}",
escape_label(&m.model),
m.requests
);
}
let _ = writeln!(
out,
"# HELP leanctx_model_tokens_total Billed tokens per served model and direction.\n# TYPE leanctx_model_tokens_total counter"
);
for m in &spend {
let model = escape_label(&m.model);
let _ = writeln!(
out,
"leanctx_model_tokens_total{{model=\"{model}\",direction=\"input\"}} {}",
m.input_tokens
);
let _ = writeln!(
out,
"leanctx_model_tokens_total{{model=\"{model}\",direction=\"output\"}} {}",
m.output_tokens
);
let _ = writeln!(
out,
"leanctx_model_tokens_total{{model=\"{model}\",direction=\"cache_read\"}} {}",
m.cache_read_tokens
);
}
let _ = writeln!(
out,
"# HELP leanctx_model_cost_usd_total Measured provider cost per served model (USD).\n# TYPE leanctx_model_cost_usd_total counter"
);
for m in &spend {
let _ = writeln!(
out,
"leanctx_model_cost_usd_total{{model=\"{}\"}} {}",
escape_label(&m.model),
m.cost_usd
);
}
let ledger = crate::core::savings_ledger::summary();
let _ = writeln!(
out,
"# HELP leanctx_saved_tokens_total Verified net tokens saved (signed ledger).\n# TYPE leanctx_saved_tokens_total counter\nleanctx_saved_tokens_total {}",
ledger.net_saved_tokens()
);
let _ = writeln!(
out,
"# HELP leanctx_saved_usd_total Verified USD saved (signed ledger).\n# TYPE leanctx_saved_usd_total counter\nleanctx_saved_usd_total {}",
ledger.saved_usd
);
for (mechanism, tokens, usd) in &ledger.by_mechanism {
let _ = writeln!(
out,
"leanctx_saved_by_mechanism_tokens_total{{mechanism=\"{}\"}} {tokens}",
escape_label(mechanism)
);
let _ = writeln!(
out,
"leanctx_saved_by_mechanism_usd_total{{mechanism=\"{}\"}} {usd}",
escape_label(mechanism)
);
}
let _ = writeln!(
out,
"# HELP leanctx_usage_events_dropped_total Usage events dropped because the store writer was saturated (fail-open).\n# TYPE leanctx_usage_events_dropped_total counter\nleanctx_usage_events_dropped_total {}",
crate::proxy::usage_sink::dropped_count()
);
let _ = writeln!(
out,
"# HELP leanctx_mcp_events_dropped_total MCP exchanges dropped because the metering writer was saturated (fail-open).\n# TYPE leanctx_mcp_events_dropped_total counter\nleanctx_mcp_events_dropped_total {}",
super::mcp::metering::dropped_count()
);
let (blocked_model, blocked_budget, blocked_rate) =
crate::proxy::policy_gate::blocked_counters();
let _ = writeln!(
out,
"# HELP leanctx_policy_blocked_total Requests refused by the enforced org policy.\n# TYPE leanctx_policy_blocked_total counter"
);
let _ = writeln!(
out,
"leanctx_policy_blocked_total{{reason=\"model_ceiling\"}} {blocked_model}"
);
let _ = writeln!(
out,
"leanctx_policy_blocked_total{{reason=\"budget\"}} {blocked_budget}"
);
let _ = writeln!(
out,
"leanctx_policy_blocked_total{{reason=\"rate_limit\"}} {blocked_rate}"
);
}
fn escape_label(v: &str) -> String {
v.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn label_escaping_covers_prometheus_specials() {
assert_eq!(escape_label(r#"a"b\c"#), r#"a\"b\\c"#);
assert_eq!(escape_label("x\ny"), "x\\ny");
}
#[test]
fn metrics_render_is_valid_exposition_shape() {
let mut out = String::new();
render_metrics(&mut out);
for line in out.lines().filter(|l| !l.starts_with('#') && !l.is_empty()) {
let (name_part, value) = line.rsplit_once(' ').expect("metric line has value");
assert!(
value.parse::<f64>().is_ok(),
"metric value must be numeric: {line}"
);
assert!(
name_part.starts_with("leanctx_"),
"metric namespace: {line}"
);
}
assert!(out.contains("leanctx_usage_events_dropped_total"));
}
#[test]
fn admin_token_requires_non_empty() {
assert!(std::env::var(ADMIN_TOKEN_ENV).is_err() || admin_token().is_some());
}
#[test]
fn constant_time_eq_basic() {
assert!(constant_time_eq(b"abc", b"abc"));
assert!(!constant_time_eq(b"abc", b"abd"));
assert!(!constant_time_eq(b"abc", b"ab"));
}
#[test]
fn constant_time_eq_handles_wildly_different_lengths() {
assert!(!constant_time_eq(b"", b"nonempty"));
assert!(constant_time_eq(b"", b""));
let short = b"tok";
let long = [b'a'; 4096];
assert!(!constant_time_eq(short, &long));
}
}