From cdcc84a5ca2f6bdc640f0721e2706b12b1556784 Mon Sep 17 00:00:00 2001 From: Michael Ferris Date: Thu, 10 Apr 2025 11:30:26 -0400 Subject: [PATCH 1/3] feat: adds sync audit tables --- entity/src/lib.rs | 1 + entity/src/sync_audit/mod.rs | 3 + entity/src/sync_audit/sync_audit.rs | 62 ++++++ entity/src/sync_audit/sync_audit_error.rs | 52 +++++ migration/src/lib.rs | 2 + .../src/m20250317_193433_create_sync_audit.rs | 182 ++++++++++++++++++ 6 files changed, 302 insertions(+) create mode 100644 entity/src/sync_audit/mod.rs create mode 100644 entity/src/sync_audit/sync_audit.rs create mode 100644 entity/src/sync_audit/sync_audit_error.rs create mode 100644 migration/src/m20250317_193433_create_sync_audit.rs diff --git a/entity/src/lib.rs b/entity/src/lib.rs index 2616a461..098feb28 100644 --- a/entity/src/lib.rs +++ b/entity/src/lib.rs @@ -16,5 +16,6 @@ pub mod key_value; pub mod node; pub mod record; pub mod state_roots; +pub mod sync_audit; pub mod test; pub mod utils; diff --git a/entity/src/sync_audit/mod.rs b/entity/src/sync_audit/mod.rs new file mode 100644 index 00000000..554f7abc --- /dev/null +++ b/entity/src/sync_audit/mod.rs @@ -0,0 +1,3 @@ +pub mod sync_audit; +pub mod sync_audit_error; +pub mod sync_audit_segment; diff --git a/entity/src/sync_audit/sync_audit.rs b/entity/src/sync_audit/sync_audit.rs new file mode 100644 index 00000000..ac7be8b9 --- /dev/null +++ b/entity/src/sync_audit/sync_audit.rs @@ -0,0 +1,62 @@ +//! `SeaORM` Entity for sync_audit +use anyhow::Result; +use chrono::{DateTime, Utc}; +use sea_orm::{entity::prelude::*, ActiveValue::NotSet, Set}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "sync_audit")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub started_at: DateTime, + pub completed_at: Option>, + pub status: SyncAuditStatus, + pub segment_size: i32, +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, EnumIter, DeriveActiveEnum)] +#[sea_orm(rs_type = "i32", db_type = "Integer")] +pub enum SyncAuditStatus { + InProgress = 0, + Completed = 1, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::sync_audit_segment::Entity")] + SyncAuditSegment, +} + +impl ActiveModelBehavior for ActiveModel {} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::SyncAuditSegment.def() + } +} + +pub async fn create(segment_size: i32, conn: &DatabaseConnection) -> Result { + let active = ActiveModel { + id: NotSet, + started_at: Set(Utc::now()), + completed_at: Set(None), + status: Set(SyncAuditStatus::InProgress), + segment_size: Set(segment_size), + }; + Ok(active.insert(conn).await?) +} + +pub async fn get(id: i32, conn: &DatabaseConnection) -> Result> { + Ok(Entity::find_by_id(id).one(conn).await?) +} + +pub async fn mark_complete(id: i32, conn: &DatabaseConnection) -> Result<()> { + let audit = get(id, conn) + .await? + .ok_or_else(|| anyhow::anyhow!("Audit not found"))?; + let mut audit: ActiveModel = audit.into(); + audit.completed_at = Set(Some(Utc::now())); + audit.status = Set(SyncAuditStatus::Completed); + audit.update(conn).await?; + Ok(()) +} diff --git a/entity/src/sync_audit/sync_audit_error.rs b/entity/src/sync_audit/sync_audit_error.rs new file mode 100644 index 00000000..f6c69bb1 --- /dev/null +++ b/entity/src/sync_audit/sync_audit_error.rs @@ -0,0 +1,52 @@ +//! `SeaORM` Entity for sync_audit_error +use anyhow::Result; +use chrono::{DateTime, Utc}; +use sea_orm::{entity::prelude::*, ActiveValue::NotSet, Set}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "sync_audit_error")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub sync_audit_segment_id: i32, + pub block_number: i32, + pub error_type: Option, + pub error_message: Option, + pub created_at: DateTime, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::SyncAuditSegment.def() + } +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::sync_audit_segment::Entity", + from = "Column::SyncAuditSegmentId", + to = "super::sync_audit_segment::Column::Id" + )] + SyncAuditSegment, +} + +impl ActiveModelBehavior for ActiveModel {} + +pub async fn create( + sync_audit_segment_id: i32, + block_number: i32, + error_type: Option, + error_message: Option, + conn: &DatabaseConnection, +) -> Result { + let active = ActiveModel { + id: NotSet, + sync_audit_segment_id: Set(sync_audit_segment_id), + block_number: Set(block_number), + error_type: Set(error_type), + error_message: Set(error_message), + created_at: Set(Utc::now()), + }; + Ok(active.insert(conn).await?) +} diff --git a/migration/src/lib.rs b/migration/src/lib.rs index 128a68b4..59cdff7a 100644 --- a/migration/src/lib.rs +++ b/migration/src/lib.rs @@ -26,6 +26,7 @@ mod m20250311_115816_create_blocks; mod m20250311_121724_create_audit_result_latest; mod m20250314_144135_add_history_headers_by_number_audit_stats; mod m20250317_183352_refactor_history_audit_stats; +mod m20250317_193433_create_sync_audit; mod m20250404_202958_internal_failures_replace_node_with_record; mod m20250404_220628_add_client_info_to_census_node; @@ -59,6 +60,7 @@ impl MigratorTrait for Migrator { Box::new(m20250317_183352_refactor_history_audit_stats::Migration), Box::new(m20250404_202958_internal_failures_replace_node_with_record::Migration), Box::new(m20250404_220628_add_client_info_to_census_node::Migration), + Box::new(m20250317_193433_create_sync_audit::Migration), ] } } diff --git a/migration/src/m20250317_193433_create_sync_audit.rs b/migration/src/m20250317_193433_create_sync_audit.rs new file mode 100644 index 00000000..0d7b4530 --- /dev/null +++ b/migration/src/m20250317_193433_create_sync_audit.rs @@ -0,0 +1,182 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(SyncAudit::Table) + .if_not_exists() + .col( + ColumnDef::new(SyncAudit::Id) + .integer() + .auto_increment() + .not_null() + .primary_key(), + ) + .col( + ColumnDef::new(SyncAudit::StartedAt) + .timestamp_with_time_zone() + .not_null(), + ) + .col(ColumnDef::new(SyncAudit::CompletedAt).timestamp_with_time_zone()) + .col(ColumnDef::new(SyncAudit::SegmentSize).integer().not_null()) + .col(ColumnDef::new(SyncAudit::Status).integer().not_null()) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(SyncAuditSegment::Table) + .if_not_exists() + .col( + ColumnDef::new(SyncAuditSegment::Id) + .integer() + .auto_increment() + .not_null() + .primary_key(), + ) + .col( + ColumnDef::new(SyncAuditSegment::SyncAuditId) + .integer() + .auto_increment() + .not_null(), + ) + .col( + ColumnDef::new(SyncAuditSegment::StartBlock) + .integer() + .not_null(), + ) + .col( + ColumnDef::new(SyncAuditSegment::EndBlock) + .integer() + .not_null(), + ) + .col( + ColumnDef::new(SyncAuditSegment::NumBlocks) + .integer() + .not_null(), + ) + .col(ColumnDef::new(SyncAuditSegment::MinResponseMs).integer()) + .col(ColumnDef::new(SyncAuditSegment::MaxResponseMs).integer()) + .col(ColumnDef::new(SyncAuditSegment::MeanResponseMs).integer()) + .col(ColumnDef::new(SyncAuditSegment::MedianResponseMs).integer()) + .col(ColumnDef::new(SyncAuditSegment::P99ResponseMs).integer()) + .col(ColumnDef::new(SyncAuditSegment::TotalDurationMs).integer()) + .col( + ColumnDef::new(SyncAuditSegment::NumErrors) + .integer() + .not_null() + .default(0), + ) + .col(ColumnDef::new(SyncAuditSegment::Status).integer()) + .foreign_key( + ForeignKey::create() + .name("fk-sync_audit_segment-audit_id") + .from(SyncAuditSegment::Table, SyncAuditSegment::SyncAuditId) + .to(SyncAudit::Table, SyncAudit::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(SyncAuditError::Table) + .if_not_exists() + .col( + ColumnDef::new(SyncAuditError::Id) + .integer() + .auto_increment() + .not_null() + .primary_key(), + ) + .col( + ColumnDef::new(SyncAuditError::SyncAuditSegmentId) + .integer() + .auto_increment() + .not_null(), + ) + .col( + ColumnDef::new(SyncAuditError::BlockNumber) + .integer() + .not_null(), + ) + .col(ColumnDef::new(SyncAuditError::ErrorType).string()) + .col(ColumnDef::new(SyncAuditError::ErrorMessage).text()) + .col( + ColumnDef::new(SyncAuditError::CreatedAt) + .timestamp_with_time_zone() + .not_null(), + ) + .foreign_key( + ForeignKey::create() + .name("fk-sync_audit_error-record_id") + .from(SyncAuditError::Table, SyncAuditError::SyncAuditSegmentId) + .to(SyncAuditSegment::Table, SyncAuditSegment::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(SyncAuditError::Table).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(SyncAuditSegment::Table).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(SyncAudit::Table).to_owned()) + .await + } +} + +#[derive(Iden)] +enum SyncAudit { + Table, + Id, + StartedAt, + CompletedAt, + SegmentSize, + Status, +} + +#[derive(Iden)] +enum SyncAuditSegment { + Table, + Id, + SyncAuditId, + StartBlock, + EndBlock, + NumBlocks, + MinResponseMs, + MaxResponseMs, + MeanResponseMs, + MedianResponseMs, + P99ResponseMs, + TotalDurationMs, + NumErrors, + Status, +} + +#[derive(Iden)] +enum SyncAuditError { + Table, + Id, + SyncAuditSegmentId, + BlockNumber, + ErrorType, + ErrorMessage, + CreatedAt, +} From edf10d676b3a213f7eaa710b50e346754380e194 Mon Sep 17 00:00:00 2001 From: Michael Ferris Date: Mon, 5 May 2025 10:41:43 -0400 Subject: [PATCH 2/3] feat: adds sync flag to glados-audit --- Cargo.lock | 1 + glados-audit/Cargo.toml | 1 + glados-audit/src/cli.rs | 4 +++ glados-audit/src/lib.rs | 9 +++++++ glados-audit/src/validation.rs | 4 +-- glados-core/src/jsonrpc.rs | 49 +++++++++++++++++++--------------- 6 files changed, 44 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d31d0e6..027f9bff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2739,6 +2739,7 @@ dependencies = [ "env_logger 0.10.2", "eth_trie", "ethportal-api", + "futures", "glados-core", "migration", "pgtemp", diff --git a/glados-audit/Cargo.toml b/glados-audit/Cargo.toml index 6cdc4b19..cdcc8c0a 100644 --- a/glados-audit/Cargo.toml +++ b/glados-audit/Cargo.toml @@ -21,6 +21,7 @@ eth_trie = "0.5.0" ethportal-api.workspace = true glados-core.workspace = true migration.workspace = true +futures.workspace = true rand.workspace = true sea-orm.workspace = true serde_json.workspace= true diff --git a/glados-audit/src/cli.rs b/glados-audit/src/cli.rs index 3d10a487..70f8be38 100644 --- a/glados-audit/src/cli.rs +++ b/glados-audit/src/cli.rs @@ -110,6 +110,9 @@ pub struct Args { #[arg(long, action(ArgAction::Append))] pub portal_client: Vec, + #[arg(long, default_value = "false")] + pub sync: bool, + #[command(subcommand)] pub subcommand: Option, } @@ -141,6 +144,7 @@ impl Default for Args { state: false, state_strategy: None, portal_client: vec!["ipc:////tmp/trin-jsonrpc.ipc".to_owned()], + sync: false, subcommand: None, stats_recording_period: 300, } diff --git a/glados-audit/src/lib.rs b/glados-audit/src/lib.rs index 26e486ed..504d0667 100644 --- a/glados-audit/src/lib.rs +++ b/glados-audit/src/lib.rs @@ -14,6 +14,7 @@ use std::{ thread::available_parallelism, vec, }; +use sync_audit::run_sync_audit; use tokio::{ sync::mpsc::{self, Receiver}, @@ -40,6 +41,7 @@ pub mod cli; pub(crate) mod selection; mod state; pub mod stats; +pub mod sync_audit; pub(crate) mod validation; /// Configuration created from CLI arguments. @@ -59,6 +61,8 @@ pub struct AuditConfig { pub state: bool, /// Specific state audit strategies to run. pub state_strategies: Vec, + /// Run 4444 sync audits. + pub sync: bool, /// Weight for each strategy. pub weights: HashMap, /// Number requests to a Portal node active at the same time. @@ -127,6 +131,7 @@ impl AuditConfig { beacon_strategies: args.beacon_strategy.unwrap_or_default(), state: args.state, state_strategies: args.state_strategy.unwrap_or_default(), + sync: args.sync, }) } } @@ -168,6 +173,10 @@ pub async fn run_glados_command(conn: DatabaseConnection, command: cli::Command) } pub async fn run_glados_audit(conn: DatabaseConnection, config: AuditConfig) { + if config.sync { + run_sync_audit(config.clone(), &conn).await.unwrap(); + return (); + } // if state network is enabled, run state audits if config.state { spawn_state_audit(conn.clone(), config.clone()).await; diff --git a/glados-audit/src/validation.rs b/glados-audit/src/validation.rs index c157b147..a37d11f8 100644 --- a/glados-audit/src/validation.rs +++ b/glados-audit/src/validation.rs @@ -35,7 +35,7 @@ pub fn content_is_valid(content: &content::Model, content_bytes: &[u8]) -> bool } } -fn validate_beacon(content_key: &BeaconContentKey, content_bytes: &[u8]) -> bool { +pub fn validate_beacon(content_key: &BeaconContentKey, content_bytes: &[u8]) -> bool { let content: BeaconContentValue = match BeaconContentValue::decode(content_key, content_bytes) { Ok(c) => c, Err(e) => { @@ -72,7 +72,7 @@ fn validate_beacon(content_key: &BeaconContentKey, content_bytes: &[u8]) -> bool } } -fn validate_history(content_key: &HistoryContentKey, content_bytes: &[u8]) -> bool { +pub fn validate_history(content_key: &HistoryContentKey, content_bytes: &[u8]) -> bool { // check deserialization is valid let content: HistoryContentValue = match HistoryContentValue::decode(content_key, content_bytes) { diff --git a/glados-core/src/jsonrpc.rs b/glados-core/src/jsonrpc.rs index 49f21356..934c52ae 100644 --- a/glados-core/src/jsonrpc.rs +++ b/glados-core/src/jsonrpc.rs @@ -247,29 +247,10 @@ impl PortalApi { ) -> Result<(Option, QueryTrace), JsonRpcError> { match content.protocol_id { content::SubProtocol::History => { - match HistoryNetworkApiClient::trace_get_content( - &self.client, - HistoryContentKey::try_from_bytes(&content.content_key)?, - ) + self.get_history_content_with_trace(HistoryContentKey::try_from_bytes( + &content.content_key, + )?) .await - { - Ok(TraceContentInfo { content, trace, .. }) => Ok(( - Some(Content { - raw: content.into(), - }), - trace, - )), - Err(err) => match err.into() { - JsonRpcError::ContentNotFound { trace } => { - if let Some(trace) = trace { - Ok((None, trace)) - } else { - Err(JsonRpcError::MissingQueryTrace) - } - } - err => Err(err), - }, - } } content::SubProtocol::State => { match StateNetworkApiClient::trace_get_content( @@ -323,4 +304,28 @@ impl PortalApi { } } } + + pub async fn get_history_content_with_trace( + self, + key: HistoryContentKey, + ) -> Result<(Option, QueryTrace), JsonRpcError> { + match HistoryNetworkApiClient::trace_get_content(&self.client, key).await { + Ok(TraceContentInfo { content, trace, .. }) => Ok(( + Some(Content { + raw: content.into(), + }), + trace, + )), + Err(err) => match err.into() { + JsonRpcError::ContentNotFound { trace } => { + if let Some(trace) = trace { + Ok((None, trace)) + } else { + Err(JsonRpcError::MissingQueryTrace) + } + } + err => Err(err), + }, + } + } } From 3e82382d320e56b6f907475ba16343aca31c0f5e Mon Sep 17 00:00:00 2001 From: Michael Ferris Date: Mon, 5 May 2025 10:44:52 -0400 Subject: [PATCH 3/3] feat: adds sync audit web dashboard to glados-audit --- glados-web/assets/js/sync_audit.js | 129 +++++++++++++++++++++++++++ glados-web/src/lib.rs | 2 + glados-web/src/routes.rs | 71 +++++++++++++++ glados-web/src/templates.rs | 4 + glados-web/templates/sync_audit.html | 60 +++++++++++++ 5 files changed, 266 insertions(+) create mode 100644 glados-web/assets/js/sync_audit.js create mode 100644 glados-web/templates/sync_audit.html diff --git a/glados-web/assets/js/sync_audit.js b/glados-web/assets/js/sync_audit.js new file mode 100644 index 00000000..18ba0ee8 --- /dev/null +++ b/glados-web/assets/js/sync_audit.js @@ -0,0 +1,129 @@ +function syncAudit() { + const MERGE_BLOCK = 15_537_393; + const DEFAULT_SEGMENT_SIZE = 100_000; + const BAR_WIDTH = 8; + + const EXPECTED_BARS = Math.ceil(MERGE_BLOCK / DEFAULT_SEGMENT_SIZE); + + const LEFT_OFFSET = (window.innerWidth - (EXPECTED_BARS * BAR_WIDTH)) / 2; + + const margin = { top: 20, right: 20, bottom: 70, left: LEFT_OFFSET}; + const height = window.innerHeight - margin.top - margin.bottom; + + const tooltip = d3.select("#tooltip"); + + async function drawChart() { + const response = await fetch("/api/sync-audit-json/"); + const data = (await response.json()).records; + + const segmentSize = data[0]?.segment_end - data[0]?.segment_start + 1 || DEFAULT_SEGMENT_SIZE; + const totalSegments = Math.ceil(MERGE_BLOCK / segmentSize); + const totalWidth = totalSegments * BAR_WIDTH; + + const expectedData = Array.from({ length: totalSegments }, (_, i) => { + const start = i * segmentSize; + const end = Math.min(MERGE_BLOCK, start + segmentSize - 1); + const existing = data.find(d => d.segment_start === start); + return existing || { + segment_start: start, + segment_end: end, + mean_ms: 0, + median_ms: 0, + p99_ms: 0, + min_ms: 0, + max_ms: 0, + num_errors: 0, + in_progress: true + }; + }); + + const x = d3.scaleLinear() + .domain([0, expectedData.length]) + .range([0, totalWidth]); + + const y = d3.scaleLinear() + .domain([0, d3.max(expectedData, d => d.median_ms || 1000)]) + .nice() + .range([height, 0]); + + const svg = d3.select("#chart") + .html("") + .append("svg") + .attr("width", totalWidth + margin.left + margin.right) + .attr("height", height + margin.top + margin.bottom) + .append("g") + .attr("transform", `translate(${margin.left},${margin.top})`); + + const xAxis = d3.axisBottom(x) + .tickValues(d3.range(0, totalSegments, Math.ceil(totalSegments / 20))) + .tickFormat(i => expectedData[i]?.segment_start.toLocaleString() || ""); + + const yAxis = d3.axisLeft(y); + + svg.append("g") + .attr("transform", `translate(0,${height})`) + .call(xAxis) + .selectAll("text") + .attr("transform", "rotate(-45)") + .style("text-anchor", "end"); + + svg.append("g").call(yAxis); + + svg.append("text") + .attr("class", "axis-label") + .attr("x", totalWidth / 2) + .attr("y", height + 60) + .style("text-anchor", "middle") + .text("Segment Start Block"); + + svg.append("text") + .attr("class", "axis-label") + .attr("transform", "rotate(-90)") + .attr("x", -height / 2) + .attr("y", -40) + .style("text-anchor", "middle") + .text("Median Response Time (ms)"); + + const bars = svg.selectAll(".bar") + .data(expectedData) + .enter() + .append("rect") + .attr("class", d => d.in_progress ? "bar in-progress" : "bar") + .attr("fill", "#1f77b4") + .attr("x", (_, i) => x(i)) + .attr("y", d => y(d.median_ms || 0)) + .attr("width", BAR_WIDTH) + .attr("height", d => height - y(d.median_ms || 0)) + .on("mouseover", function (event, d, i) { + d3.select(this).attr("fill", "#72b3f0"); + + tooltip.transition().duration(200).style("opacity", 0.9); + tooltip + .html(d.in_progress + ? `Segment: ${d.segment_start.toLocaleString()}–${d.segment_end.toLocaleString()}
In progress...` + : `Block: ${d.segment_start.toLocaleString()}–${d.segment_end.toLocaleString()}
+ Mean: ${d.mean_ms.toLocaleString()} ms
+ Median: ${d.median_ms.toLocaleString()} ms
+ P99: ${d.p99_ms.toLocaleString()} ms
+ Min/Max: ${d.min_ms.toLocaleString()} / ${d.max_ms.toLocaleString()} ms
+ Errors: ${d.num_errors.toLocaleString()}`) + .style("left", (event.pageX + 15) + "px") + .style("top", (event.pageY - 30) + "px"); + }) + .on("mousemove", function (event, d) { + tooltip.style("left", (event.pageX + 15) + "px") + .style("top", (event.pageY - 30) + "px"); + }) + .on("mouseout", function () { + d3.select(this).attr("fill", "#1f77b4"); + tooltip.transition().duration(500).style("opacity", 0); + }) + .on("click", function (_, d, i) { + const allBars = d3.selectAll(".bar").nodes(); + const next = allBars[i + 1]; + if (next) next.scrollIntoView({ behavior: "smooth", block: "center", inline: "center" }); + }); + } + + drawChart(); +} \ No newline at end of file diff --git a/glados-web/src/lib.rs b/glados-web/src/lib.rs index f067c656..ce9c645d 100644 --- a/glados-web/src/lib.rs +++ b/glados-web/src/lib.rs @@ -131,6 +131,8 @@ pub async fn run_glados_web(config: Arc) -> Result<()> { get(routes::census_timeseries), ) .route("/api/audit-block-status/", get(routes::audit_block_status)) + .route("/api/sync-audit-json/", get(routes::latest_sync_audit_json)) + .route("/sync-audit/", get(routes::sync_audit)) .nest_service("/static/", serve_dir.clone()) .fallback_service(serve_dir) .layer(Extension(config)); diff --git a/glados-web/src/routes.rs b/glados-web/src/routes.rs index 6cde184e..ca777a01 100644 --- a/glados-web/src/routes.rs +++ b/glados-web/src/routes.rs @@ -14,6 +14,7 @@ use axum::{ }; use chrono::{DateTime, TimeZone, Utc}; use enr::NodeId; +use entity::sync_audit::{sync_audit, sync_audit_segment}; use ethportal_api::{ jsonrpsee::core::__reexports::serde_json, types::{ @@ -37,6 +38,7 @@ use crate::templates::{ ContentAuditDetailTemplate, ContentIdDetailTemplate, ContentIdListTemplate, ContentKeyDetailTemplate, ContentKeyListTemplate, EnrDetailTemplate, HtmlTemplate, IndexTemplate, NodeDetailTemplate, PaginatedCensusListTemplate, SingleCensusViewTemplate, + SyncAuditTemplate, }; use crate::{state::State, templates::AuditTuple}; use entity::{ @@ -473,6 +475,11 @@ pub async fn census_explorer() -> Result, S Ok(HtmlTemplate(template)) } +pub async fn sync_audit() -> Result, StatusCode> { + let template = SyncAuditTemplate {}; + Ok(HtmlTemplate(template)) +} + /// Returns the success rate for the last hour as a percentage. pub async fn hourly_success_rate( Extension(state): Extension>, @@ -2085,3 +2092,67 @@ mod tests { assert_eq!(nested, expected_nested); } } +#[derive(Serialize)] +struct SyncAuditSummary { + segment_start: i32, + segment_end: i32, + min_ms: i32, + max_ms: i32, + mean_ms: i32, + median_ms: i32, + p99_ms: i32, + num_errors: i32, +} + +#[derive(Serialize)] +struct SyncAuditResponse { + started_at: DateTime, + completed_at: Option>, + records: Vec, +} + +pub async fn latest_sync_audit_json( + Extension(state): Extension>, +) -> Result { + let Some(audit) = sync_audit::Entity::find() + .order_by_desc(sync_audit::Column::StartedAt) + .one(&state.database_connection) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + else { + return Err(StatusCode::NOT_FOUND); + }; + + let records = match audit + .find_related(sync_audit_segment::Entity) + .order_by_asc(sync_audit_segment::Column::StartBlock) + .all(&state.database_connection) + .await + { + Ok(records) => records, + Err(e) => { + error!("Failed to fetch sync audit segments: {e}"); + return Err(StatusCode::INTERNAL_SERVER_ERROR); + } + }; + + let summaries = records + .into_iter() + .map(|r| SyncAuditSummary { + segment_start: r.start_block, + segment_end: r.end_block, + min_ms: r.min_response_ms, + max_ms: r.max_response_ms, + mean_ms: r.mean_response_ms, + median_ms: r.median_response_ms, + p99_ms: r.p99_response_ms, + num_errors: r.num_errors, + }) + .collect(); + + Ok(Json(SyncAuditResponse { + started_at: audit.started_at, + completed_at: audit.completed_at, + records: summaries, + })) +} diff --git a/glados-web/src/templates.rs b/glados-web/src/templates.rs index 10c86e6f..1c0ab7ce 100644 --- a/glados-web/src/templates.rs +++ b/glados-web/src/templates.rs @@ -52,6 +52,10 @@ pub struct SingleCensusViewTemplate { #[template(path = "census_explorer.html")] pub struct CensusExplorerTemplate {} +#[derive(Template)] +#[template(path = "sync_audit.html")] +pub struct SyncAuditTemplate {} + #[derive(Template)] #[template(path = "node_detail.html")] pub struct NodeDetailTemplate { diff --git a/glados-web/templates/sync_audit.html b/glados-web/templates/sync_audit.html new file mode 100644 index 00000000..c80ef7a9 --- /dev/null +++ b/glados-web/templates/sync_audit.html @@ -0,0 +1,60 @@ +{% extends "base.html" %} + +{% block title %}Glados{% endblock %} + +{% block head %} + + Latest 4444s Sync Audit + + + +{% endblock %} + +{% block content %} + +

Latest Sync Audit

+
+
+
+
+ +{% endblock %} \ No newline at end of file