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 Error(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
118#[derive(Debug, Clone, Deserialize)]
119#[serde(from = "RawRedisStatus")]
120pub struct RedisStatus {
121 pub url: String,
122 pub status: Health,
123}
124
125#[derive(Debug, Clone, Deserialize)]
126#[serde(from = "RawCeleryStatus")]
127pub struct CeleryStatus {
128 pub status: Health,
129 pub url: String,
130}
131
132#[derive(Debug, Clone, Deserialize)]
133#[serde(from = "RawIndexStatus")]
134pub struct IndexStatus {
135 pub status: Health,
136 pub last_modified: Option<chrono::DateTime<chrono::Utc>>,
137}
138
139#[derive(Debug, Clone, Deserialize)]
140#[serde(from = "RawClassifierStatus")]
141pub struct ClassifierStatus {
142 pub status: Health,
143 pub last_trained: Option<chrono::DateTime<chrono::Utc>>,
144}
145
146#[derive(Debug, Clone, Deserialize)]
147#[serde(from = "RawSanityCheckStatus")]
148pub struct SanityCheckStatus {
149 pub status: Health,
150 pub last_run: Option<chrono::DateTime<chrono::Utc>>,
151}
152
153#[derive(Deserialize)]
154pub struct RawDatabase {
155 #[serde(rename = "type")]
156 pub db_type: String,
157 pub url: String,
158 pub status: String,
159 pub error: Option<String>,
160 pub migration_status: MigrationStatus,
161}
162
163#[derive(Deserialize)]
164#[allow(clippy::struct_field_names)]
165struct RawRedisStatus {
166 redis_url: String,
167 redis_status: String,
168 redis_error: Option<String>,
169}
170
171#[derive(Deserialize)]
172#[allow(clippy::struct_field_names)]
173struct RawCeleryStatus {
174 celery_status: String,
175 celery_url: String,
176 celery_error: Option<String>,
177}
178
179#[derive(Deserialize)]
180#[allow(clippy::struct_field_names)]
181struct RawIndexStatus {
182 index_status: String,
183 index_last_modified: Option<chrono::DateTime<chrono::Utc>>,
184 index_error: Option<String>,
185}
186
187#[derive(Deserialize)]
188#[allow(clippy::struct_field_names)]
189struct RawClassifierStatus {
190 classifier_status: String,
191 classifier_last_trained: Option<chrono::DateTime<chrono::Utc>>,
192 classifier_error: Option<String>,
193}
194
195#[derive(Deserialize)]
196#[allow(clippy::struct_field_names)]
197struct RawSanityCheckStatus {
198 sanity_check_status: String,
199 sanity_check_last_run: Option<chrono::DateTime<chrono::Utc>>,
200 sanity_check_error: Option<String>,
201}
202
203impl From<RawDatabase> for Database {
204 fn from(raw: RawDatabase) -> Self {
205 Self {
206 db_type: raw.db_type,
207 url: raw.url,
208 status: merge_status_with_error(raw.status, raw.error),
209 migration_status: raw.migration_status,
210 }
211 }
212}
213
214impl From<RawRedisStatus> for RedisStatus {
215 fn from(raw: RawRedisStatus) -> Self {
216 Self {
217 url: raw.redis_url,
218 status: merge_status_with_error(raw.redis_status, raw.redis_error),
219 }
220 }
221}
222
223impl From<RawCeleryStatus> for CeleryStatus {
224 fn from(raw: RawCeleryStatus) -> Self {
225 Self {
226 status: merge_status_with_error(raw.celery_status, raw.celery_error),
227 url: raw.celery_url,
228 }
229 }
230}
231
232impl From<RawIndexStatus> for IndexStatus {
233 fn from(raw: RawIndexStatus) -> Self {
234 Self {
235 status: merge_status_with_error(raw.index_status, raw.index_error),
236 last_modified: raw.index_last_modified,
237 }
238 }
239}
240
241impl From<RawClassifierStatus> for ClassifierStatus {
242 fn from(raw: RawClassifierStatus) -> Self {
243 Self {
244 status: merge_status_with_error(raw.classifier_status, raw.classifier_error),
245 last_trained: raw.classifier_last_trained,
246 }
247 }
248}
249
250impl From<RawSanityCheckStatus> for SanityCheckStatus {
251 fn from(raw: RawSanityCheckStatus) -> Self {
252 Self {
253 status: merge_status_with_error(raw.sanity_check_status, raw.sanity_check_error),
254 last_run: raw.sanity_check_last_run,
255 }
256 }
257}
258
259impl std::fmt::Display for Health {
260 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261 match self {
262 Health::Ok => write!(f, "OK"),
263 Health::Error(err) => write!(f, "Error: {err}"),
264 }
265 }
266}
267
268fn merge_status_with_error(status: String, error: Option<String>) -> Health {
269 if let Some(error) = error {
270 Health::Error(error)
271 } else if status.to_lowercase() != "ok" {
272 Health::Error(status)
273 } else {
274 Health::Ok
275 }
276}
277
278impl ServerStatus {
279 #[must_use]
282 pub fn overall(&self) -> Health {
283 let mut errors = Vec::new();
284
285 if let Health::Error(ref err) = self.database.status {
286 errors.push(format!("database: {err}"));
287 }
288 if let Health::Error(ref err) = self.tasks.redis.status {
289 errors.push(format!("task redis: {err}"));
290 }
291 if let Health::Error(ref err) = self.tasks.celery.status {
292 errors.push(format!("task celery: {err}"));
293 }
294 if let Health::Error(ref err) = self.tasks.index.status {
295 errors.push(format!("task index: {err}"));
296 }
297 if let Health::Error(ref err) = self.tasks.sanity_check.status {
298 errors.push(format!("task sanity_check: {err}"));
299 }
300
301 if errors.is_empty() {
302 Health::Ok
303 } else {
304 Health::Error(errors.join(", "))
305 }
306 }
307}