dashboard: Group messages by service

This commit is contained in:
2024-11-03 11:53:45 +01:00
parent af604426c8
commit 6d371970f1
3 changed files with 93 additions and 24 deletions

View File

@ -4,11 +4,11 @@ use chrono::{DateTime, Local};
use eyre::{eyre, ContextCompat}; use eyre::{eyre, ContextCompat};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub enum Severity { pub enum Severity {
Fatal,
Error,
Warning, Warning,
Error,
Fatal,
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]

View File

@ -1,6 +1,9 @@
use std::{cmp::max, collections::BTreeMap};
use rocket::{http::Status, response::content::RawHtml, State}; use rocket::{http::Status, response::content::RawHtml, State};
use rocket_dyn_templates::{context, Template}; use rocket_dyn_templates::{context, Template};
use serde::Serialize; use serde::Serialize;
use snitch_lib::Severity;
use crate::database::Database; use crate::database::Database;
@ -9,25 +12,33 @@ pub async fn index(db: &State<Database>) -> Result<RawHtml<Template>, Status> {
// TODO: // TODO:
// maybe only show most recent error for each service in dashboard view, i.e.: // maybe only show most recent error for each service in dashboard view, i.e.:
// select distinct on (service) time, message from (select * from record order by time desc) as foo; // select distinct on (service) time, message from (select * from record order by time desc) as foo;
let messages = db.get_all_log_messages().map_err(|e| { let records = db.get_all_log_messages().map_err(|e| {
log::error!("failed to query database: {e}"); log::error!("failed to query database: {e}");
Status::InternalServerError Status::InternalServerError
})?; })?;
#[derive(Serialize)]
struct ServiceRecords<'a> {
first_record: &'a Record,
last_record: &'a Record,
severity: Severity,
all_records: Vec<&'a Record>,
}
#[derive(Serialize)] #[derive(Serialize)]
struct Record { struct Record {
time: String, time: String,
severity: String, severity: Severity,
service: String, service: String,
message: String, message: String,
location: String, location: String,
} }
let messages: Vec<_> = messages let records: Vec<_> = records
.into_iter() .into_iter()
.map(|(_id, record)| Record { .map(|(_id, record)| Record {
time: record.time.map(|time| time.to_string()).unwrap_or_default(), time: record.time.map(|time| time.to_string()).unwrap_or_default(),
severity: record.severity.as_str().to_string(), severity: record.severity,
service: record.service, service: record.service,
message: record.message, message: record.message,
location: record location: record
@ -38,10 +49,28 @@ pub async fn index(db: &State<Database>) -> Result<RawHtml<Template>, Status> {
}) })
.collect(); .collect();
let mut records_by_service = BTreeMap::new();
for record in &records {
let entry = records_by_service
.entry(record.service.as_str())
.or_insert_with(|| ServiceRecords {
first_record: record,
last_record: record,
all_records: vec![],
severity: record.severity,
});
entry.last_record = record;
entry.all_records.push(record);
entry.severity = max(record.severity, entry.severity);
}
Ok(RawHtml(Template::render( Ok(RawHtml(Template::render(
"index", "index",
context! { context! {
messages: &messages records: &records,
records_by_service: &records_by_service,
}, },
))) )))
} }

View File

@ -26,26 +26,66 @@
padding: 0.5em; padding: 0.5em;
font-family: 'Ubuntu Mono', mono; font-family: 'Ubuntu Mono', mono;
} }
.service-error-list {
max-height: 20em;
font-size: small;
overflow: scroll;
transition: max-height ease-in-out 0.4s;
}
.collapsed {
max-height: 0;
}
</style> </style>
<script>
function collapse(id) {
document.getElementById(id).classList.toggle("collapsed")
}
</script>
</head> </head>
<body> <body>
{{#if records}}
<table> <table>
<tr> <tr>
<th>Service</th> <th>Severity</th>
<th>Severity</th> <th>Service</th>
<th>Time</th> <th>Count</th>
<th>Message</th> <th>Last message at</th>
<th>Location</th> </tr>
</tr> {{#each records_by_service}}
{{#each messages}} <tr onclick="collapse('{{@key}}-error-list')">
<tr> <td>{{this.severity}}</td>
<td>{{this.service}}</td> <td>{{@key}}</td>
<td>{{this.severity}}</td> <td>{{len this.all_records}}</td>
<td>{{this.time}}</td> <td>{{this.last_record.time}}</td>
<td>{{this.message}}</td> </tr>
<td>{{this.location}}</td> <tr><td colspan="4">
</tr> <div
{{/each}} id="{{@key}}-error-list"
class="service-error-list collapsed"
>
<table>
<tr>
<th>Severity</th>
<th>Message</th>
<th>Time</th>
</tr>
{{#each this.all_records}}
<tr>
<td>{{this.severity}}</td>
<td>{{this.message}}</td>
<td>{{this.time}}</td>
</tr>
{{/each}}
</table>
</div></td></tr>
{{/each}}
</table> </table>
{{else}}
<p>
No entries! All is well.
</p>
{{/if}}
</body> </body>
</html> </html>