20260616 增加产品UI查询服务

This commit is contained in:
2026-06-17 10:16:04 +08:00
parent 4e6bd687db
commit 6ef2c98890
20 changed files with 7526 additions and 426 deletions

View File

@@ -3,8 +3,12 @@ use std::path::Path;
use serde::Serialize;
use serde::ser::SerializeSeq;
use sha2::Digest;
use crate::audit::{AspaOutput, AuditRunMeta, AuditWarning, VrpOutput};
use crate::audit::{
AspaOutput, AuditRunMeta, AuditWarning, QueryAuditManifest, ValidationEvent,
ValidationEventCounts, VrpOutput,
};
use crate::ccr::canonical_vrp_prefix;
use super::{PostValidationShared, RunStageTiming};
@@ -49,6 +53,8 @@ struct BorrowedAuditReportV2<'a> {
downloads: &'a [crate::audit::AuditDownloadEvent],
download_stats: &'a crate::audit::AuditDownloadStats,
repo_sync_stats: crate::audit::AuditRepoSyncStats,
#[serde(rename = "queryAudit", skip_serializing_if = "Option::is_none")]
query_audit: Option<QueryAuditManifest>,
}
#[derive(Serialize)]
@@ -125,6 +131,7 @@ pub(super) fn write_report_json_from_shared(
.format(&Rfc3339)
.expect("format validation_time");
let repo_sync_stats = super::build_repo_sync_stats(shared.publication_points.as_ref());
let query_audit = write_validation_events_sidecar(path, &validation_time_rfc3339_utc, shared)?;
let report = BorrowedAuditReportV2 {
format_version: 2,
meta: AuditRunMeta {
@@ -142,6 +149,7 @@ pub(super) fn write_report_json_from_shared(
downloads: shared.downloads.as_ref(),
download_stats: &shared.download_stats,
repo_sync_stats,
query_audit: Some(query_audit),
};
let build_ms = build_started.elapsed().as_millis() as u64;
@@ -153,6 +161,148 @@ pub(super) fn write_report_json_from_shared(
})
}
fn write_validation_events_sidecar(
report_path: &Path,
validation_time: &str,
shared: &PostValidationShared,
) -> Result<QueryAuditManifest, String> {
let events_path = report_path.with_file_name("validation-events.jsonl");
if let Some(parent) = events_path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| format!("create validation events parent failed: {e}"))?;
}
let mut writer = BufWriter::new(std::fs::File::create(&events_path).map_err(|e| {
format!(
"create validation events failed: {}: {e}",
events_path.display()
)
})?);
let mut seq = 0u64;
let mut hasher = sha2::Sha256::new();
emit_validation_events(validation_time, shared, &mut seq, &mut |event| {
let mut line = serde_json::to_vec(&event)
.map_err(|e| format!("serialize validation event failed: {e}"))?;
line.push(b'\n');
std::io::Write::write_all(&mut writer, &line)
.map_err(|e| format!("write validation event failed: {e}"))?;
hasher.update(&line);
Ok(())
})?;
std::io::Write::flush(&mut writer)
.map_err(|e| format!("flush validation events failed: {e}"))?;
let events_count = seq;
let events_sha256 = hex::encode(hasher.finalize());
Ok(QueryAuditManifest {
schema_version: 1,
status: "complete".to_string(),
events_path: events_path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("validation-events.jsonl")
.to_string(),
events_count,
events_sha256,
writer_version: 1,
error: None,
})
}
fn emit_validation_events(
validation_time: &str,
shared: &PostValidationShared,
seq: &mut u64,
emit: &mut impl FnMut(ValidationEvent) -> Result<(), String>,
) -> Result<(), String> {
emit(next_event(seq, "run_summary", validation_time, |event| {
event.counts = Some(ValidationEventCounts {
objects: Some(
shared
.publication_points
.iter()
.map(|pp| pp.objects.len() as u64)
.sum(),
),
warnings: Some(
(shared.tree_warnings.len()
+ shared
.publication_points
.iter()
.map(|pp| pp.warnings.len())
.sum::<usize>()) as u64,
),
vrps: Some(shared.vrps.len() as u64),
aspas: Some(shared.aspas.len() as u64),
});
}))?;
for pp in shared.publication_points.iter() {
emit(next_event(
seq,
"publication_point",
validation_time,
|event| {
event.pp_node_id = pp.node_id;
event.pp_manifest_uri = Some(pp.manifest_rsync_uri.clone());
event.pp_rsync_base_uri = Some(pp.rsync_base_uri.clone());
event.repo_sync_phase = pp.repo_sync_phase.clone();
event.repo_terminal_state = Some(pp.repo_terminal_state.clone());
event.counts = Some(ValidationEventCounts {
objects: Some(pp.objects.len() as u64),
warnings: Some(pp.warnings.len() as u64),
vrps: None,
aspas: None,
});
},
))?;
for object in &pp.objects {
emit(next_event(seq, "object", validation_time, |event| {
event.pp_node_id = pp.node_id;
event.pp_manifest_uri = Some(pp.manifest_rsync_uri.clone());
event.object_uri = Some(object.rsync_uri.clone());
event.sha256 = Some(object.sha256_hex.clone());
event.object_type = Some(object.kind.clone());
event.result = Some(object.result.clone());
event.reason = object.detail.clone();
}))?;
}
for warning in &pp.warnings {
emit(next_event(seq, "warning", validation_time, |event| {
event.pp_node_id = pp.node_id;
event.pp_manifest_uri = Some(pp.manifest_rsync_uri.clone());
event.reason = Some(warning.message.clone());
}))?;
}
}
Ok(())
}
fn next_event(
seq: &mut u64,
event_type: &str,
validation_time: &str,
fill: impl FnOnce(&mut ValidationEvent),
) -> ValidationEvent {
*seq += 1;
let mut event = ValidationEvent {
schema_version: 1,
seq: *seq,
event_type: event_type.to_string(),
validation_time: validation_time.to_string(),
pp_node_id: None,
pp_manifest_uri: None,
pp_rsync_base_uri: None,
repo_sync_phase: None,
repo_terminal_state: None,
object_uri: None,
sha256: None,
object_type: None,
result: None,
reason: None,
counts: None,
};
fill(&mut event);
event
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct CompareViewTaskOutput {
pub(super) build_ms: Option<u64>,

View File

@@ -1452,6 +1452,14 @@ fn synthetic_post_validation_shared() -> PostValidationShared {
let mut pp1 = crate::audit::PublicationPointAudit::default();
pp1.source = "fresh".to_string();
pp1.rrdp_notification_uri = Some("https://example.test/n1.xml".to_string());
pp1.manifest_rsync_uri = "rsync://example.test/repo/pp1/manifest.mft".to_string();
pp1.objects.push(crate::audit::ObjectAuditEntry {
rsync_uri: "rsync://example.test/repo/pp1/a.roa".to_string(),
sha256_hex: "11".repeat(32),
kind: crate::audit::AuditObjectKind::Roa,
result: crate::audit::AuditObjectResult::Ok,
detail: None,
});
let mut pp2 = crate::audit::PublicationPointAudit::default();
pp2.source = "fresh".to_string();
pp2.rrdp_notification_uri = Some("https://example.test/n1.xml".to_string());
@@ -1549,6 +1557,19 @@ fn run_report_task_and_stage_timing_work() {
serde_json::from_str(&report_json).expect("parse compact report json");
assert_eq!(report["vrps"].as_array().unwrap().len(), 2);
assert_eq!(report["aspas"].as_array().unwrap().len(), 1);
assert_eq!(report["queryAudit"]["status"].as_str(), Some("complete"));
assert!(report["queryAudit"]["eventsCount"].as_u64().unwrap() > 0);
let events_path = dir.path().join(
report["queryAudit"]["eventsPath"]
.as_str()
.expect("events path"),
);
let events = std::fs::read_to_string(events_path).expect("read validation events");
assert!(
events
.lines()
.any(|line| line.contains("\"eventType\":\"object\""))
);
let stage_timing = RunStageTiming {
validation_ms: 1,
@@ -1858,6 +1879,7 @@ fn write_json_writes_report() {
downloads: Vec::new(),
download_stats: crate::audit::AuditDownloadStats::default(),
repo_sync_stats: crate::audit::AuditRepoSyncStats::default(),
query_audit: None,
};
let dir = tempfile::tempdir().expect("tmpdir");