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)]
75pub struct Storage {
76 pub total: u64,
78
79 pub available: u64,
81}
82
83#[derive(Debug, Clone, Deserialize)]
85#[serde(from = "RawDatabase")]
86pub struct Database {
87 pub db_type: String,
89
90 pub url: String,
92
93 pub status: Health,
95
96 pub migration_status: MigrationStatus,
98}
99
100#[derive(Debug, Clone, Deserialize)]
102pub struct MigrationStatus {
103 pub latest_migration: Option<String>,
105
106 pub unapplied_migrations: Vec<String>,
108}
109
110#[derive(Debug, Clone, Deserialize)]
112pub enum Health {
113 #[serde(rename = "OK")]
115 Ok,
116
117 #[serde(untagged)]
119 NotOk(String),
120}
121
122#[derive(Debug, Clone, Deserialize)]
124pub struct StatusTask {
125 #[serde(flatten)]
126 pub redis: RedisStatus,
127
128 #[serde(flatten)]
129 pub celery: CeleryStatus,
130
131 #[serde(flatten)]
132 pub index: IndexStatus,
133
134 #[serde(flatten)]
135 pub sanity_check: SanityCheckStatus,
136
137 #[serde(flatten)]
138 pub classifier: ClassifierStatus,
139}
140
141#[derive(Debug, Clone, Deserialize)]
143#[serde(from = "RawRedisStatus")]
144pub struct RedisStatus {
145 pub url: String,
147
148 pub status: Health,
150}
151
152#[derive(Debug, Clone, Deserialize)]
154#[serde(from = "RawCeleryStatus")]
155pub struct CeleryStatus {
156 pub status: Health,
158
159 pub url: String,
161}
162
163#[derive(Debug, Clone, Deserialize)]
165#[serde(from = "RawIndexStatus")]
166pub struct IndexStatus {
167 pub status: Health,
169
170 pub last_modified: Option<chrono::DateTime<chrono::Utc>>,
172}
173
174#[derive(Debug, Clone, Deserialize)]
176#[serde(from = "RawClassifierStatus")]
177pub struct ClassifierStatus {
178 pub status: Health,
180
181 pub last_trained: Option<chrono::DateTime<chrono::Utc>>,
183}
184
185#[derive(Debug, Clone, Deserialize)]
187#[serde(from = "RawSanityCheckStatus")]
188pub struct SanityCheckStatus {
189 pub status: Health,
191
192 pub last_run: Option<chrono::DateTime<chrono::Utc>>,
194}
195
196#[derive(Deserialize)]
197pub struct RawDatabase {
198 #[serde(rename = "type")]
199 pub db_type: String,
200 pub url: String,
201 pub status: String,
202 pub error: Option<String>,
203 pub migration_status: MigrationStatus,
204}
205
206#[derive(Deserialize)]
207#[allow(clippy::struct_field_names)]
208struct RawRedisStatus {
209 redis_url: String,
210 redis_status: String,
211 redis_error: Option<String>,
212}
213
214#[derive(Deserialize)]
215#[allow(clippy::struct_field_names)]
216struct RawCeleryStatus {
217 celery_status: String,
218 celery_url: String,
219 celery_error: Option<String>,
220}
221
222#[derive(Deserialize)]
223#[allow(clippy::struct_field_names)]
224struct RawIndexStatus {
225 index_status: String,
226 index_last_modified: Option<chrono::DateTime<chrono::Utc>>,
227 index_error: Option<String>,
228}
229
230#[derive(Deserialize)]
231#[allow(clippy::struct_field_names)]
232struct RawClassifierStatus {
233 classifier_status: String,
234 classifier_last_trained: Option<chrono::DateTime<chrono::Utc>>,
235 classifier_error: Option<String>,
236}
237
238#[derive(Deserialize)]
239#[allow(clippy::struct_field_names)]
240struct RawSanityCheckStatus {
241 sanity_check_status: String,
242 sanity_check_last_run: Option<chrono::DateTime<chrono::Utc>>,
243 sanity_check_error: Option<String>,
244}
245
246impl From<RawDatabase> for Database {
247 fn from(raw: RawDatabase) -> Self {
248 Self {
249 db_type: raw.db_type,
250 url: raw.url,
251 status: merge_status_with_error(&raw.status, raw.error),
252 migration_status: raw.migration_status,
253 }
254 }
255}
256
257impl From<RawRedisStatus> for RedisStatus {
258 fn from(raw: RawRedisStatus) -> Self {
259 Self {
260 url: raw.redis_url,
261 status: merge_status_with_error(&raw.redis_status, raw.redis_error),
262 }
263 }
264}
265
266impl From<RawCeleryStatus> for CeleryStatus {
267 fn from(raw: RawCeleryStatus) -> Self {
268 Self {
269 status: merge_status_with_error(&raw.celery_status, raw.celery_error),
270 url: raw.celery_url,
271 }
272 }
273}
274
275impl From<RawIndexStatus> for IndexStatus {
276 fn from(raw: RawIndexStatus) -> Self {
277 Self {
278 status: merge_status_with_error(&raw.index_status, raw.index_error),
279 last_modified: raw.index_last_modified,
280 }
281 }
282}
283
284impl From<RawClassifierStatus> for ClassifierStatus {
285 fn from(raw: RawClassifierStatus) -> Self {
286 Self {
287 status: merge_status_with_error(&raw.classifier_status, raw.classifier_error),
288 last_trained: raw.classifier_last_trained,
289 }
290 }
291}
292
293impl From<RawSanityCheckStatus> for SanityCheckStatus {
294 fn from(raw: RawSanityCheckStatus) -> Self {
295 Self {
296 status: merge_status_with_error(&raw.sanity_check_status, raw.sanity_check_error),
297 last_run: raw.sanity_check_last_run,
298 }
299 }
300}
301
302impl std::fmt::Display for Health {
303 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
304 match self {
305 Health::Ok => write!(f, "OK"),
306 Health::NotOk(err) => write!(f, "Error: {err}"),
307 }
308 }
309}
310
311fn merge_status_with_error(status: &str, error: Option<String>) -> Health {
312 if status.to_lowercase() == "ok" && error.is_none() {
313 Health::Ok
314 } else {
315 Health::NotOk(format!(
316 "{status}: {error}",
317 error = error.unwrap_or_default()
318 ))
319 }
320}
321
322impl ServerStatus {
323 #[must_use]
326 pub fn overall(&self) -> Health {
327 let components = [
328 ("database.status", &self.database.status),
329 ("redis", &self.tasks.redis.status),
330 ("celery", &self.tasks.celery.status),
331 ("index", &self.tasks.index.status),
332 ("sanity_check", &self.tasks.sanity_check.status),
333 ("classifier", &self.tasks.classifier.status),
334 ];
335
336 let errors: Vec<_> = components
337 .iter()
338 .filter_map(|(name, health)| match health {
339 Health::NotOk(err) => Some(format!("{name}: {err}")),
340 Health::Ok => None,
341 })
342 .collect();
343
344 if errors.is_empty() {
345 Health::Ok
346 } else {
347 Health::NotOk(errors.join(", "))
348 }
349 }
350}