modify rtr server metrics data
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Instant;
|
||||
|
||||
@@ -9,15 +9,18 @@ use chrono_tz::Tz;
|
||||
use serde::Serialize;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::rtr::cache::{CacheAvailability, SharedRtrCache, VersionReportStats};
|
||||
use crate::rtr::cache::{CacheAvailability, CacheMemoryStats, SharedRtrCache, VersionReportStats};
|
||||
use crate::rtr::config::format_timezone;
|
||||
use crate::rtr::server::{RtrNotifier, RtrServiceStats, RtrTransportConnectionCounts};
|
||||
use crate::source::pipeline::{DataQualityReport, SourceLoadReport};
|
||||
use crate::source::pipeline::{
|
||||
DataQualityReport, FileFingerprint, SourceFingerprint, SourceLoadReport,
|
||||
};
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
pub struct ReportConfiguration {
|
||||
source_refresh_interval_seconds: u64,
|
||||
report_interval_seconds: u64,
|
||||
runtime_report_interval_seconds: u64,
|
||||
report_history_limit: usize,
|
||||
max_delta: u8,
|
||||
prune_delta_by_snapshot_size: bool,
|
||||
strict_ccr_validation: bool,
|
||||
@@ -35,7 +38,8 @@ struct TimingReport {
|
||||
impl ReportConfiguration {
|
||||
pub fn new(
|
||||
source_refresh_interval_seconds: u64,
|
||||
report_interval_seconds: u64,
|
||||
runtime_report_interval_seconds: u64,
|
||||
report_history_limit: usize,
|
||||
max_delta: u8,
|
||||
prune_delta_by_snapshot_size: bool,
|
||||
strict_ccr_validation: bool,
|
||||
@@ -44,7 +48,8 @@ impl ReportConfiguration {
|
||||
) -> Self {
|
||||
Self {
|
||||
source_refresh_interval_seconds,
|
||||
report_interval_seconds,
|
||||
runtime_report_interval_seconds,
|
||||
report_history_limit,
|
||||
max_delta,
|
||||
prune_delta_by_snapshot_size,
|
||||
strict_ccr_validation,
|
||||
@@ -76,6 +81,7 @@ pub struct ReportContext {
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct RuntimeReportState {
|
||||
source: Option<SourceLoadReport>,
|
||||
source_fingerprint: Option<SourceFingerprint>,
|
||||
data_quality: Option<DataQualityReport>,
|
||||
refresh: RefreshReport,
|
||||
}
|
||||
@@ -178,6 +184,14 @@ impl ReportContext {
|
||||
state.refresh.last_error = None;
|
||||
}
|
||||
|
||||
pub fn record_source_fingerprint(&self, fingerprint: SourceFingerprint) {
|
||||
let mut state = self
|
||||
.runtime
|
||||
.write()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
state.source_fingerprint = Some(fingerprint);
|
||||
}
|
||||
|
||||
pub fn record_refresh_unchanged(&self, attempted_at: DateTime<Utc>, duration_ms: u128) {
|
||||
let mut state = self
|
||||
.runtime
|
||||
@@ -227,6 +241,63 @@ impl ReportContext {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_source_or_warn(
|
||||
&self,
|
||||
report_dir: &Path,
|
||||
phase: &str,
|
||||
shared_cache: &SharedRtrCache,
|
||||
notifier: &RtrNotifier,
|
||||
service_stats: &RtrServiceStats,
|
||||
) {
|
||||
if let Err(err) =
|
||||
self.write_source(report_dir, phase, shared_cache, notifier, service_stats)
|
||||
{
|
||||
warn!(
|
||||
"failed to write RTR source report to {}: {:?}",
|
||||
report_dir.display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_clients_or_warn(
|
||||
&self,
|
||||
report_dir: &Path,
|
||||
phase: &str,
|
||||
shared_cache: &SharedRtrCache,
|
||||
notifier: &RtrNotifier,
|
||||
service_stats: &RtrServiceStats,
|
||||
) {
|
||||
if let Err(err) =
|
||||
self.write_clients(report_dir, phase, shared_cache, notifier, service_stats)
|
||||
{
|
||||
warn!(
|
||||
"failed to write RTR clients report to {}: {:?}",
|
||||
report_dir.display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_runtime_or_warn(
|
||||
&self,
|
||||
report_dir: &Path,
|
||||
phase: &str,
|
||||
shared_cache: &SharedRtrCache,
|
||||
notifier: &RtrNotifier,
|
||||
service_stats: &RtrServiceStats,
|
||||
) {
|
||||
if let Err(err) =
|
||||
self.write_runtime(report_dir, phase, shared_cache, notifier, service_stats)
|
||||
{
|
||||
warn!(
|
||||
"failed to write RTR runtime report to {}: {:?}",
|
||||
report_dir.display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn write(
|
||||
&self,
|
||||
report_dir: &Path,
|
||||
@@ -235,6 +306,64 @@ impl ReportContext {
|
||||
notifier: &RtrNotifier,
|
||||
service_stats: &RtrServiceStats,
|
||||
) -> Result<()> {
|
||||
let parts = self.build_reports(phase, shared_cache, notifier, service_stats);
|
||||
fs::create_dir_all(report_dir)
|
||||
.with_context(|| format!("create report directory {}", report_dir.display()))?;
|
||||
self.write_source_report(report_dir, &parts.suffix, &parts.source)?;
|
||||
self.write_clients_report(report_dir, &parts.suffix, &parts.clients)?;
|
||||
self.write_runtime_report(report_dir, &parts.suffix, &parts.runtime)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_source(
|
||||
&self,
|
||||
report_dir: &Path,
|
||||
phase: &str,
|
||||
shared_cache: &SharedRtrCache,
|
||||
notifier: &RtrNotifier,
|
||||
service_stats: &RtrServiceStats,
|
||||
) -> Result<()> {
|
||||
let parts = self.build_reports(phase, shared_cache, notifier, service_stats);
|
||||
fs::create_dir_all(report_dir)
|
||||
.with_context(|| format!("create report directory {}", report_dir.display()))?;
|
||||
self.write_source_report(report_dir, &parts.suffix, &parts.source)
|
||||
}
|
||||
|
||||
fn write_clients(
|
||||
&self,
|
||||
report_dir: &Path,
|
||||
phase: &str,
|
||||
shared_cache: &SharedRtrCache,
|
||||
notifier: &RtrNotifier,
|
||||
service_stats: &RtrServiceStats,
|
||||
) -> Result<()> {
|
||||
let parts = self.build_reports(phase, shared_cache, notifier, service_stats);
|
||||
fs::create_dir_all(report_dir)
|
||||
.with_context(|| format!("create report directory {}", report_dir.display()))?;
|
||||
self.write_clients_report(report_dir, &parts.suffix, &parts.clients)
|
||||
}
|
||||
|
||||
fn write_runtime(
|
||||
&self,
|
||||
report_dir: &Path,
|
||||
phase: &str,
|
||||
shared_cache: &SharedRtrCache,
|
||||
notifier: &RtrNotifier,
|
||||
service_stats: &RtrServiceStats,
|
||||
) -> Result<()> {
|
||||
let parts = self.build_reports(phase, shared_cache, notifier, service_stats);
|
||||
fs::create_dir_all(report_dir)
|
||||
.with_context(|| format!("create report directory {}", report_dir.display()))?;
|
||||
self.write_runtime_report(report_dir, &parts.suffix, &parts.runtime)
|
||||
}
|
||||
|
||||
fn build_reports(
|
||||
&self,
|
||||
phase: &str,
|
||||
shared_cache: &SharedRtrCache,
|
||||
notifier: &RtrNotifier,
|
||||
service_stats: &RtrServiceStats,
|
||||
) -> ReportParts {
|
||||
let cache = shared_cache.load_full();
|
||||
let runtime = self
|
||||
.runtime
|
||||
@@ -248,63 +377,124 @@ impl ReportContext {
|
||||
let active_connections = service_stats.active_connections();
|
||||
let connections_by_transport = service_stats.transport_connections();
|
||||
let max_connections = service_stats.max_connections();
|
||||
let report = RtrReport {
|
||||
let generated_at = self.report_now();
|
||||
let metadata = ReportMetadata {
|
||||
schema_version: 2,
|
||||
generated_at: self.report_now(),
|
||||
generated_at,
|
||||
phase: phase.to_string(),
|
||||
service: ServiceReport {
|
||||
started_at: self.to_report_time(self.started_at),
|
||||
uptime_seconds: self.started_instant.elapsed().as_secs(),
|
||||
active_connections,
|
||||
connections_by_transport: TransportConnectionReport::from(connections_by_transport),
|
||||
session_listeners: notifier.listener_count(),
|
||||
max_connections,
|
||||
connection_utilization: active_connections as f64 / max_connections as f64,
|
||||
},
|
||||
process: ProcessReport {
|
||||
rss_mib: current_rss_mib(),
|
||||
},
|
||||
source: runtime
|
||||
.source
|
||||
.map(|source| SourceLoadReportView::from_report(source, self.timezone)),
|
||||
refresh: RefreshReportView::from_report(runtime.refresh, self.timezone),
|
||||
data_quality: runtime.data_quality,
|
||||
configuration: self.configuration.clone(),
|
||||
cache: CacheReport {
|
||||
availability,
|
||||
created_at: self.to_report_time(cache.created_at().utc()),
|
||||
last_update_begin: self.to_report_time(cache.last_update_begin().utc()),
|
||||
last_update_end: self.to_report_time(cache.last_update_end().utc()),
|
||||
versions: cache.version_report_stats(),
|
||||
},
|
||||
};
|
||||
let service = ServiceReport {
|
||||
started_at: self.to_report_time(self.started_at),
|
||||
uptime_seconds: self.started_instant.elapsed().as_secs(),
|
||||
active_connections,
|
||||
connections_by_transport: TransportConnectionReport::from(connections_by_transport),
|
||||
session_listeners: notifier.listener_count(),
|
||||
max_connections,
|
||||
connection_utilization: active_connections as f64 / max_connections as f64,
|
||||
};
|
||||
let process = ProcessReport {
|
||||
rss_mib: current_rss_mib(),
|
||||
};
|
||||
let source = runtime
|
||||
.source
|
||||
.map(|source| SourceLoadReportView::from_report(source, self.timezone));
|
||||
let refresh = RefreshReportView::from_report(runtime.refresh, self.timezone);
|
||||
let cache = CacheReport {
|
||||
availability,
|
||||
created_at: self.to_report_time(cache.created_at().utc()),
|
||||
last_update_begin: self.to_report_time(cache.last_update_begin().utc()),
|
||||
last_update_end: self.to_report_time(cache.last_update_end().utc()),
|
||||
memory: cache.memory_stats(),
|
||||
versions: cache.version_report_stats(),
|
||||
};
|
||||
|
||||
fs::create_dir_all(report_dir)
|
||||
.with_context(|| format!("create report directory {}", report_dir.display()))?;
|
||||
let target = report_dir.join("rtr-server.json");
|
||||
let temporary = report_dir.join(".rtr-server.json.tmp");
|
||||
let json = serde_json::to_vec_pretty(&report).context("serialize RTR report")?;
|
||||
fs::write(&temporary, json)
|
||||
.with_context(|| format!("write temporary report {}", temporary.display()))?;
|
||||
let source_report = SourceReport {
|
||||
schema_version: metadata.schema_version,
|
||||
generated_at: metadata.generated_at,
|
||||
phase: metadata.phase.clone(),
|
||||
source,
|
||||
source_fingerprint: runtime.source_fingerprint.map(|fingerprint| {
|
||||
SourceFingerprintReport::from_fingerprint(fingerprint, self.timezone)
|
||||
}),
|
||||
refresh,
|
||||
data_quality: runtime.data_quality,
|
||||
cache,
|
||||
};
|
||||
let clients_report = ClientsReport {
|
||||
schema_version: metadata.schema_version,
|
||||
generated_at: metadata.generated_at,
|
||||
phase: metadata.phase.clone(),
|
||||
service: ServiceReport {
|
||||
started_at: service.started_at,
|
||||
uptime_seconds: service.uptime_seconds,
|
||||
active_connections: service.active_connections,
|
||||
connections_by_transport: service.connections_by_transport,
|
||||
session_listeners: service.session_listeners,
|
||||
max_connections: service.max_connections,
|
||||
connection_utilization: service.connection_utilization,
|
||||
},
|
||||
};
|
||||
let runtime_report = RuntimeReport {
|
||||
schema_version: metadata.schema_version,
|
||||
generated_at: metadata.generated_at,
|
||||
phase: metadata.phase,
|
||||
service,
|
||||
process,
|
||||
configuration: self.configuration.clone(),
|
||||
};
|
||||
let suffix = report_file_suffix(generated_at);
|
||||
|
||||
if let Err(err) = fs::rename(&temporary, &target) {
|
||||
if target.exists() {
|
||||
fs::remove_file(&target)
|
||||
.with_context(|| format!("replace existing report {}", target.display()))?;
|
||||
fs::rename(&temporary, &target)
|
||||
.with_context(|| format!("move report into {}", target.display()))?;
|
||||
} else {
|
||||
return Err(err).with_context(|| {
|
||||
format!(
|
||||
"move temporary report {} into {}",
|
||||
temporary.display(),
|
||||
target.display()
|
||||
)
|
||||
});
|
||||
}
|
||||
ReportParts {
|
||||
suffix,
|
||||
source: source_report,
|
||||
clients: clients_report,
|
||||
runtime: runtime_report,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
fn write_source_report(
|
||||
&self,
|
||||
report_dir: &Path,
|
||||
suffix: &str,
|
||||
report: &SourceReport,
|
||||
) -> Result<()> {
|
||||
write_rolling_report(
|
||||
report_dir,
|
||||
"rtr-source",
|
||||
suffix,
|
||||
self.configuration.report_history_limit,
|
||||
report,
|
||||
)
|
||||
}
|
||||
|
||||
fn write_clients_report(
|
||||
&self,
|
||||
report_dir: &Path,
|
||||
suffix: &str,
|
||||
report: &ClientsReport,
|
||||
) -> Result<()> {
|
||||
write_rolling_report(
|
||||
report_dir,
|
||||
"rtr-clients",
|
||||
suffix,
|
||||
self.configuration.report_history_limit,
|
||||
report,
|
||||
)
|
||||
}
|
||||
|
||||
fn write_runtime_report(
|
||||
&self,
|
||||
report_dir: &Path,
|
||||
suffix: &str,
|
||||
report: &RuntimeReport,
|
||||
) -> Result<()> {
|
||||
write_rolling_report(
|
||||
report_dir,
|
||||
"rtr-runtime",
|
||||
suffix,
|
||||
self.configuration.report_history_limit,
|
||||
report,
|
||||
)
|
||||
}
|
||||
|
||||
fn report_now(&self) -> DateTime<Tz> {
|
||||
@@ -316,21 +506,51 @@ impl ReportContext {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
struct ReportMetadata {
|
||||
schema_version: u16,
|
||||
generated_at: DateTime<Tz>,
|
||||
phase: String,
|
||||
}
|
||||
|
||||
struct ReportParts {
|
||||
suffix: String,
|
||||
source: SourceReport,
|
||||
clients: ClientsReport,
|
||||
runtime: RuntimeReport,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct RtrReport {
|
||||
struct SourceReport {
|
||||
schema_version: u16,
|
||||
generated_at: DateTime<Tz>,
|
||||
phase: String,
|
||||
source: Option<SourceLoadReportView>,
|
||||
source_fingerprint: Option<SourceFingerprintReport>,
|
||||
refresh: RefreshReportView,
|
||||
data_quality: Option<DataQualityReport>,
|
||||
cache: CacheReport,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ClientsReport {
|
||||
schema_version: u16,
|
||||
generated_at: DateTime<Tz>,
|
||||
phase: String,
|
||||
service: ServiceReport,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct RuntimeReport {
|
||||
schema_version: u16,
|
||||
generated_at: DateTime<Tz>,
|
||||
phase: String,
|
||||
service: ServiceReport,
|
||||
process: ProcessReport,
|
||||
source: Option<SourceLoadReportView>,
|
||||
refresh: RefreshReportView,
|
||||
data_quality: Option<DataQualityReport>,
|
||||
configuration: ReportConfiguration,
|
||||
cache: CacheReport,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Clone, Copy, Serialize)]
|
||||
struct ServiceReport {
|
||||
started_at: DateTime<Tz>,
|
||||
uptime_seconds: u64,
|
||||
@@ -353,6 +573,45 @@ struct SourceLoadReportView {
|
||||
slurm_version: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SourceFingerprintReport {
|
||||
ccr: FileFingerprintReport,
|
||||
slurm_files: Vec<FileFingerprintReport>,
|
||||
}
|
||||
|
||||
impl SourceFingerprintReport {
|
||||
fn from_fingerprint(fingerprint: SourceFingerprint, timezone: Tz) -> Self {
|
||||
Self {
|
||||
ccr: FileFingerprintReport::from_fingerprint(fingerprint.ccr, timezone),
|
||||
slurm_files: fingerprint
|
||||
.slurm_files
|
||||
.into_iter()
|
||||
.map(|fingerprint| FileFingerprintReport::from_fingerprint(fingerprint, timezone))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct FileFingerprintReport {
|
||||
path: String,
|
||||
len: u64,
|
||||
modified_unix_secs: u64,
|
||||
modified_at: Option<DateTime<Tz>>,
|
||||
}
|
||||
|
||||
impl FileFingerprintReport {
|
||||
fn from_fingerprint(fingerprint: FileFingerprint, timezone: Tz) -> Self {
|
||||
Self {
|
||||
path: fingerprint.path,
|
||||
len: fingerprint.len,
|
||||
modified_unix_secs: fingerprint.modified_unix_secs,
|
||||
modified_at: DateTime::from_timestamp(fingerprint.modified_unix_secs as i64, 0)
|
||||
.map(|time| to_report_time(time, timezone)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SourceLoadReportView {
|
||||
fn from_report(report: SourceLoadReport, timezone: Tz) -> Self {
|
||||
Self {
|
||||
@@ -370,7 +629,7 @@ impl SourceLoadReportView {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Clone, Copy, Serialize)]
|
||||
struct TransportConnectionReport {
|
||||
tcp: usize,
|
||||
tls: usize,
|
||||
@@ -398,6 +657,7 @@ struct CacheReport {
|
||||
created_at: DateTime<Tz>,
|
||||
last_update_begin: DateTime<Tz>,
|
||||
last_update_end: DateTime<Tz>,
|
||||
memory: CacheMemoryStats,
|
||||
versions: [VersionReportStats; 3],
|
||||
}
|
||||
|
||||
@@ -415,6 +675,77 @@ pub fn current_rss_mib() -> Option<u64> {
|
||||
Some(kb / 1024)
|
||||
}
|
||||
|
||||
fn report_file_suffix(time: DateTime<Tz>) -> String {
|
||||
time.format("%Y%m%d%H%M%S%9f").to_string()
|
||||
}
|
||||
|
||||
fn write_rolling_report<T: Serialize>(
|
||||
report_dir: &Path,
|
||||
prefix: &str,
|
||||
suffix: &str,
|
||||
keep: usize,
|
||||
report: &T,
|
||||
) -> Result<()> {
|
||||
let file_name = format!("{prefix}-{suffix}.json");
|
||||
let target = report_dir.join(&file_name);
|
||||
let temporary = report_dir.join(format!(".{file_name}.tmp"));
|
||||
let json = serde_json::to_vec_pretty(report)
|
||||
.with_context(|| format!("serialize {prefix} RTR report"))?;
|
||||
fs::write(&temporary, json)
|
||||
.with_context(|| format!("write temporary report {}", temporary.display()))?;
|
||||
replace_file(&temporary, &target)?;
|
||||
prune_rolling_reports(report_dir, prefix, keep)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn replace_file(temporary: &Path, target: &Path) -> Result<()> {
|
||||
if let Err(err) = fs::rename(temporary, target) {
|
||||
if target.exists() {
|
||||
fs::remove_file(target)
|
||||
.with_context(|| format!("replace existing report {}", target.display()))?;
|
||||
fs::rename(temporary, target)
|
||||
.with_context(|| format!("move report into {}", target.display()))?;
|
||||
} else {
|
||||
return Err(err).with_context(|| {
|
||||
format!(
|
||||
"move temporary report {} into {}",
|
||||
temporary.display(),
|
||||
target.display()
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prune_rolling_reports(report_dir: &Path, prefix: &str, keep: usize) -> Result<()> {
|
||||
let start = format!("{prefix}-");
|
||||
let mut files = Vec::<PathBuf>::new();
|
||||
for entry in fs::read_dir(report_dir)
|
||||
.with_context(|| format!("read report directory {}", report_dir.display()))?
|
||||
{
|
||||
let entry =
|
||||
entry.with_context(|| format!("iterate report directory {}", report_dir.display()))?;
|
||||
let path = entry.path();
|
||||
if !path.is_file() {
|
||||
continue;
|
||||
}
|
||||
let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
|
||||
continue;
|
||||
};
|
||||
if name.starts_with(&start) && name.ends_with(".json") {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
files.sort();
|
||||
let remove_count = files.len().saturating_sub(keep);
|
||||
for path in files.into_iter().take(remove_count) {
|
||||
fs::remove_file(&path)
|
||||
.with_context(|| format!("remove old rolling report {}", path.display()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
@@ -432,7 +763,8 @@ mod tests {
|
||||
fn test_configuration() -> ReportConfiguration {
|
||||
ReportConfiguration::new(
|
||||
300,
|
||||
60,
|
||||
300,
|
||||
10,
|
||||
100,
|
||||
false,
|
||||
false,
|
||||
@@ -460,29 +792,53 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let report: Value =
|
||||
serde_json::from_slice(&fs::read(report_dir.join("rtr-server.json")).unwrap()).unwrap();
|
||||
assert_eq!(report["schema_version"], 2);
|
||||
assert_eq!(report["phase"], "test");
|
||||
assert_report_time_offset(&report["generated_at"]);
|
||||
assert_report_time_offset(&report["service"]["started_at"]);
|
||||
assert_report_time_offset(&report["cache"]["created_at"]);
|
||||
assert_eq!(report["cache"]["availability"], "ready");
|
||||
assert_eq!(report["refresh"]["status"], "not_attempted");
|
||||
assert!(report["source"].is_null());
|
||||
assert!(report["data_quality"].is_null());
|
||||
let source_report = read_single_report(&report_dir, "rtr-source");
|
||||
assert_eq!(source_report["schema_version"], 2);
|
||||
assert_eq!(source_report["phase"], "test");
|
||||
assert_report_time_offset(&source_report["generated_at"]);
|
||||
assert_report_time_offset(&source_report["cache"]["created_at"]);
|
||||
assert_eq!(source_report["cache"]["availability"], "ready");
|
||||
assert_eq!(source_report["refresh"]["status"], "not_attempted");
|
||||
assert!(source_report["source"].is_null());
|
||||
assert!(source_report["data_quality"].is_null());
|
||||
|
||||
let runtime_report = read_single_report(&report_dir, "rtr-runtime");
|
||||
assert_report_time_offset(&runtime_report["service"]["started_at"]);
|
||||
assert_eq!(
|
||||
report["configuration"]["source_refresh_interval_seconds"],
|
||||
runtime_report["configuration"]["source_refresh_interval_seconds"],
|
||||
300
|
||||
);
|
||||
assert_eq!(report["service"]["max_connections"], 1024);
|
||||
assert_eq!(report["service"]["active_connections"], 0);
|
||||
assert_eq!(report["service"]["connections_by_transport"]["tcp"], 0);
|
||||
assert_eq!(report["service"]["connections_by_transport"]["tls"], 0);
|
||||
assert_eq!(report["service"]["connections_by_transport"]["ssh"], 0);
|
||||
assert_eq!(report["cache"]["versions"].as_array().unwrap().len(), 3);
|
||||
assert_eq!(report["cache"]["versions"][2]["snapshot"]["total"], 0);
|
||||
assert!(!report_dir.join(".rtr-server.json.tmp").exists());
|
||||
assert_eq!(runtime_report["configuration"]["report_history_limit"], 10);
|
||||
|
||||
let clients_report = read_single_report(&report_dir, "rtr-clients");
|
||||
assert_eq!(clients_report["service"]["max_connections"], 1024);
|
||||
assert_eq!(clients_report["service"]["active_connections"], 0);
|
||||
assert_eq!(
|
||||
clients_report["service"]["connections_by_transport"]["tcp"],
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
clients_report["service"]["connections_by_transport"]["tls"],
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
clients_report["service"]["connections_by_transport"]["ssh"],
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
source_report["cache"]["versions"].as_array().unwrap().len(),
|
||||
3
|
||||
);
|
||||
assert_eq!(
|
||||
source_report["cache"]["versions"][2]["snapshot"]["total"],
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
source_report["cache"]["memory"]["delta_payload_counts"][2],
|
||||
0
|
||||
);
|
||||
assert!(source_report["source_fingerprint"].is_null());
|
||||
assert_no_temporary_reports(&report_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -530,6 +886,18 @@ mod tests {
|
||||
},
|
||||
slurm_assertions: SlurmRuleCounts::default(),
|
||||
};
|
||||
context.record_source_fingerprint(SourceFingerprint {
|
||||
ccr: FileFingerprint {
|
||||
path: "data/example.ccr".to_string(),
|
||||
len: 123,
|
||||
modified_unix_secs: 1_781_404_800,
|
||||
},
|
||||
slurm_files: vec![FileFingerprint {
|
||||
path: "policy.slurm".to_string(),
|
||||
len: 42,
|
||||
modified_unix_secs: 1_781_408_400,
|
||||
}],
|
||||
});
|
||||
|
||||
context.record_refresh_success(Utc::now(), 12, true, source, quality);
|
||||
context.record_refresh_failure(Utc::now(), 5, &anyhow::anyhow!("source unavailable"));
|
||||
@@ -543,9 +911,18 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let report: Value =
|
||||
serde_json::from_slice(&fs::read(report_dir.join("rtr-server.json")).unwrap()).unwrap();
|
||||
let report = read_single_report(&report_dir, "rtr-source");
|
||||
assert_eq!(report["source"]["ccr_file"], "data/example.ccr");
|
||||
assert_eq!(
|
||||
report["source_fingerprint"]["ccr"]["path"],
|
||||
"data/example.ccr"
|
||||
);
|
||||
assert_eq!(report["source_fingerprint"]["ccr"]["len"], 123);
|
||||
assert_report_time_offset(&report["source_fingerprint"]["ccr"]["modified_at"]);
|
||||
assert_eq!(
|
||||
report["source_fingerprint"]["slurm_files"][0]["path"],
|
||||
"policy.slurm"
|
||||
);
|
||||
assert_report_time_offset(&report["source"]["ccr_modified_at"]);
|
||||
assert_eq!(report["data_quality"]["after_slurm"]["total"], 10);
|
||||
assert_eq!(report["refresh"]["status"], "failed");
|
||||
@@ -555,6 +932,84 @@ mod tests {
|
||||
assert_report_time_offset(&report["refresh"]["last_success_at"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rolling_reports_keep_latest_files_per_category() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let report_dir = temp.path().join("report");
|
||||
let shared_cache = Arc::new(ArcSwap::from_pointee(RtrCache::default()));
|
||||
let service = RtrService::new(shared_cache.clone());
|
||||
let notifier = service.notifier();
|
||||
let mut configuration = test_configuration();
|
||||
configuration.report_history_limit = 2;
|
||||
let context = ReportContext::new(configuration);
|
||||
|
||||
for phase in ["one", "two", "three"] {
|
||||
context
|
||||
.write(
|
||||
&report_dir,
|
||||
phase,
|
||||
&shared_cache,
|
||||
¬ifier,
|
||||
&service.stats(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
assert_eq!(report_files(&report_dir, "rtr-source").len(), 2);
|
||||
assert_eq!(report_files(&report_dir, "rtr-clients").len(), 2);
|
||||
assert_eq!(report_files(&report_dir, "rtr-runtime").len(), 2);
|
||||
assert_no_temporary_reports(&report_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn category_writes_only_create_requested_report_type() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let report_dir = temp.path().join("report");
|
||||
let shared_cache = Arc::new(ArcSwap::from_pointee(RtrCache::default()));
|
||||
let service = RtrService::new(shared_cache.clone());
|
||||
let notifier = service.notifier();
|
||||
let context = ReportContext::new(test_configuration());
|
||||
|
||||
context
|
||||
.write_source(
|
||||
&report_dir,
|
||||
"source_only",
|
||||
&shared_cache,
|
||||
¬ifier,
|
||||
&service.stats(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(report_files(&report_dir, "rtr-source").len(), 1);
|
||||
assert_eq!(report_files(&report_dir, "rtr-clients").len(), 0);
|
||||
assert_eq!(report_files(&report_dir, "rtr-runtime").len(), 0);
|
||||
|
||||
context
|
||||
.write_clients(
|
||||
&report_dir,
|
||||
"clients_only",
|
||||
&shared_cache,
|
||||
¬ifier,
|
||||
&service.stats(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(report_files(&report_dir, "rtr-source").len(), 1);
|
||||
assert_eq!(report_files(&report_dir, "rtr-clients").len(), 1);
|
||||
assert_eq!(report_files(&report_dir, "rtr-runtime").len(), 0);
|
||||
|
||||
context
|
||||
.write_runtime(
|
||||
&report_dir,
|
||||
"runtime_only",
|
||||
&shared_cache,
|
||||
¬ifier,
|
||||
&service.stats(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(report_files(&report_dir, "rtr-source").len(), 1);
|
||||
assert_eq!(report_files(&report_dir, "rtr-clients").len(), 1);
|
||||
assert_eq!(report_files(&report_dir, "rtr-runtime").len(), 1);
|
||||
}
|
||||
|
||||
fn assert_report_time_offset(value: &Value) {
|
||||
let value = value.as_str().expect("report time should be a string");
|
||||
assert!(
|
||||
@@ -562,4 +1017,36 @@ mod tests {
|
||||
"report time should use +08:00 offset, got {value}"
|
||||
);
|
||||
}
|
||||
|
||||
fn read_single_report(report_dir: &Path, prefix: &str) -> Value {
|
||||
let files = report_files(report_dir, prefix);
|
||||
assert_eq!(files.len(), 1, "expected one {prefix} report");
|
||||
serde_json::from_slice(&fs::read(&files[0]).unwrap()).unwrap()
|
||||
}
|
||||
|
||||
fn report_files(report_dir: &Path, prefix: &str) -> Vec<PathBuf> {
|
||||
let start = format!("{prefix}-");
|
||||
let mut files = fs::read_dir(report_dir)
|
||||
.unwrap()
|
||||
.map(|entry| entry.unwrap().path())
|
||||
.filter(|path| {
|
||||
path.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.is_some_and(|name| name.starts_with(&start) && name.ends_with(".json"))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
files.sort();
|
||||
files
|
||||
}
|
||||
|
||||
fn assert_no_temporary_reports(report_dir: &Path) {
|
||||
let has_temporary = fs::read_dir(report_dir).unwrap().any(|entry| {
|
||||
entry
|
||||
.unwrap()
|
||||
.file_name()
|
||||
.to_str()
|
||||
.is_some_and(|name| name.ends_with(".tmp"))
|
||||
});
|
||||
assert!(!has_temporary);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user