#![allow(private_interfaces)]
use std::sync::Arc;
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::Json;
use serde::Serialize;
use super::{internal_error, open_vector_store, AppState, ErrorBody};
mod repos;
pub(super) use repos::*;
pub(super) async fn healthz() -> Json<serde_json::Value> {
Json(serde_json::json!({"status": "ok"}))
}
#[derive(Serialize)]
pub(super) struct StatusResponse {
status: String,
index: crate::types::IndexStats,
repos: Vec<String>,
sources: crate::config::SourcesConfig,
#[serde(skip_serializing_if = "Option::is_none")]
repo_path_prefix: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
quipu_endpoint: Option<String>,
}
pub(super) async fn status(
State(state): State<Arc<AppState>>,
) -> Result<Json<StatusResponse>, (StatusCode, Json<ErrorBody>)> {
let store = open_vector_store(&state).await.map_err(internal_error)?;
let stats = store
.get_stats(None)
.await
.map_err(|e| internal_error(e.into()))?;
let repos = store
.get_all_repos()
.await
.map_err(|e| internal_error(e.into()))?;
Ok(Json(StatusResponse {
status: "ok".to_string(),
index: stats,
repos,
sources: state.resolved_sources.clone(),
repo_path_prefix: state.config.server.repo_path_prefix.clone(),
quipu_endpoint: state.config.quipu_endpoint.clone(),
}))
}
#[derive(Serialize)]
pub(super) struct VersionFeatures {
knowledge: bool,
}
#[derive(Serialize)]
pub(super) struct VersionResponse {
version: &'static str,
git_sha: &'static str,
git_dirty: bool,
features: VersionFeatures,
}
pub(super) async fn version() -> Json<VersionResponse> {
Json(VersionResponse {
version: env!("CARGO_PKG_VERSION"),
git_sha: env!("BOBBIN_GIT_SHA"),
git_dirty: env!("BOBBIN_GIT_DIRTY") == "true",
features: VersionFeatures {
knowledge: cfg!(feature = "knowledge"),
},
})
}
pub(super) async fn metrics(State(state): State<Arc<AppState>>) -> impl IntoResponse {
let store = match open_vector_store(&state).await {
Ok(s) => s,
Err(_) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
[("content-type", "text/plain; version=0.0.4; charset=utf-8")],
"# Failed to open vector store\nbobbin_up 0\n".to_string(),
);
}
};
let stats = store.get_stats(None).await.ok();
let mut out = String::new();
out.push_str("# HELP bobbin_up Whether bobbin is running.\n");
out.push_str("# TYPE bobbin_up gauge\n");
out.push_str("bobbin_up 1\n");
out.push_str(&crate::operational_metrics::render_fts_metrics());
let coverage = store.fts_coverage().await;
out.push_str("# HELP bobbin_fts_index_present Whether a content FTS index exists.\n");
out.push_str("# TYPE bobbin_fts_index_present gauge\n");
out.push_str(&format!(
"bobbin_fts_index_present {}\n",
u8::from(coverage.is_some())
));
if let Some((indexed, unindexed)) = coverage {
out.push_str("# HELP bobbin_fts_indexed_rows Rows the content FTS index covers.\n");
out.push_str("# TYPE bobbin_fts_indexed_rows gauge\n");
out.push_str(&format!("bobbin_fts_indexed_rows {indexed}\n"));
out.push_str(
"# HELP bobbin_fts_unindexed_rows Rows outside the FTS index, scanned on every keyword query.\n",
);
out.push_str("# TYPE bobbin_fts_unindexed_rows gauge\n");
out.push_str(&format!("bobbin_fts_unindexed_rows {unindexed}\n"));
}
if let Some(s) = stats {
out.push_str("# HELP bobbin_index_files_total Total indexed files.\n");
out.push_str("# TYPE bobbin_index_files_total gauge\n");
out.push_str(&format!("bobbin_index_files_total {}\n", s.total_files));
out.push_str("# HELP bobbin_index_chunks_total Total indexed chunks.\n");
out.push_str("# TYPE bobbin_index_chunks_total gauge\n");
out.push_str(&format!("bobbin_index_chunks_total {}\n", s.total_chunks));
out.push_str("# HELP bobbin_index_embeddings_total Total embeddings.\n");
out.push_str("# TYPE bobbin_index_embeddings_total gauge\n");
out.push_str(&format!(
"bobbin_index_embeddings_total {}\n",
s.total_embeddings
));
}
let status = store.maintenance_status();
out.push_str(
"# HELP bobbin_maintenance_last_success_timestamp_seconds \
Unix time of the last completed prune/compact sweep of the vector store.\n",
);
out.push_str("# TYPE bobbin_maintenance_last_success_timestamp_seconds gauge\n");
for (op, ts) in [
("prune", status.last_prune_unix),
("compact", status.last_compact_unix),
] {
if let Some(ts) = ts {
out.push_str(&format!(
"bobbin_maintenance_last_success_timestamp_seconds{{op=\"{op}\"}} {ts}\n"
));
}
}
(
StatusCode::OK,
[("content-type", "text/plain; version=0.0.4; charset=utf-8")],
out,
)
}