use std::sync::Arc;
use std::sync::atomic::Ordering;
use std::time::Instant;
use bytes::Bytes;
use hyper::{Response, StatusCode};
use zygo_core::supervisor::{Request as Control, Response as Reply};
use super::reply::{ApiBody, HttpError, json, reply_to_response, whole};
use super::routes_fn::mine;
use super::{Api, control};
pub(super) async fn healthz(api: &Arc<Api>) -> Result<Response<ApiBody>, HttpError> {
const FRESH: std::time::Duration = std::time::Duration::from_secs(1);
if let Some((at, body)) = api.health.lock().expect("health").clone()
&& at.elapsed() < FRESH
{
let stopping = body["status"] == "stopping";
return Ok(json(
if stopping {
StatusCode::SERVICE_UNAVAILABLE
} else {
StatusCode::OK
},
&body,
));
}
let reply = control(api, |c| Ok(c.send(&Control::Runtimes)?)).await;
let (status, body) = match reply {
Ok(Reply::Runtimes { runtimes }) => {
let below: Vec<&str> = runtimes
.iter()
.filter(|r| r.warm + r.paused < r.min_warm)
.map(|r| r.name.as_str())
.collect();
if below.is_empty() {
(
StatusCode::OK,
serde_json::json!({
"ok": true,
"status": "ok",
"uptime_s": api.started.elapsed().as_secs(),
}),
)
} else {
(
StatusCode::OK,
serde_json::json!({
"ok": true,
"status": "degraded",
"below_min_warm": below,
"uptime_s": api.started.elapsed().as_secs(),
}),
)
}
}
_ => (
StatusCode::SERVICE_UNAVAILABLE,
serde_json::json!({
"ok": false,
"status": "stopping",
"uptime_s": api.started.elapsed().as_secs(),
}),
),
};
*api.health.lock().expect("health") = Some((Instant::now(), body.clone()));
Ok(json(status, &body))
}
pub(super) async fn drain(api: &Arc<Api>, grace_ms: u64) -> Result<Response<ApiBody>, HttpError> {
let reply = control(api, move |c| Ok(c.send(&Control::Drain { grace_ms })?)).await?;
let response = reply_to_response(reply);
tokio::spawn(async {
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
std::process::exit(0);
});
Ok(response)
}
pub(super) async fn snapshot(api: &Arc<Api>) -> anyhow::Result<crate::cmd::otlp::Snapshot> {
let reply = control(api, |c| Ok(c.send(&Control::List)?)).await?;
let functions = match reply {
Reply::Functions { functions } => functions,
Reply::Error { code, message } => anyhow::bail!("{}: {message}", code.as_str()),
other => anyhow::bail!("unexpected answer to `list`: {other:?}"),
};
Ok(crate::cmd::otlp::Snapshot {
api_requests: api.requests.load(Ordering::Relaxed),
api_errors: api.errors.load(Ordering::Relaxed),
functions,
tenants: api.usage.lock().expect("usage").snapshot(),
})
}
pub(super) async fn metrics(
api: &Arc<Api>,
tenant: Option<String>,
) -> Result<Response<ApiBody>, HttpError> {
let snapshot = snapshot(api).await?;
let out = render_metrics(&snapshot, tenant.as_deref());
Ok(Response::builder()
.status(StatusCode::OK)
.header("content-type", "text/plain; version=0.0.4; charset=utf-8")
.body(whole(Bytes::from(out)))
.expect("a valid response"))
}
fn render_metrics(snapshot: &crate::cmd::otlp::Snapshot, tenant: Option<&str>) -> String {
let functions = mine(snapshot.functions.clone(), tenant);
use std::fmt::Write as _;
let mut out = String::new();
let _ = writeln!(
out,
"# HELP zygo_api_requests_total HTTP requests received."
);
let _ = writeln!(out, "# TYPE zygo_api_requests_total counter");
let _ = writeln!(out, "zygo_api_requests_total {}", snapshot.api_requests);
let _ = writeln!(
out,
"# HELP zygo_api_errors_total HTTP requests answered with an error."
);
let _ = writeln!(out, "# TYPE zygo_api_errors_total counter");
let _ = writeln!(out, "zygo_api_errors_total {}", snapshot.api_errors);
let _ = writeln!(
out,
"# HELP zygo_function_requests_total Requests served per function."
);
let _ = writeln!(out, "# TYPE zygo_function_requests_total counter");
for f in &functions {
let _ = writeln!(
out,
"zygo_function_requests_total{{fn=\"{}\"}} {}",
f.name, f.requests
);
}
let _ = writeln!(
out,
"# HELP zygo_function_failures_total Requests that failed per function."
);
let _ = writeln!(out, "# TYPE zygo_function_failures_total counter");
for f in &functions {
let _ = writeln!(
out,
"zygo_function_failures_total{{fn=\"{}\"}} {}",
f.name, f.failures
);
}
let _ = writeln!(
out,
"# HELP zygo_function_rss_bytes Resident memory of the warm zygote."
);
let _ = writeln!(out, "# TYPE zygo_function_rss_bytes gauge");
for f in &functions {
let _ = writeln!(
out,
"zygo_function_rss_bytes{{fn=\"{}\"}} {}",
f.name,
f.rss_kb * 1024
);
}
let _ = writeln!(
out,
"# HELP zygo_function_state Current state, one series per function set to 1."
);
let _ = writeln!(out, "# TYPE zygo_function_state gauge");
for f in &functions {
let _ = writeln!(
out,
"zygo_function_state{{fn=\"{}\",state=\"{}\"}} 1",
f.name,
f.state.as_str()
);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn status(name: &str, tenant: &str) -> zygo_core::pool::Status {
zygo_core::pool::Status {
name: name.into(),
tenant: tenant.into(),
image: String::new(),
state: zygo_core::sandbox::SandboxState::Warm,
runtime: "python/3.12".into(),
rss_kb: 0,
imports_ms: 0.0,
requests: 3,
failures: 1,
}
}
#[test]
fn metrics_show_one_tenant_their_own_series_only() {
let snapshot = crate::cmd::otlp::Snapshot {
api_requests: 7,
api_errors: 2,
functions: vec![
status("resize", "acme"),
status("resize-2", "globex"),
status("internal", "default"),
],
tenants: Vec::new(),
};
let operator = render_metrics(&snapshot, None);
for name in ["resize", "resize-2", "internal"] {
assert!(
operator.contains(&format!(
"zygo_function_requests_total{{fn=\"{name}\"}} 3\n"
)),
"the operator sees the host: {operator}"
);
}
let acme = render_metrics(&snapshot, Some("acme"));
assert!(acme.contains("zygo_api_requests_total 7\n"), "{acme}");
assert!(acme.contains("zygo_api_errors_total 2\n"), "{acme}");
assert!(acme.contains("zygo_function_requests_total{fn=\"resize\"} 3\n"));
assert!(acme.contains("zygo_function_failures_total{fn=\"resize\"} 1\n"));
assert!(acme.contains("zygo_function_rss_bytes{fn=\"resize\"} 0\n"));
assert!(acme.contains("zygo_function_state{fn=\"resize\",state=\"warm\"} 1\n"));
assert!(
!acme.contains("resize-2") && !acme.contains("internal"),
"a tenant token must not learn another tenant's names: {acme}"
);
assert!(acme.contains("# TYPE zygo_function_state gauge\n"));
}
}