1use serde::Deserialize;
4
5#[derive(Debug, Clone, Deserialize)]
6pub struct Statistics {
7 pub documents_total: u32,
9
10 pub documents_inbox: u32,
12
13 pub inbox_tag: u32,
15
16 pub inbox_tags: Vec<u32>,
18
19 pub document_file_type_counts: Vec<DocumentFileTypeCount>,
21
22 pub character_count: u64,
24
25 pub tag_count: u32,
27
28 pub correspondent_count: u32,
30
31 pub document_type_count: u32,
33
34 pub storage_path_count: u32,
36
37 pub current_asn: u32,
39}
40
41#[derive(Debug, Clone, Deserialize)]
42pub struct DocumentFileTypeCount {
43 pub mime_type: String,
45
46 #[serde(rename = "mime_type_count")]
48 pub count: u32,
49}
50
51#[derive(Debug, Clone, Deserialize)]
52pub struct ServerStatus {
53 #[serde(rename = "pngx_version")]
55 pub version: String,
56
57 pub server_os: String,
59
60 pub install_type: String,
62
63 pub storage: Storage,
65
66 pub database: Database,
68
69 pub tasks: StatusTask,
71}
72
73#[derive(Debug, Clone, Deserialize)]
74pub struct Storage {
75 pub total: u64,
76 pub available: u64,
77}
78
79#[derive(Debug, Clone, Deserialize)]
80#[serde(from = "RawDatabase")]
81pub struct Database {
82 pub db_type: String,
83 pub url: String,
84 pub status: Health,
85 pub migration_status: MigrationStatus,
86}
87
88#[derive(Debug, Clone, Deserialize)]
89pub struct MigrationStatus {
90 pub latest_migration: String,
91 pub unapplied_migrations: Vec<String>,
92}
93
94#[derive(Debug, Clone, Deserialize)]
95pub enum Health {
96 #[serde(rename = "OK")]
97 Ok,
98
99 #[serde(untagged)]
100 NotOk(String),
101}
102
103#[derive(Debug, Clone, Deserialize)]
104pub struct StatusTask {
105 #[serde(flatten)]
106 pub redis: RedisStatus,
107
108 #[serde(flatten)]
109 pub celery: CeleryStatus,
110
111 #[serde(flatten)]
112 pub index: IndexStatus,
113
114 #[serde(flatten)]
115 pub sanity_check: SanityCheckStatus,
116
117 #[serde(flatten)]
118 pub classifier: ClassifierStatus,
119}
120
121#[derive(Debug, Clone, Deserialize)]
122#[serde(from = "RawRedisStatus")]
123pub struct RedisStatus {
124 pub url: String,
125 pub status: Health,
126}
127
128#[derive(Debug, Clone, Deserialize)]
129#[serde(from = "RawCeleryStatus")]
130pub struct CeleryStatus {
131 pub status: Health,
132 pub url: String,
133}
134
135#[derive(Debug, Clone, Deserialize)]
136#[serde(from = "RawIndexStatus")]
137pub struct IndexStatus {
138 pub status: Health,
139 pub last_modified: Option<chrono::DateTime<chrono::Utc>>,
140}
141
142#[derive(Debug, Clone, Deserialize)]
143#[serde(from = "RawClassifierStatus")]
144pub struct ClassifierStatus {
145 pub status: Health,
146 pub last_trained: Option<chrono::DateTime<chrono::Utc>>,
147}
148
149#[derive(Debug, Clone, Deserialize)]
150#[serde(from = "RawSanityCheckStatus")]
151pub struct SanityCheckStatus {
152 pub status: Health,
153 pub last_run: Option<chrono::DateTime<chrono::Utc>>,
154}
155
156#[derive(Deserialize)]
157pub struct RawDatabase {
158 #[serde(rename = "type")]
159 pub db_type: String,
160 pub url: String,
161 pub status: String,
162 pub error: Option<String>,
163 pub migration_status: MigrationStatus,
164}
165
166#[derive(Deserialize)]
167#[allow(clippy::struct_field_names)]
168struct RawRedisStatus {
169 redis_url: String,
170 redis_status: String,
171 redis_error: Option<String>,
172}
173
174#[derive(Deserialize)]
175#[allow(clippy::struct_field_names)]
176struct RawCeleryStatus {
177 celery_status: String,
178 celery_url: String,
179 celery_error: Option<String>,
180}
181
182#[derive(Deserialize)]
183#[allow(clippy::struct_field_names)]
184struct RawIndexStatus {
185 index_status: String,
186 index_last_modified: Option<chrono::DateTime<chrono::Utc>>,
187 index_error: Option<String>,
188}
189
190#[derive(Deserialize)]
191#[allow(clippy::struct_field_names)]
192struct RawClassifierStatus {
193 classifier_status: String,
194 classifier_last_trained: Option<chrono::DateTime<chrono::Utc>>,
195 classifier_error: Option<String>,
196}
197
198#[derive(Deserialize)]
199#[allow(clippy::struct_field_names)]
200struct RawSanityCheckStatus {
201 sanity_check_status: String,
202 sanity_check_last_run: Option<chrono::DateTime<chrono::Utc>>,
203 sanity_check_error: Option<String>,
204}
205
206impl From<RawDatabase> for Database {
207 fn from(raw: RawDatabase) -> Self {
208 Self {
209 db_type: raw.db_type,
210 url: raw.url,
211 status: merge_status_with_error(&raw.status, raw.error),
212 migration_status: raw.migration_status,
213 }
214 }
215}
216
217impl From<RawRedisStatus> for RedisStatus {
218 fn from(raw: RawRedisStatus) -> Self {
219 Self {
220 url: raw.redis_url,
221 status: merge_status_with_error(&raw.redis_status, raw.redis_error),
222 }
223 }
224}
225
226impl From<RawCeleryStatus> for CeleryStatus {
227 fn from(raw: RawCeleryStatus) -> Self {
228 Self {
229 status: merge_status_with_error(&raw.celery_status, raw.celery_error),
230 url: raw.celery_url,
231 }
232 }
233}
234
235impl From<RawIndexStatus> for IndexStatus {
236 fn from(raw: RawIndexStatus) -> Self {
237 Self {
238 status: merge_status_with_error(&raw.index_status, raw.index_error),
239 last_modified: raw.index_last_modified,
240 }
241 }
242}
243
244impl From<RawClassifierStatus> for ClassifierStatus {
245 fn from(raw: RawClassifierStatus) -> Self {
246 Self {
247 status: merge_status_with_error(&raw.classifier_status, raw.classifier_error),
248 last_trained: raw.classifier_last_trained,
249 }
250 }
251}
252
253impl From<RawSanityCheckStatus> for SanityCheckStatus {
254 fn from(raw: RawSanityCheckStatus) -> Self {
255 Self {
256 status: merge_status_with_error(&raw.sanity_check_status, raw.sanity_check_error),
257 last_run: raw.sanity_check_last_run,
258 }
259 }
260}
261
262impl std::fmt::Display for Health {
263 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264 match self {
265 Health::Ok => write!(f, "OK"),
266 Health::NotOk(err) => write!(f, "Error: {err}"),
267 }
268 }
269}
270
271fn merge_status_with_error(status: &str, error: Option<String>) -> Health {
272 if status.to_lowercase() == "ok" && error.is_none() {
273 Health::Ok
274 } else {
275 Health::NotOk(format!(
276 "{status}: {error}",
277 error = error.unwrap_or_default()
278 ))
279 }
280}
281
282impl ServerStatus {
283 #[must_use]
286 pub fn overall(&self) -> Health {
287 let components = [
288 ("database.status", &self.database.status),
289 ("redis", &self.tasks.redis.status),
290 ("celery", &self.tasks.celery.status),
291 ("index", &self.tasks.index.status),
292 ("sanity_check", &self.tasks.sanity_check.status),
293 ("classifier", &self.tasks.classifier.status),
294 ];
295
296 let errors: Vec<_> = components
297 .iter()
298 .filter_map(|(name, health)| match health {
299 Health::NotOk(err) => Some(format!("{name}: {err}")),
300 Health::Ok => None,
301 })
302 .collect();
303
304 if errors.is_empty() {
305 Health::Ok
306 } else {
307 Health::NotOk(errors.join(", "))
308 }
309 }
310}