mod config;
mod events;
mod shared;
#[cfg(feature = "telemetry")]
mod client;
mod queue;
pub use config::TelemetryConfig;
pub use events::{TelemetryEvent, ValidationRunEvent, is_valid_rule_id};
pub use queue::EventQueue;
#[cfg(feature = "telemetry")]
pub use client::TelemetryClient;
use std::collections::HashMap;
#[cfg(feature = "telemetry")]
use std::thread;
pub fn record_validation(
file_type_counts: HashMap<String, u32>,
rule_trigger_counts: HashMap<String, u32>,
error_count: u32,
warning_count: u32,
info_count: u32,
duration_ms: u64,
) {
let config = match TelemetryConfig::load() {
Ok(c) => c,
Err(error) => {
tracing::debug!(%error, "telemetry config load failed; skipping validation event");
return;
}
};
if !config.is_enabled() {
return;
}
let event = TelemetryEvent::ValidationRun(ValidationRunEvent {
file_type_counts,
rule_trigger_counts,
error_count,
warning_count,
info_count,
duration_ms,
timestamp: chrono_timestamp(),
});
let mut queue = match EventQueue::load() {
Ok(q) => q,
Err(error) => {
tracing::debug!(%error, "telemetry queue load failed; skipping validation event");
return;
}
};
if let Err(error) = queue.push(event) {
tracing::debug!(%error, "telemetry queue push failed; dropping validation event");
return;
}
#[cfg(feature = "telemetry")]
{
thread::spawn(move || {
try_submit_queued_events(&config, &mut queue);
});
}
}
#[cfg(feature = "telemetry")]
fn try_submit_queued_events(config: &TelemetryConfig, queue: &mut EventQueue) {
let client = match TelemetryClient::new(config) {
Ok(client) => client,
Err(error) => {
tracing::debug!(%error, "telemetry client initialization failed");
return;
}
};
let events = queue.take_batch(10);
if events.is_empty() {
return;
}
match client.submit_batch(&events) {
Ok(_) => {
queue.remove_batch(events.len());
if let Err(error) = queue.save() {
tracing::debug!(%error, "telemetry queue save failed after submission");
}
}
Err(error) => {
tracing::debug!(%error, "telemetry submission failed; events remain queued");
}
}
}
fn chrono_timestamp() -> String {
shared::chrono_timestamp()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chrono_timestamp_format() {
let ts = chrono_timestamp();
assert!(ts.len() == 20);
assert!(ts.ends_with('Z'));
assert!(ts.contains('T'));
let year: i32 = ts[0..4].parse().unwrap();
assert!((2020..=2100).contains(&year));
}
}