use serde::{Deserialize, Serialize};
use std::fmt;
use std::time::Duration;
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct EnrichmentStats {
pub components_queried: usize,
pub components_with_vulns: usize,
pub total_vulns_found: usize,
pub cache_hits: usize,
pub api_calls: usize,
pub components_skipped: usize,
#[serde(with = "duration_serde")]
pub duration: Duration,
pub errors: Vec<EnrichmentError>,
}
impl EnrichmentStats {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn empty() -> Self {
Self::default()
}
#[must_use]
pub const fn components_checked(&self) -> usize {
self.components_queried + self.components_skipped
}
pub fn log_summary(&self) {
tracing::info!(
"Enrichment complete: {} components queried, {} with vulns ({} total), \
{} cache hits, {} API calls, {} skipped in {:?}",
self.components_queried,
self.components_with_vulns,
self.total_vulns_found,
self.cache_hits,
self.api_calls,
self.components_skipped,
self.duration
);
for err in &self.errors {
tracing::warn!("Enrichment error: {}", err);
}
}
#[must_use]
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
pub fn merge(&mut self, other: &Self) {
self.components_queried += other.components_queried;
self.components_with_vulns += other.components_with_vulns;
self.total_vulns_found += other.total_vulns_found;
self.cache_hits += other.cache_hits;
self.api_calls += other.api_calls;
self.components_skipped += other.components_skipped;
self.duration += other.duration;
self.errors.extend(other.errors.iter().cloned());
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum EnrichmentError {
ApiError(String),
RateLimitExceeded,
CacheError(String),
ParseError(String),
Timeout,
MissingIdentifiers(String),
}
impl fmt::Display for EnrichmentError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ApiError(msg) => write!(f, "API error: {msg}"),
Self::RateLimitExceeded => write!(f, "Rate limit exceeded"),
Self::CacheError(msg) => write!(f, "Cache error: {msg}"),
Self::ParseError(msg) => write!(f, "Parse error: {msg}"),
Self::Timeout => write!(f, "Request timeout"),
Self::MissingIdentifiers(name) => {
write!(f, "Component '{name}' missing identifiers for query")
}
}
}
}
impl std::error::Error for EnrichmentError {}
mod duration_serde {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::time::Duration;
pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
duration.as_millis().serialize(serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
D: Deserializer<'de>,
{
let millis = u64::deserialize(deserializer)?;
Ok(Duration::from_millis(millis))
}
}