use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct Statistics {
pub documents_total: u32,
pub documents_inbox: u32,
pub inbox_tag: u32,
pub inbox_tags: Vec<u32>,
pub document_file_type_counts: Vec<DocumentFileTypeCount>,
pub character_count: u64,
pub tag_count: u32,
pub correspondent_count: u32,
pub document_type_count: u32,
pub storage_path_count: u32,
pub current_asn: u32,
}
#[derive(Debug, Clone, Deserialize)]
pub struct DocumentFileTypeCount {
pub mime_type: String,
#[serde(rename = "mime_type_count")]
pub count: u32,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ServerStatus {
#[serde(rename = "pngx_version")]
pub version: String,
pub server_os: String,
pub install_type: String,
pub storage: Storage,
pub database: Database,
pub tasks: StatusTask,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Storage {
pub total: u64,
pub available: u64,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(from = "RawDatabase")]
pub struct Database {
pub db_type: String,
pub url: String,
pub status: Health,
pub migration_status: MigrationStatus,
}
#[derive(Debug, Clone, Deserialize)]
pub struct MigrationStatus {
pub latest_migration: Option<String>,
pub unapplied_migrations: Vec<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub enum Health {
#[serde(rename = "OK")]
Ok,
#[serde(untagged)]
NotOk(String),
}
#[derive(Debug, Clone, Deserialize)]
pub struct StatusTask {
#[serde(flatten)]
pub redis: RedisStatus,
#[serde(flatten)]
pub celery: CeleryStatus,
#[serde(flatten)]
pub index: IndexStatus,
#[serde(flatten)]
pub sanity_check: SanityCheckStatus,
#[serde(flatten)]
pub classifier: ClassifierStatus,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(from = "RawRedisStatus")]
pub struct RedisStatus {
pub url: String,
pub status: Health,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(from = "RawCeleryStatus")]
pub struct CeleryStatus {
pub status: Health,
pub url: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(from = "RawIndexStatus")]
pub struct IndexStatus {
pub status: Health,
pub last_modified: Option<chrono::DateTime<chrono::Utc>>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(from = "RawClassifierStatus")]
pub struct ClassifierStatus {
pub status: Health,
pub last_trained: Option<chrono::DateTime<chrono::Utc>>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(from = "RawSanityCheckStatus")]
pub struct SanityCheckStatus {
pub status: Health,
pub last_run: Option<chrono::DateTime<chrono::Utc>>,
}
#[derive(Deserialize)]
pub struct RawDatabase {
#[serde(rename = "type")]
pub db_type: String,
pub url: String,
pub status: String,
pub error: Option<String>,
pub migration_status: MigrationStatus,
}
#[derive(Deserialize)]
#[allow(clippy::struct_field_names)]
struct RawRedisStatus {
redis_url: String,
redis_status: String,
redis_error: Option<String>,
}
#[derive(Deserialize)]
#[allow(clippy::struct_field_names)]
struct RawCeleryStatus {
celery_status: String,
celery_url: String,
celery_error: Option<String>,
}
#[derive(Deserialize)]
#[allow(clippy::struct_field_names)]
struct RawIndexStatus {
index_status: String,
index_last_modified: Option<chrono::DateTime<chrono::Utc>>,
index_error: Option<String>,
}
#[derive(Deserialize)]
#[allow(clippy::struct_field_names)]
struct RawClassifierStatus {
classifier_status: String,
classifier_last_trained: Option<chrono::DateTime<chrono::Utc>>,
classifier_error: Option<String>,
}
#[derive(Deserialize)]
#[allow(clippy::struct_field_names)]
struct RawSanityCheckStatus {
sanity_check_status: String,
sanity_check_last_run: Option<chrono::DateTime<chrono::Utc>>,
sanity_check_error: Option<String>,
}
impl From<RawDatabase> for Database {
fn from(raw: RawDatabase) -> Self {
Self {
db_type: raw.db_type,
url: raw.url,
status: merge_status_with_error(&raw.status, raw.error),
migration_status: raw.migration_status,
}
}
}
impl From<RawRedisStatus> for RedisStatus {
fn from(raw: RawRedisStatus) -> Self {
Self {
url: raw.redis_url,
status: merge_status_with_error(&raw.redis_status, raw.redis_error),
}
}
}
impl From<RawCeleryStatus> for CeleryStatus {
fn from(raw: RawCeleryStatus) -> Self {
Self {
status: merge_status_with_error(&raw.celery_status, raw.celery_error),
url: raw.celery_url,
}
}
}
impl From<RawIndexStatus> for IndexStatus {
fn from(raw: RawIndexStatus) -> Self {
Self {
status: merge_status_with_error(&raw.index_status, raw.index_error),
last_modified: raw.index_last_modified,
}
}
}
impl From<RawClassifierStatus> for ClassifierStatus {
fn from(raw: RawClassifierStatus) -> Self {
Self {
status: merge_status_with_error(&raw.classifier_status, raw.classifier_error),
last_trained: raw.classifier_last_trained,
}
}
}
impl From<RawSanityCheckStatus> for SanityCheckStatus {
fn from(raw: RawSanityCheckStatus) -> Self {
Self {
status: merge_status_with_error(&raw.sanity_check_status, raw.sanity_check_error),
last_run: raw.sanity_check_last_run,
}
}
}
impl std::fmt::Display for Health {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Health::Ok => write!(f, "OK"),
Health::NotOk(err) => write!(f, "Error: {err}"),
}
}
}
fn merge_status_with_error(status: &str, error: Option<String>) -> Health {
if status.to_lowercase() == "ok" && error.is_none() {
Health::Ok
} else {
Health::NotOk(format!(
"{status}: {error}",
error = error.unwrap_or_default()
))
}
}
impl ServerStatus {
#[must_use]
pub fn overall(&self) -> Health {
let components = [
("database.status", &self.database.status),
("redis", &self.tasks.redis.status),
("celery", &self.tasks.celery.status),
("index", &self.tasks.index.status),
("sanity_check", &self.tasks.sanity_check.status),
("classifier", &self.tasks.classifier.status),
];
let errors: Vec<_> = components
.iter()
.filter_map(|(name, health)| match health {
Health::NotOk(err) => Some(format!("{name}: {err}")),
Health::Ok => None,
})
.collect();
if errors.is_empty() {
Health::Ok
} else {
Health::NotOk(errors.join(", "))
}
}
}