From dc05b2aa2a7309b7224823c6a4a35a6447d4923b Mon Sep 17 00:00:00 2001 From: mrferris Date: Fri, 20 Oct 2023 20:18:58 -0400 Subject: [PATCH] feat: adds filterable stats on audit dashboard --- glados-web/src/routes.rs | 128 ++++++++++++++++++---- glados-web/src/templates.rs | 13 ++- glados-web/templates/audit_dashboard.html | 58 +++++----- glados-web/templates/audit_table.html | 128 ++++++++++++++-------- glados-web/templates/base.html | 14 ++- glados-web/templates/index.html | 51 ++++----- 6 files changed, 262 insertions(+), 130 deletions(-) diff --git a/glados-web/src/routes.rs b/glados-web/src/routes.rs index 9ebf5053..26b3541b 100644 --- a/glados-web/src/routes.rs +++ b/glados-web/src/routes.rs @@ -5,7 +5,9 @@ use axum::{ Json, }; use chrono::{DateTime, Duration, Utc}; -use entity::{census, census_node, client_info, content_audit::SelectionStrategy}; +use entity::{ + census, census_node, client_info, content::SubProtocol, content_audit::SelectionStrategy, +}; use entity::{ content, content_audit::{self, AuditResult}, @@ -18,7 +20,7 @@ use ethportal_api::{HistoryContentKey, OverlayContentKey}; use migration::{Alias, IntoCondition, JoinType, Order}; use sea_orm::{ sea_query::{Expr, Query, SeaRc}, - RelationTrait, + RelationTrait, Select, }; use sea_orm::{ ColumnTrait, ConnectionTrait, DatabaseConnection, DbBackend, DynIden, EntityTrait, @@ -801,10 +803,12 @@ pub enum ContentTypeFilter { Receipts, } +/// Takes an AuditFilter object generated from http query params +/// Conditionally creates a query based on the filters pub async fn contentaudit_filter( Extension(state): Extension>, filters: HttpQuery, -) -> impl IntoResponse { +) -> Result, StatusCode> { let audits = content_audit::Entity::find(); let audits = match filters.strategy { @@ -835,41 +839,70 @@ pub async fn contentaudit_filter( JoinType::InnerJoin, content_audit::Relation::Content .def() - .on_condition(|_left, _right| { - Expr::cust("get_byte(content.content_key, 0) = 0x00").into_condition() + .on_condition(|_left, right| { + Expr::cust("get_byte(content.content_key, 0) = 0x00") + .and( + Expr::col((right, content::Column::ProtocolId)) + .eq(SubProtocol::History), + ) + .into_condition() }), ), - ContentTypeFilter::Bodies => audits.join( JoinType::InnerJoin, content_audit::Relation::Content .def() - .on_condition(|_left, _right| { - Expr::cust("get_byte(content.content_key, 0) = 0x01").into_condition() + .on_condition(|_left, right| { + Expr::cust("get_byte(content.content_key, 0) = 0x01") + .and( + Expr::col((right, content::Column::ProtocolId)) + .eq(SubProtocol::History), + ) + .into_condition() }), ), ContentTypeFilter::Receipts => audits.join( JoinType::InnerJoin, content_audit::Relation::Content .def() - .on_condition(|_left, _right| { - Expr::cust("get_byte(content.content_key, 0) = 0x02").into_condition() + .on_condition(|_left, right| { + Expr::cust("get_byte(content.content_key, 0) = 0x02") + .and( + Expr::col((right, content::Column::ProtocolId)) + .eq(SubProtocol::History), + ) + .into_condition() }), ), }; - let audits = audits - .order_by_desc(content_audit::Column::CreatedAt) - .limit(100) - .all(&state.database_connection) - .await - .unwrap(); - let audits = get_audit_tuples_from_audit_models(audits, &state.database_connection) - .await - .unwrap(); + let (hour_stats, day_stats, week_stats, filtered_audits) = tokio::join!( + get_filtered_audit_stats(audits.clone(), Period::Hour, &state.database_connection), + get_filtered_audit_stats(audits.clone(), Period::Day, &state.database_connection), + get_filtered_audit_stats(audits.clone(), Period::Week, &state.database_connection), + audits + .order_by_desc(content_audit::Column::CreatedAt) + .limit(30) + .all(&state.database_connection), + ); - let template = AuditTableTemplate { audits }; - HtmlTemplate(template) + let filtered_audits = filtered_audits.map_err(|e| { + error!(err=?e, "Could not look up audit stats"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + let hour_stats = hour_stats?; + let day_stats = day_stats?; + let week_stats = week_stats?; + + let filtered_audits: Vec = + get_audit_tuples_from_audit_models(filtered_audits, &state.database_connection).await?; + + let template = AuditTableTemplate { + stats: [hour_stats, day_stats, week_stats], + audits: filtered_audits, + }; + + Ok(HtmlTemplate(template)) } pub enum Period { @@ -991,6 +1024,59 @@ pub struct Stats { pub audits_per_minute: u32, } +async fn get_filtered_audit_stats( + filtered: Select, + period: Period, + conn: &DatabaseConnection, +) -> Result { + let cutoff = period.cutoff_time(); + + let total_audits = filtered + .clone() + .filter(content_audit::Column::CreatedAt.gt(cutoff)) + .count(conn) + .await + .map_err(|e| { + error!(err=?e, "Could not look up audit stats"); + StatusCode::INTERNAL_SERVER_ERROR + })? as u32; + + let total_passes = filtered + .filter(content_audit::Column::CreatedAt.gt(cutoff)) + .filter(content_audit::Column::Result.eq(AuditResult::Success)) + .count(conn) + .await + .map_err(|e| { + error!(err=?e, "Could not look up audit stats"); + StatusCode::INTERNAL_SERVER_ERROR + })? as u32; + + let total_failures = total_audits - total_passes; + + let audits_per_minute = 0; + + let (pass_percent, fail_percent) = if total_audits == 0 { + (0.0, 0.0) + } else { + let total_audits = total_audits as f32; + ( + (total_passes as f32) * 100.0 / total_audits, + (total_failures as f32) * 100.0 / total_audits, + ) + }; + + Ok(Stats { + period, + new_content: 0, + total_audits, + total_passes, + pass_percent, + total_failures, + fail_percent, + audits_per_minute, + }) +} + async fn get_audit_stats(period: Period, conn: &DatabaseConnection) -> Result { let cutoff = period.cutoff_time(); let new_content = content::Entity::find() diff --git a/glados-web/src/templates.rs b/glados-web/src/templates.rs index 75704e48..23b0c007 100644 --- a/glados-web/src/templates.rs +++ b/glados-web/src/templates.rs @@ -56,12 +56,6 @@ pub struct ContentDashboardTemplate { pub recent_audit_failures: Vec, } -#[derive(Template)] -#[template(path = "audit_table.html")] -pub struct AuditTableTemplate { - pub audits: Vec, -} - #[derive(Template)] #[template(path = "contentid_list.html")] pub struct ContentIdListTemplate { @@ -93,6 +87,13 @@ pub struct ContentKeyListTemplate { #[template(path = "audit_dashboard.html")] pub struct AuditDashboardTemplate {} +#[derive(Template)] +#[template(path = "audit_table.html")] +pub struct AuditTableTemplate { + pub stats: [Stats; 3], + pub audits: Vec, +} + #[derive(Template)] #[template(path = "contentkey_detail.html")] pub struct ContentKeyDetailTemplate { diff --git a/glados-web/templates/audit_dashboard.html b/glados-web/templates/audit_dashboard.html index e4937f7e..291e7601 100644 --- a/glados-web/templates/audit_dashboard.html +++ b/glados-web/templates/audit_dashboard.html @@ -9,34 +9,36 @@

    -

    Audit Dashboard

    -
    - - - - -
    -
    - - - - -
    -
    - - - +

    Audit Dashboard

    +
    +
    + + + + +
    +
    + + + + +
    +
    + + + +
diff --git a/glados-web/templates/audit_table.html b/glados-web/templates/audit_table.html index 61408c64..d39759d3 100644 --- a/glados-web/templates/audit_table.html +++ b/glados-web/templates/audit_table.html @@ -1,50 +1,84 @@ -
-
-
    -
    -
    - - - - - - - - - - - - - - - - {% for (audit, content, client_info) in audits %} - - - - - - - - - - - - {% endfor %} - -
    AuditResultSub-protocolStrategyContent KeyContent IDContent first availableAudited atClient
    {% if audit.trace != "" %}{{ audit.id }}{% - else - %} - {{ audit.id }}{% endif %} - {% - if audit.is_success() %}Success{% else %}Fail{% endif %}{{ content.protocol_id.as_text() }}{{ audit.strategy_as_text() }}{{ content.key_as_hex_short() - }} - {{ content.id_as_hex_short() }} - {{ content.available_at_humanized() - }}{{ audit.created_at_humanized() }}{{ client_info.version_info }}
    +
    +
    + + + + + + + + + + + + + + {% for stat in stats %} + + + + + + + + + + {% endfor %} + +
    PeriodTotal auditsTotal audit passesTotal audit failuresPass rate (%)Failure rate (%)Audits per minute
    {{ stat.period.to_string() }}{{ stat.total_audits }}{{ stat.total_passes }}{{ stat.total_failures }}{{ "{:.1}"|format(stat.pass_percent) }}%{{ "{:.1}"|format(stat.fail_percent) }}%{{ stat.audits_per_minute }}
    +
    +
    +
    +
      +
      +
      + + + + + + + + + + + + + + + + {% for (audit, content, client_info) in audits %} + + + + + + + + + + + + {% endfor %} + +
      AuditResultSub-protocolStrategyContent KeyContent IDContent first availableAudited atClient
      {% if audit.trace != "" %}{{ audit.id + }}{% + else + %} + {{ audit.id }}{% endif %} + {% + if audit.is_success() %}Success{% else %}Fail{% endif %}{{ content.protocol_id.as_text() }}{{ audit.strategy_as_text() }}{{ content.key_as_hex_short() + }} + {{ content.id_as_hex_short() + }} + {{ + content.available_at_humanized() + }}{{ audit.created_at_humanized() }} + {{ client_info.version_info }}
      +
      -
    -
+ +
diff --git a/glados-web/templates/base.html b/glados-web/templates/base.html index f50840f0..460b71fe 100644 --- a/glados-web/templates/base.html +++ b/glados-web/templates/base.html @@ -18,17 +18,25 @@