diff --git a/README.md b/README.md index 28e5487..d8e54f8 100644 --- a/README.md +++ b/README.md @@ -432,7 +432,7 @@ for packet in &result2.packets { **Notes:** - Scoped parsers (`AutoScopedParser`, `RouterScopedParser`) inherit the pending flows configuration from the builder -- Pending flow metrics (`pending_cached`, `pending_replayed`, `pending_dropped`, `pending_replay_failed`) are available via `CacheStats` +- Pending flow metrics (`pending_cached`, `pending_replayed`, `pending_dropped`, `pending_replay_failed`) are available via `CacheInfo` ### Filtering Versions @@ -886,11 +886,11 @@ let mut parser = NetflowParser::default(); parser.parse_bytes(&data); // Get cache statistics -let v9_stats = parser.v9_cache_stats(); -println!("V9 Cache: {}/{} templates", v9_stats.current_size, v9_stats.max_size_per_cache); +let v9_info = parser.v9_cache_info(); +println!("V9 Cache: {}/{} templates", v9_info.current_size, v9_info.max_size_per_cache); // Access performance metrics -let metrics = &v9_stats.metrics; +let metrics = &v9_info.metrics; println!("Cache hits: {}", metrics.hits); println!("Cache misses: {}", metrics.misses); println!("Evictions: {}", metrics.evictions); @@ -1011,9 +1011,9 @@ let mut scoped = RouterScopedParser::::try_with_builder(router_builder). Monitor when template IDs are reused with different definitions: ```rust,ignore -let v9_stats = parser.v9_cache_stats(); -if v9_stats.metrics.collisions > 0 { - println!("Warning: {} template collisions detected", v9_stats.metrics.collisions); +let v9_info = parser.v9_cache_info(); +if v9_info.metrics.collisions > 0 { + println!("Warning: {} template collisions detected", v9_info.metrics.collisions); println!("Use AutoScopedParser for RFC-compliant multi-source deployments"); } ``` @@ -1069,11 +1069,11 @@ use netflow_parser::NetflowParser; let parser = NetflowParser::default(); // Get cache statistics -let v9_stats = parser.v9_cache_stats(); -println!("V9 cache: {}/{} templates", v9_stats.current_size, v9_stats.max_size_per_cache); +let v9_info = parser.v9_cache_info(); +println!("V9 cache: {}/{} templates", v9_info.current_size, v9_info.max_size_per_cache); -let ipfix_stats = parser.ipfix_cache_stats(); -println!("IPFIX cache: {}/{} templates", ipfix_stats.current_size, ipfix_stats.max_size_per_cache); +let ipfix_info = parser.ipfix_cache_info(); +println!("IPFIX cache: {}/{} templates", ipfix_info.current_size, ipfix_info.max_size_per_cache); // List all cached template IDs let v9_templates = parser.v9_template_ids(); diff --git a/RELEASES.md b/RELEASES.md index 74344bc..2fb824e 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -31,7 +31,16 @@ - Removed `available_templates` field. Use `parser.v9_available_template_ids()` or `parser.ipfix_available_template_ids()` instead - Added `truncated: bool` field. Code that destructures `NoTemplateInfo` must include the new field (or use `..`) -* **`CacheMetrics` methods now require `&mut self` instead of `&self`** +* **Cache observability types renamed for clarity** + - `CacheStats` → `CacheInfo` (structural cache state: size, capacity, TTL, pending count) + - `CacheMetricsSnapshot` → `CacheMetrics` (operational counters: hits, misses, evictions, etc.) + - `ParserCacheStats` → `ParserCacheInfo` (groups V9 + IPFIX `CacheInfo`) + - The internal mutable metrics type is now `pub(crate) CacheMetricsInner` (not part of public API) + - Methods renamed: `v9_cache_stats()` → `v9_cache_info()`, `ipfix_cache_stats()` → `ipfix_cache_info()` + - Scoped parser methods renamed: `all_stats()` → `all_info()`, `get_source_stats()` → `get_source_info()`, `v9_stats()` → `v9_info()`, `ipfix_stats()` → `ipfix_info()`, `legacy_stats()` → `legacy_info()` + - Migration: rename types and method calls. The `CacheInfo.metrics` field is now `CacheMetrics` (was `CacheMetricsSnapshot`) + +* **`CacheMetrics` (formerly `CacheMetricsInner`) methods now require `&mut self` instead of `&self`** - Uses plain `u64` counters instead of `AtomicU64`, removing atomic overhead in the single-threaded parser * **`NetflowParser` fields are now `pub(crate)`** @@ -100,17 +109,17 @@ - `None` when the field is 1 byte (classification engine ID only, no selector) - Fixes round-trip serialization: previously a 1-byte field serialized to 2 bytes -* **`CacheStats` struct field changes** +* **`CacheInfo` struct field changes** - `max_size` renamed to `max_size_per_cache` (clarifies that it applies per internal LRU cache) - Added `num_caches: usize` field (V9 has 2 caches, IPFIX has 4) - - Code that destructures `CacheStats` must update the field name and include `num_caches` (or use `..`) + - Code that destructures `CacheInfo` must update the field name and include `num_caches` (or use `..`) * **`TemplateEvent` field `template_id` changed from `u16` to `Option`** - All variants (`Learned`, `Collision`, `Evicted`, `Expired`, `MissingTemplate`) now use `Option` - `None` when the event is derived from metric deltas (specific ID not available from metrics layer) - Pattern matching must use `template_id: Some(id)` or `template_id: _` -* **`CacheMetrics` record methods scoped to `pub(crate)`** +* **`CacheMetricsInner` record methods scoped to `pub(crate)`** - `record_hit()`, `record_miss()`, `record_eviction()`, `record_insertion()`, `record_expiration()`, `record_collision()`, and pending flow record methods changed from `pub` to `pub(crate)` - `reset()` method removed entirely - `snapshot()`, `new()`, `hit_rate()` remain public @@ -132,12 +141,12 @@ - Carries only the version number, not the entire packet - Pattern matching must use `UnknownVersion(version)` instead of `UnknownVersion(packet)` -* **`get_source_stats()` on scoped parsers changed from `&self` to `&mut self`** +* **`get_source_info()` on scoped parsers changed from `&self` to `&mut self`** - LRU cache iteration requires mutable access - Code calling this from an immutable reference must switch to `&mut` * **`#[non_exhaustive]` added to public types** - - Affected types: `NetflowPacket`, `ParseResult`, `NetflowError`, `ConfigError`, `FieldValue`, `Config`, `PendingFlowsConfig`, `CacheStats`, `ParserCacheStats`, `CacheMetricsSnapshot`, `NoTemplateInfo`, `TemplateEvent`, `TemplateProtocol`, `ScopingInfo` + - Affected types: `NetflowPacket`, `ParseResult`, `NetflowError`, `ConfigError`, `FieldValue`, `Config`, `PendingFlowsConfig`, `CacheInfo`, `ParserCacheInfo`, `CacheMetrics`, `NoTemplateInfo`, `TemplateEvent`, `TemplateProtocol`, `ScopingInfo` - External code with exhaustive `match` statements must add a wildcard `_ =>` arm - External code constructing these structs directly must use `..` for forward compatibility @@ -202,7 +211,7 @@ - The crate contains zero `unsafe` blocks; this is now enforced at the crate level * **Expanded root re-exports** - - `Config`, `ConfigError`, `TtlConfig`, `EnterpriseFieldRegistry`, `CacheMetrics`, `CacheMetricsSnapshot`, `NoTemplateInfo`, `DEFAULT_MAX_RECORDS_PER_FLOWSET`, `DEFAULT_MAX_SOURCES` — now available at crate root + - `Config`, `ConfigError`, `TtlConfig`, `EnterpriseFieldRegistry`, `CacheMetrics`, `NoTemplateInfo`, `DEFAULT_MAX_RECORDS_PER_FLOWSET`, `DEFAULT_MAX_SOURCES` — now available at crate root - `DataNumber`, `FieldDataType`, `FieldValue` — commonly used field/data types at crate root - `V9Field`, `V9FieldPair`, `V9FlowRecord` — symmetric with IPFIX equivalents already at root @@ -460,18 +469,18 @@ * **`ConfigError`** gains an `InvalidPendingCacheSize(usize)` variant - Returned when `PendingFlowsConfig::max_pending_flows` is 0 - Exhaustive matches on `ConfigError` must add this arm -* **`CacheStats`** gains a `pending_flow_count: usize` field - - Code that destructures `CacheStats` must include the new field (or use `..`) -* **`CacheMetrics`** and **`CacheMetricsSnapshot`** gain four fields +* **`CacheInfo`** (formerly `CacheStats`) gains a `pending_flow_count: usize` field + - Code that destructures `CacheInfo` must include the new field (or use `..`) +* **`CacheMetrics`** gains four fields - `pending_cached`, `pending_replayed`, `pending_dropped`, `pending_replay_failed` - - Code that destructures either struct must include the new fields (or use `..`) + - Code that destructures the struct must include the new fields (or use `..`) # 0.8.4 ## Breaking Changes -* **Replaced tuple returns with named `ParserCacheStats` struct** - - Functions `get_source_stats()`, `all_stats()`, `ipfix_stats()`, `v9_stats()`, and `legacy_stats()` now return `ParserCacheStats` with `.v9` and `.ipfix` fields instead of `(CacheStats, CacheStats)` tuples +* **Replaced tuple returns with named `ParserCacheInfo` struct** (formerly `ParserCacheStats`) + - Functions `get_source_info()`, `all_info()`, `ipfix_info()`, `v9_info()`, and `legacy_info()` now return `ParserCacheInfo` with `.v9` and `.ipfix` fields instead of `(CacheInfo, CacheInfo)` tuples - This eliminates ambiguity about which positional element is V9 vs IPFIX - Migration: Replace `(key, v9_stats, ipfix_stats)` destructuring with `(key, stats)` and access `stats.v9` / `stats.ipfix` diff --git a/examples/multi_source_comparison.rs b/examples/multi_source_comparison.rs index b5fb6e7..68a3297 100644 --- a/examples/multi_source_comparison.rs +++ b/examples/multi_source_comparison.rs @@ -42,20 +42,20 @@ fn demo_single_parser(sources: &[&str]) { println!(" ✓ Parsed packet from router {} ({})", i + 1, source); } - let v9_stats = parser.v9_cache_stats(); - let ipfix_stats = parser.ipfix_cache_stats(); + let v9_info = parser.v9_cache_info(); + let ipfix_info = parser.ipfix_cache_info(); println!("\nCache Statistics:"); println!( " V9 Templates: {}/{}", - v9_stats.current_size, v9_stats.max_size_per_cache + v9_info.current_size, v9_info.max_size_per_cache ); println!( " IPFIX Templates: {}/{}", - ipfix_stats.current_size, ipfix_stats.max_size_per_cache + ipfix_info.current_size, ipfix_info.max_size_per_cache ); - println!(" V9 Collisions: {}", v9_stats.metrics.collisions); - println!(" IPFIX Collisions: {}", ipfix_stats.metrics.collisions); + println!(" V9 Collisions: {}", v9_info.metrics.collisions); + println!(" IPFIX Collisions: {}", ipfix_info.metrics.collisions); println!("\n⚠️ Problem: With V9/IPFIX templates, the same template ID"); println!(" from different routers would overwrite each other!"); diff --git a/examples/netflow_udp_listener_multi_threaded.rs b/examples/netflow_udp_listener_multi_threaded.rs index 8faa7e4..49bbea3 100644 --- a/examples/netflow_udp_listener_multi_threaded.rs +++ b/examples/netflow_udp_listener_multi_threaded.rs @@ -37,7 +37,7 @@ fn main() { if parser.source_count() > 0 { println!("\nPer-Source Template Cache Stats:"); - for (source, stats) in parser.all_stats() { + for (source, stats) in parser.all_info() { println!("\n Source: {}", source); // V9 stats diff --git a/examples/netflow_udp_listener_single_threaded.rs b/examples/netflow_udp_listener_single_threaded.rs index d5aedbd..0d8d1af 100644 --- a/examples/netflow_udp_listener_single_threaded.rs +++ b/examples/netflow_udp_listener_single_threaded.rs @@ -48,7 +48,7 @@ fn main() { if scoped_parser.source_count() > 0 { println!("\nPer-Source Template Cache Stats:"); - for (source, stats) in scoped_parser.all_stats() { + for (source, stats) in scoped_parser.all_info() { println!("\n Source: {}", source); // V9 stats diff --git a/examples/netflow_udp_listener_tokio.rs b/examples/netflow_udp_listener_tokio.rs index 40fdf9c..fb904fe 100644 --- a/examples/netflow_udp_listener_tokio.rs +++ b/examples/netflow_udp_listener_tokio.rs @@ -51,7 +51,7 @@ async fn main() -> io::Result<()> { println!(" Legacy sources: {}", parser_lock.legacy_source_count()); // Show IPFIX sources with RFC-compliant scoping - let ipfix_sources = parser_lock.ipfix_stats(); + let ipfix_sources = parser_lock.ipfix_info(); if !ipfix_sources.is_empty() { println!("\nIPFIX Sources (RFC 7011 scoping):"); for (key, stats) in ipfix_sources { @@ -80,7 +80,7 @@ async fn main() -> io::Result<()> { } // Show NetFlow v9 sources with RFC-compliant scoping - let v9_sources = parser_lock.v9_stats(); + let v9_sources = parser_lock.v9_info(); if !v9_sources.is_empty() { println!("\nNetFlow v9 Sources (RFC 3954 scoping):"); for (key, stats) in v9_sources { @@ -106,7 +106,7 @@ async fn main() -> io::Result<()> { } // Show legacy sources (v5/v7) - let legacy_sources = parser_lock.legacy_stats(); + let legacy_sources = parser_lock.legacy_info(); if !legacy_sources.is_empty() { println!("\nLegacy Sources (NetFlow v5/v7):"); for (addr, stats) in legacy_sources { diff --git a/examples/template_management_demo.rs b/examples/template_management_demo.rs index 781087e..28de311 100644 --- a/examples/template_management_demo.rs +++ b/examples/template_management_demo.rs @@ -51,20 +51,20 @@ fn demo_cache_metrics() { let _ = parser.parse_bytes(&dummy_data).packets; // Get cache statistics - let v9_stats = parser.v9_cache_stats(); - let _ipfix_stats = parser.ipfix_cache_stats(); + let v9_info = parser.v9_cache_info(); + let _ipfix_info = parser.ipfix_cache_info(); println!("\nV9 Cache Statistics:"); println!( " Current size: {}/{}", - v9_stats.current_size, v9_stats.max_size_per_cache + v9_info.current_size, v9_info.max_size_per_cache ); println!( " Utilization: {:.1}%", - (v9_stats.current_size as f64 / v9_stats.max_size_per_cache as f64) * 100.0 + (v9_info.current_size as f64 / v9_info.max_size_per_cache as f64) * 100.0 ); - let metrics = &v9_stats.metrics; + let metrics = &v9_info.metrics; println!("\nPerformance Metrics:"); println!(" Hits: {}", metrics.hits); println!(" Misses: {}", metrics.misses); @@ -115,7 +115,7 @@ fn demo_multi_source() { // Get statistics per source println!("\nPer-Source Statistics:"); - for (source, stats) in scoped_parser.all_stats() { + for (source, stats) in scoped_parser.all_info() { println!("\n Router: {}", source); println!( " V9 templates: {}/{}", @@ -149,14 +149,14 @@ fn demo_collision_detection() { let dummy_data = vec![0u8; 100]; let _ = parser.parse_bytes(&dummy_data).packets; - let v9_stats = parser.v9_cache_stats(); + let v9_info = parser.v9_cache_info(); println!("\nCollision Monitoring:"); - println!(" Total collisions: {}", v9_stats.metrics.collisions); + println!(" Total collisions: {}", v9_info.metrics.collisions); - if v9_stats.metrics.collisions > 0 { + if v9_info.metrics.collisions > 0 { let collision_rate = - v9_stats.metrics.collisions as f64 / v9_stats.metrics.insertions.max(1) as f64; + v9_info.metrics.collisions as f64 / v9_info.metrics.insertions.max(1) as f64; println!(" Collision rate: {:.2}%", collision_rate * 100.0); println!("\n⚠️ Recommendations:"); @@ -249,14 +249,14 @@ fn demo_template_lifecycle() { // Cache management println!("\nCache Management Operations:"); - let stats_before = parser.v9_cache_stats(); + let stats_before = parser.v9_cache_info(); println!(" Templates before clear: {}", stats_before.current_size); // Clear templates (useful for testing or forcing re-learning) parser.clear_v9_templates(); parser.clear_ipfix_templates(); - let stats_after = parser.v9_cache_stats(); + let stats_after = parser.v9_cache_info(); println!(" Templates after clear: {}", stats_after.current_size); println!("\nCache Configuration:"); diff --git a/src/lib.rs b/src/lib.rs index 4cd1048..5585f58 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,7 +50,7 @@ pub use template_events::{ // Re-export configuration and utility types for convenience pub use variable_versions::enterprise_registry::{EnterpriseFieldDef, EnterpriseFieldRegistry}; -pub use variable_versions::metrics::{CacheMetrics, CacheMetricsSnapshot}; +pub use variable_versions::metrics::{CacheInfo, CacheMetrics, ParserCacheInfo}; pub use variable_versions::ttl::TtlConfig; pub use variable_versions::{ Config, ConfigError, DEFAULT_MAX_RECORDS_PER_FLOWSET, NoTemplateInfo, PendingFlowsConfig, @@ -250,44 +250,6 @@ pub struct NetflowParser { template_hooks: TemplateHooks, } -/// Statistics about template cache utilization. -#[non_exhaustive] -#[derive(Debug, Clone)] -pub struct CacheStats { - /// Current number of cached templates (summed across all internal caches). - /// - /// This is the total across `num_caches` independent LRU caches. The theoretical - /// maximum is `max_size_per_cache * num_caches`, since each cache enforces - /// `max_size_per_cache` independently. - pub current_size: usize, - /// Maximum cache size per internal cache (each template type has its own LRU cache). - /// - /// Each of the `num_caches` internal caches can hold up to this many templates - /// independently. - pub max_size_per_cache: usize, - /// Number of internal caches (V9 has 2: templates + options; IPFIX has 4) - pub num_caches: usize, - /// TTL configuration (if enabled) - pub ttl_config: Option, - /// Performance metrics snapshot - pub metrics: variable_versions::metrics::CacheMetricsSnapshot, - /// Number of flows currently cached as pending (awaiting template) - pub pending_flow_count: usize, -} - -/// Combined cache statistics for both V9 and IPFIX template caches. -/// -/// This struct provides named fields instead of positional tuples, -/// making it clear which stats belong to V9 vs IPFIX. -#[non_exhaustive] -#[derive(Debug, Clone)] -pub struct ParserCacheStats { - /// V9 template cache statistics - pub v9: CacheStats, - /// IPFIX template cache statistics - pub ipfix: CacheStats, -} - /// Builder for configuring and constructing a [`NetflowParser`]. /// /// # Examples @@ -1199,11 +1161,11 @@ impl NetflowParser { /// use netflow_parser::NetflowParser; /// /// let parser = NetflowParser::default(); - /// let stats = parser.v9_cache_stats(); + /// let stats = parser.v9_cache_info(); /// println!("V9 cache: {}/{} templates", stats.current_size, stats.max_size_per_cache); /// ``` - pub fn v9_cache_stats(&self) -> CacheStats { - CacheStats { + pub fn v9_cache_info(&self) -> CacheInfo { + CacheInfo { current_size: count_valid_templates( &self.v9_parser.templates, &self.v9_parser.ttl_config, @@ -1227,12 +1189,12 @@ impl NetflowParser { /// use netflow_parser::NetflowParser; /// /// let parser = NetflowParser::default(); - /// let stats = parser.ipfix_cache_stats(); + /// let stats = parser.ipfix_cache_info(); /// println!("IPFIX cache: {}/{} templates", stats.current_size, stats.max_size_per_cache); /// ``` - pub fn ipfix_cache_stats(&self) -> CacheStats { + pub fn ipfix_cache_info(&self) -> CacheInfo { let ttl = &self.ipfix_parser.ttl_config; - CacheStats { + CacheInfo { current_size: count_valid_templates(&self.ipfix_parser.templates, ttl) + count_valid_templates(&self.ipfix_parser.v9_templates, ttl) + count_valid_templates(&self.ipfix_parser.ipfix_options_templates, ttl) @@ -1657,8 +1619,8 @@ impl NetflowParser { fn fire_metric_delta_events( &mut self, - before: &variable_versions::metrics::CacheMetrics, - after: &variable_versions::metrics::CacheMetrics, + before: &variable_versions::metrics::CacheMetricsInner, + after: &variable_versions::metrics::CacheMetricsInner, protocol: TemplateProtocol, ) { // Cap events per type per parse call to prevent hook amplification from diff --git a/src/scoped_parser.rs b/src/scoped_parser.rs index 01ab6f7..e499ce6 100644 --- a/src/scoped_parser.rs +++ b/src/scoped_parser.rs @@ -5,7 +5,7 @@ use crate::{ ConfigError, NetflowError, NetflowPacket, NetflowParser, NetflowParserBuilder, ParseResult, - ParserCacheStats, + ParserCacheInfo, }; use lru::LruCache; use std::hash::Hash; @@ -289,12 +289,12 @@ impl RouterScopedParser { /// Get statistics for a specific source's template cache. /// /// Returns `None` if the source hasn't sent any packets yet. - pub fn get_source_stats(&mut self, source: &K) -> Option { + pub fn get_source_info(&mut self, source: &K) -> Option { self.parsers .peek(source) - .map(|(parser, _)| ParserCacheStats { - v9: parser.v9_cache_stats(), - ipfix: parser.ipfix_cache_stats(), + .map(|(parser, _)| ParserCacheInfo { + v9: parser.v9_cache_info(), + ipfix: parser.ipfix_cache_info(), }) } @@ -309,15 +309,15 @@ impl RouterScopedParser { } /// Get statistics for all sources. - pub fn all_stats(&self) -> Vec<(&K, ParserCacheStats)> { + pub fn all_info(&self) -> Vec<(&K, ParserCacheInfo)> { self.parsers .iter() .map(|(source, (parser, _))| { ( source, - ParserCacheStats { - v9: parser.v9_cache_stats(), - ipfix: parser.ipfix_cache_stats(), + ParserCacheInfo { + v9: parser.v9_cache_info(), + ipfix: parser.ipfix_cache_info(), }, ) }) @@ -783,15 +783,15 @@ impl AutoScopedParser { } /// Get statistics for all IPFIX sources. - pub fn ipfix_stats(&self) -> Vec<(&IpfixSourceKey, ParserCacheStats)> { + pub fn ipfix_info(&self) -> Vec<(&IpfixSourceKey, ParserCacheInfo)> { self.ipfix_parsers .iter() .map(|(key, (parser, _))| { ( key, - ParserCacheStats { - v9: parser.v9_cache_stats(), - ipfix: parser.ipfix_cache_stats(), + ParserCacheInfo { + v9: parser.v9_cache_info(), + ipfix: parser.ipfix_cache_info(), }, ) }) @@ -799,15 +799,15 @@ impl AutoScopedParser { } /// Get statistics for all NetFlow v9 sources. - pub fn v9_stats(&self) -> Vec<(&V9SourceKey, ParserCacheStats)> { + pub fn v9_info(&self) -> Vec<(&V9SourceKey, ParserCacheInfo)> { self.v9_parsers .iter() .map(|(key, (parser, _))| { ( key, - ParserCacheStats { - v9: parser.v9_cache_stats(), - ipfix: parser.ipfix_cache_stats(), + ParserCacheInfo { + v9: parser.v9_cache_info(), + ipfix: parser.ipfix_cache_info(), }, ) }) @@ -815,15 +815,15 @@ impl AutoScopedParser { } /// Get statistics for all legacy sources. - pub fn legacy_stats(&self) -> Vec<(&SocketAddr, ParserCacheStats)> { + pub fn legacy_info(&self) -> Vec<(&SocketAddr, ParserCacheInfo)> { self.legacy_parsers .iter() .map(|(addr, (parser, _))| { ( addr, - ParserCacheStats { - v9: parser.v9_cache_stats(), - ipfix: parser.ipfix_cache_stats(), + ParserCacheInfo { + v9: parser.v9_cache_info(), + ipfix: parser.ipfix_cache_info(), }, ) }) @@ -995,7 +995,7 @@ mod tests { // Now we have one source assert_eq!(scoped.source_count(), 1); - assert!(scoped.get_source_stats(&source1).is_some()); + assert!(scoped.get_source_info(&source1).is_some()); // Parse from second source let source2: SocketAddr = "192.168.1.2:2055".parse().unwrap(); @@ -1003,7 +1003,7 @@ mod tests { // Now we have two sources assert_eq!(scoped.source_count(), 2); - assert!(scoped.get_source_stats(&source2).is_some()); + assert!(scoped.get_source_info(&source2).is_some()); } // Verify RouterScopedParser works with String keys @@ -1208,9 +1208,9 @@ mod tests { let _ = parser.parse_from_source(source, &ipfix_data); // Get stats - let ipfix_stats = parser.ipfix_stats(); - assert_eq!(ipfix_stats.len(), 1); - assert_eq!(ipfix_stats[0].0.observation_domain_id, 1); + let ipfix_info = parser.ipfix_info(); + assert_eq!(ipfix_info.len(), 1); + assert_eq!(ipfix_info[0].0.observation_domain_id, 1); } // Verify clear_all_templates preserves parser instances but clears caches diff --git a/src/variable_versions/field_value.rs b/src/variable_versions/field_value.rs index 70cb907..4723168 100644 --- a/src/variable_versions/field_value.rs +++ b/src/variable_versions/field_value.rs @@ -374,7 +374,7 @@ impl DataNumber { } } -#[derive(Debug, PartialEq, PartialOrd, Clone, Serialize)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Serialize)] pub struct ApplicationId { pub classification_engine_id: u8, pub selector_id: Option, @@ -403,9 +403,17 @@ impl PartialEq for DurationValue { } } +impl Eq for DurationValue {} + +impl Ord for DurationValue { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.as_duration().cmp(&other.as_duration()) + } +} + impl PartialOrd for DurationValue { fn partial_cmp(&self, other: &Self) -> Option { - Some(self.as_duration().cmp(&other.as_duration())) + Some(self.cmp(other)) } } diff --git a/src/variable_versions/ipfix/parser.rs b/src/variable_versions/ipfix/parser.rs index b7d7a80..bf37044 100644 --- a/src/variable_versions/ipfix/parser.rs +++ b/src/variable_versions/ipfix/parser.rs @@ -13,7 +13,7 @@ use super::{ use crate::variable_versions::config::DEFAULT_MAX_RECORDS_PER_FLOWSET; use crate::variable_versions::enterprise_registry::EnterpriseFieldRegistry; use crate::variable_versions::field_value::FieldValue; -use crate::variable_versions::metrics::CacheMetrics; +use crate::variable_versions::metrics::CacheMetricsInner; use crate::variable_versions::ttl::{TemplateWithTtl, TtlConfig}; use crate::variable_versions::v9::{ DATA_TEMPLATE_V9_ID, Data as V9Data, OPTIONS_TEMPLATE_V9_ID, OptionsData as V9OptionsData, @@ -90,7 +90,7 @@ impl IPFixParser { max_error_sample_size: config.max_error_sample_size, max_records_per_flowset: config.max_records_per_flowset, enterprise_registry: config.enterprise_registry, - metrics: CacheMetrics::new(), + metrics: CacheMetricsInner::new(), pending_flows, }) } @@ -199,7 +199,7 @@ impl IPFixParser { fn cache_notemplate_ipfix_flowsets( ipfix: &mut IPFix, cache: &mut PendingFlowCache, - metrics: &mut CacheMetrics, + metrics: &mut CacheMetricsInner, max_error_sample_size: usize, ) -> Vec { let mut learned_template_ids: Vec = Vec::new(); @@ -501,7 +501,7 @@ fn insert_templates( cache: &mut LruCache>>, templates: &[T], ttl_enabled: bool, - metrics: &mut CacheMetrics, + metrics: &mut CacheMetricsInner, ) { for t in templates { let arc_template = Arc::new(t.clone()); diff --git a/src/variable_versions/ipfix/types.rs b/src/variable_versions/ipfix/types.rs index 75499d5..7b66328 100644 --- a/src/variable_versions/ipfix/types.rs +++ b/src/variable_versions/ipfix/types.rs @@ -8,7 +8,7 @@ use super::lookup::IPFixField; use crate::variable_versions::PendingFlowCache; use crate::variable_versions::enterprise_registry::EnterpriseFieldRegistry; use crate::variable_versions::field_value::FieldValue; -use crate::variable_versions::metrics::CacheMetrics; +use crate::variable_versions::metrics::CacheMetricsInner; use crate::variable_versions::ttl::{TemplateWithTtl, TtlConfig}; use nom::bytes::complete::take; @@ -55,7 +55,7 @@ pub struct IPFixParser { pub(crate) max_error_sample_size: usize, pub(crate) max_records_per_flowset: usize, pub(crate) enterprise_registry: Arc, - pub(crate) metrics: CacheMetrics, + pub(crate) metrics: CacheMetricsInner, pub(crate) pending_flows: Option, } diff --git a/src/variable_versions/metrics.rs b/src/variable_versions/metrics.rs index 1785085..c636966 100644 --- a/src/variable_versions/metrics.rs +++ b/src/variable_versions/metrics.rs @@ -1,11 +1,13 @@ -//! Template cache metrics for monitoring parser performance +//! Template cache metrics and cache information types for monitoring parser performance. + +use super::ttl::TtlConfig; /// Metrics for tracking template cache performance. /// /// All counters use plain u64 fields. The parser itself is not thread-safe /// and should not be shared across threads. #[derive(Debug, Default, Clone, Copy)] -pub struct CacheMetrics { +pub(crate) struct CacheMetricsInner { /// Number of successful template lookups (cache hits) pub hits: u64, /// Number of failed template lookups (cache misses) @@ -28,7 +30,7 @@ pub struct CacheMetrics { pub pending_replay_failed: u64, } -impl CacheMetrics { +impl CacheMetricsInner { /// Create a new metrics instance with all counters at zero pub fn new() -> Self { Self::default() @@ -101,8 +103,8 @@ impl CacheMetrics { } /// Get a snapshot of current metrics - pub fn snapshot(&self) -> CacheMetricsSnapshot { - CacheMetricsSnapshot { + pub fn snapshot(&self) -> CacheMetrics { + CacheMetrics { hits: self.hits, misses: self.misses, evictions: self.evictions, @@ -123,7 +125,7 @@ impl CacheMetrics { /// for each field access. #[non_exhaustive] #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct CacheMetricsSnapshot { +pub struct CacheMetrics { /// Number of successful template lookups (cache hits) pub hits: u64, /// Number of failed template lookups (cache misses) @@ -146,7 +148,7 @@ pub struct CacheMetricsSnapshot { pub pending_replay_failed: u64, } -impl CacheMetricsSnapshot { +impl CacheMetrics { /// Calculate the cache hit rate (0.0 to 1.0) /// /// Returns `None` if there have been no lookups yet. @@ -172,6 +174,44 @@ impl CacheMetricsSnapshot { } } +/// Statistics about template cache utilization. +#[non_exhaustive] +#[derive(Debug, Clone)] +pub struct CacheInfo { + /// Current number of cached templates (summed across all internal caches). + /// + /// This is the total across `num_caches` independent LRU caches. The theoretical + /// maximum is `max_size_per_cache * num_caches`, since each cache enforces + /// `max_size_per_cache` independently. + pub current_size: usize, + /// Maximum cache size per internal cache (each template type has its own LRU cache). + /// + /// Each of the `num_caches` internal caches can hold up to this many templates + /// independently. + pub max_size_per_cache: usize, + /// Number of internal caches (V9 has 2: templates + options; IPFIX has 4) + pub num_caches: usize, + /// TTL configuration (if enabled) + pub ttl_config: Option, + /// Performance metrics snapshot + pub metrics: CacheMetrics, + /// Number of flows currently cached as pending (awaiting template) + pub pending_flow_count: usize, +} + +/// Combined cache information for both V9 and IPFIX template caches. +/// +/// This struct provides named fields instead of positional tuples, +/// making it clear which info belongs to V9 vs IPFIX. +#[non_exhaustive] +#[derive(Debug, Clone)] +pub struct ParserCacheInfo { + /// V9 template cache information + pub v9: CacheInfo, + /// IPFIX template cache information + pub ipfix: CacheInfo, +} + #[cfg(test)] mod tests { use super::*; @@ -179,7 +219,7 @@ mod tests { // Verify each record method increments the correct counter in the snapshot #[test] fn test_metrics_recording() { - let mut metrics = CacheMetrics::new(); + let mut metrics = CacheMetricsInner::new(); metrics.record_hit(); metrics.record_hit(); @@ -199,7 +239,7 @@ mod tests { // Verify hit_rate and miss_rate calculations, including None when no lookups exist #[test] fn test_hit_rate() { - let mut metrics = CacheMetrics::new(); + let mut metrics = CacheMetricsInner::new(); // No lookups yet let snapshot = metrics.snapshot(); diff --git a/src/variable_versions/mod.rs b/src/variable_versions/mod.rs index b4140fc..c2d0869 100644 --- a/src/variable_versions/mod.rs +++ b/src/variable_versions/mod.rs @@ -94,7 +94,7 @@ pub use pending_flows::PendingFlowsConfig; pub(crate) use config::TemplateId; pub(crate) use pending_flows::{PendingFlowCache, PendingFlowEntry}; -use crate::variable_versions::metrics::CacheMetrics; +use crate::variable_versions::metrics::CacheMetricsInner; use crate::variable_versions::ttl::TtlConfig; /// Information about a data flowset that couldn't be parsed due to missing template. @@ -149,7 +149,7 @@ pub(crate) fn get_valid_template( cache: &mut lru::LruCache>>, id: &TemplateId, ttl_config: &Option, - metrics: &mut CacheMetrics, + metrics: &mut CacheMetricsInner, ) -> Option> { if let Some(wrapped) = cache.peek(id) { if let Some(config) = ttl_config @@ -175,7 +175,7 @@ pub(crate) fn peek_valid_template( cache: &mut lru::LruCache>>, id: &TemplateId, ttl_config: &Option, - metrics: &mut CacheMetrics, + metrics: &mut CacheMetricsInner, ) -> Option> { if let Some(wrapped) = cache.peek(id) { if let Some(config) = ttl_config diff --git a/src/variable_versions/pending_flows.rs b/src/variable_versions/pending_flows.rs index 624cfaf..23a5cbb 100644 --- a/src/variable_versions/pending_flows.rs +++ b/src/variable_versions/pending_flows.rs @@ -5,7 +5,7 @@ //! are drained and re-parsed. use super::config::ConfigError; -use super::metrics::CacheMetrics; +use super::metrics::CacheMetricsInner; use lru::LruCache; use std::num::NonZeroUsize; use std::time::{Duration, Instant}; @@ -217,7 +217,7 @@ impl PendingFlowCache { &mut self, template_id: u16, raw_data: Vec, - metrics: &mut CacheMetrics, + metrics: &mut CacheMetricsInner, ) -> Option> { if raw_data.len() > self.config.max_entry_size_bytes { metrics.record_pending_dropped(); @@ -306,7 +306,7 @@ impl PendingFlowCache { pub(crate) fn drain( &mut self, template_id: u16, - metrics: &mut CacheMetrics, + metrics: &mut CacheMetricsInner, ) -> Vec { let Some(entries) = self.cache.pop(&template_id) else { return Vec::new(); @@ -336,7 +336,11 @@ impl PendingFlowCache { /// Remove expired entries from a single template's vector. /// If all entries expire, the key is removed from the cache. - fn prune_expired_for_template(&mut self, template_id: u16, metrics: &mut CacheMetrics) { + fn prune_expired_for_template( + &mut self, + template_id: u16, + metrics: &mut CacheMetricsInner, + ) { let Some(ttl) = self.config.ttl else { return }; // Use peek_mut so pruning alone doesn't promote the key. let result = self @@ -353,7 +357,7 @@ impl PendingFlowCache { /// Remove expired entries from every template in the cache. /// Empty keys are removed so their slots can be reused. - fn purge_expired(&mut self, metrics: &mut CacheMetrics) { + fn purge_expired(&mut self, metrics: &mut CacheMetricsInner) { let Some(ttl) = self.config.ttl else { return }; let keys: Vec = self.cache.iter().map(|(&k, _)| k).collect(); for key in keys { @@ -376,7 +380,7 @@ impl PendingFlowCache { fn drop_expired_entries( entries: &mut Vec, ttl: Duration, - metrics: &mut CacheMetrics, + metrics: &mut CacheMetricsInner, ) -> (bool, usize) { let before_len = entries.len(); let before_bytes: usize = entries.iter().map(|e| e.raw_data.len()).sum(); @@ -426,7 +430,7 @@ impl PendingFlowCache { pub(crate) fn resize( &mut self, config: PendingFlowsConfig, - metrics: &mut CacheMetrics, + metrics: &mut CacheMetricsInner, ) -> Result { // Validate the full config, not just max_pending_flows Self::validate_config(&config)?; @@ -478,7 +482,7 @@ impl PendingFlowCache { fn trim_existing_entries( cache: &mut LruCache>, config: &PendingFlowsConfig, - metrics: &mut CacheMetrics, + metrics: &mut CacheMetricsInner, ) -> (usize, u64) { let mut freed = 0usize; let mut dropped: u64 = 0; @@ -521,10 +525,10 @@ impl PendingFlowCache { #[cfg(test)] mod tests { use super::*; - use crate::variable_versions::metrics::CacheMetrics; + use crate::variable_versions::metrics::CacheMetricsInner; - fn default_metrics() -> CacheMetrics { - CacheMetrics::default() + fn default_metrics() -> CacheMetricsInner { + CacheMetricsInner::default() } fn small_config(max_flows: usize) -> PendingFlowsConfig { diff --git a/src/variable_versions/v9/parser.rs b/src/variable_versions/v9/parser.rs index 25c86b2..ca7be12 100644 --- a/src/variable_versions/v9/parser.rs +++ b/src/variable_versions/v9/parser.rs @@ -15,7 +15,7 @@ use super::{ use crate::variable_versions::config::DEFAULT_MAX_RECORDS_PER_FLOWSET; use crate::variable_versions::enterprise_registry::EnterpriseFieldRegistry; use crate::variable_versions::field_value::FieldValue; -use crate::variable_versions::metrics::CacheMetrics; +use crate::variable_versions::metrics::CacheMetricsInner; use crate::variable_versions::ttl::{TemplateWithTtl, TtlConfig}; use crate::variable_versions::{ Config, ConfigError, ParserConfig, ParserFields, PendingFlowCache, PendingFlowEntry, @@ -42,7 +42,7 @@ pub struct V9Parser { pub(crate) max_template_total_size: usize, pub(crate) max_error_sample_size: usize, pub(crate) max_records_per_flowset: usize, - pub(crate) metrics: CacheMetrics, + pub(crate) metrics: CacheMetricsInner, pub(crate) pending_flows: Option, } @@ -99,7 +99,7 @@ impl V9Parser { max_template_total_size: config.max_template_total_size, max_error_sample_size: config.max_error_sample_size, max_records_per_flowset: config.max_records_per_flowset, - metrics: CacheMetrics::new(), + metrics: CacheMetricsInner::new(), pending_flows, }) } @@ -203,7 +203,7 @@ impl V9Parser { fn cache_notemplate_v9_flowsets( v9: &mut V9, cache: &mut PendingFlowCache, - metrics: &mut CacheMetrics, + metrics: &mut CacheMetricsInner, max_error_sample_size: usize, ) -> Vec { let mut learned_template_ids: Vec = Vec::new(); diff --git a/tests/cache_collision.rs b/tests/cache_collision.rs index 3e85fb4..3fb5cfd 100644 --- a/tests/cache_collision.rs +++ b/tests/cache_collision.rs @@ -81,13 +81,13 @@ fn test_cache_hit_and_miss_tracking() { ]; let _ = parser.parse_bytes(&v9_data_packet); - let v9_stats = parser.v9_cache_stats(); + let v9_info = parser.v9_cache_info(); assert_eq!( - v9_stats.metrics.hits, 1, + v9_info.metrics.hits, 1, "V9 cache should record exactly 1 hit after parsing data with a cached template" ); assert_eq!( - v9_stats.metrics.misses, 0, + v9_info.metrics.misses, 0, "V9 cache should have 0 misses when template is present" ); @@ -100,9 +100,9 @@ fn test_cache_hit_and_miss_tracking() { ]; let _ = parser.parse_bytes(&v9_missing_template_packet); - let v9_stats = parser.v9_cache_stats(); + let v9_info = parser.v9_cache_info(); assert_eq!( - v9_stats.metrics.misses, 1, + v9_info.metrics.misses, 1, "V9 cache should record 1 miss for unknown template" ); } diff --git a/tests/dos_edge_cases.rs b/tests/dos_edge_cases.rs index f854d3a..46eb8f4 100644 --- a/tests/dos_edge_cases.rs +++ b/tests/dos_edge_cases.rs @@ -133,7 +133,7 @@ fn test_template_cache_eviction() { } // Check cache stats - should show evictions occurred - let stats = parser.v9_cache_stats(); + let stats = parser.v9_cache_info(); assert_eq!(stats.current_size, 5, "Cache should be at max size"); assert_eq!(stats.max_size_per_cache, 5, "Max size should be 5"); @@ -214,8 +214,9 @@ fn test_error_buffer_size_configuration() { println!("Got Partial error (acceptable)"); } Some(other) => { - // Other error types are acceptable as long as parser doesn't panic - println!("Got other error type: {:?}", other); + // Other error types are acceptable as long as parser doesn't panic. + // The error sample is bounded by the error type itself (no raw buffer). + panic!("Unexpected error type for malformed V9 packet: {:?}", other); } None => panic!("Expected error on malformed packet"), } @@ -334,7 +335,7 @@ fn test_cache_metrics_accuracy() { let _ = parser.parse_bytes(&template); - let initial_stats = parser.v9_cache_stats(); + let initial_stats = parser.v9_cache_info(); let initial_hits = initial_stats.metrics.hits; // Use the template 5 times (should increment hits) @@ -346,7 +347,7 @@ fn test_cache_metrics_accuracy() { let _ = parser.parse_bytes(&data); } - let final_stats = parser.v9_cache_stats(); + let final_stats = parser.v9_cache_info(); let final_hits = final_stats.metrics.hits; // Should have 5 more hits diff --git a/tests/memory_bounds.rs b/tests/memory_bounds.rs index 86abd9d..4874306 100644 --- a/tests/memory_bounds.rs +++ b/tests/memory_bounds.rs @@ -6,7 +6,7 @@ use netflow_parser::NetflowParser; /// Verify that V9 template cache stays within configured bounds when many /// distinct templates are inserted. #[test] -fn test_cache_stats_stay_within_bounds() { +fn test_cache_info_stay_within_bounds() { let max_cache = 3; let mut parser = NetflowParser::builder() .with_cache_size(max_cache) @@ -36,16 +36,11 @@ fn test_cache_stats_stay_within_bounds() { let _ = parser.parse_bytes(&v9_template_packet); } - let v9_stats = parser.v9_cache_stats(); - assert!( - v9_stats.current_size <= max_cache, - "V9 cache size {} exceeds max {}", - v9_stats.current_size, - max_cache - ); - assert!( - v9_stats.current_size > 0, - "V9 cache should have entries after parsing templates" + let v9_info = parser.v9_cache_info(); + assert_eq!( + v9_info.current_size, max_cache, + "V9 cache should be full at max capacity {}, got {}", + max_cache, v9_info.current_size ); } diff --git a/tests/multi_version_parsing.rs b/tests/multi_version_parsing.rs index 93c9c53..ad0bcf2 100644 --- a/tests/multi_version_parsing.rs +++ b/tests/multi_version_parsing.rs @@ -135,3 +135,64 @@ fn test_iterator_filter() { assert_eq!(count, 1); } + +// --------------------------------------------------------------------------- +// parse_bytes_as_netflow_common_flowsets +// --------------------------------------------------------------------------- + +/// Verify parse_bytes_as_netflow_common_flowsets converts V5 packets correctly. +#[cfg(feature = "netflow_common")] +#[test] +fn test_parse_bytes_as_netflow_common_v5() { + let mut parser = NetflowParser::default(); + + // Minimal V5 packet: header(24) + 1 flow(48) = 72 bytes + let v5_packet: Vec = vec![ + 0, 5, // version + 0, 1, // count = 1 + 0, 0, 0, 0, // sys_uptime + 0, 0, 0, 0, // unix_secs + 0, 0, 0, 0, // unix_nsecs + 0, 0, 0, 0, // flow_sequence + 0, // engine_type + 0, // engine_id + 0, 0, // sampling_interval + // Flow record (48 bytes) + 192, 168, 1, 1, // src_addr + 10, 0, 0, 1, // dst_addr + 0, 0, 0, 0, // next_hop + 0, 0, // input + 0, 0, // output + 0, 0, 0, 100, // packets + 0, 0, 0, 200, // octets + 0, 0, 0, 0, // first + 0, 0, 0, 0, // last + 0, 80, // src_port = 80 + 0x1F, 0x90, // dst_port = 8080 + 0, // pad1 + 0, // tcp_flags + 6, // protocol = TCP + 0, // tos + 0, 0, // src_as + 0, 0, // dst_as + 0, // src_mask + 0, // dst_mask + 0, 0, // pad2 + ]; + + let (flowsets, error) = parser.parse_bytes_as_netflow_common_flowsets(&v5_packet); + assert!(error.is_none(), "V5 parse should succeed"); + assert_eq!(flowsets.len(), 1, "Should produce 1 common flowset from V5"); +} + +/// Verify parse_bytes_as_netflow_common_flowsets handles errors gracefully. +#[cfg(feature = "netflow_common")] +#[test] +fn test_parse_bytes_as_netflow_common_error() { + let mut parser = NetflowParser::default(); + + let garbage = vec![0xFF; 10]; + let (flowsets, error) = parser.parse_bytes_as_netflow_common_flowsets(&garbage); + assert!(error.is_some(), "Garbage input should produce error"); + assert!(flowsets.is_empty(), "No flowsets from garbage input"); +} diff --git a/tests/parser_configuration.rs b/tests/parser_configuration.rs index 584d528..f490ebc 100644 --- a/tests/parser_configuration.rs +++ b/tests/parser_configuration.rs @@ -25,11 +25,11 @@ fn test_parser_builder_with_cache_size() { .build() .expect("Failed to build parser"); - let stats = parser.v9_cache_stats(); + let stats = parser.v9_cache_info(); assert_eq!(stats.max_size_per_cache, 2000); - let ipfix_stats = parser.ipfix_cache_stats(); - assert_eq!(ipfix_stats.max_size_per_cache, 2000); + let ipfix_info = parser.ipfix_cache_info(); + assert_eq!(ipfix_info.max_size_per_cache, 2000); } // Verify builder can set independent cache sizes for V9 and IPFIX @@ -41,11 +41,11 @@ fn test_parser_builder_with_different_cache_sizes() { .build() .expect("Failed to build parser"); - let v9_stats = parser.v9_cache_stats(); - assert_eq!(v9_stats.max_size_per_cache, 1000); + let v9_info = parser.v9_cache_info(); + assert_eq!(v9_info.max_size_per_cache, 1000); - let ipfix_stats = parser.ipfix_cache_stats(); - assert_eq!(ipfix_stats.max_size_per_cache, 3000); + let ipfix_info = parser.ipfix_cache_info(); + assert_eq!(ipfix_info.max_size_per_cache, 3000); } // Verify builder accepts a TTL configuration and creates a valid parser @@ -163,11 +163,11 @@ fn test_parser_builder_comprehensive() { .build() .expect("Failed to build parser"); - let v9_stats = parser.v9_cache_stats(); - assert_eq!(v9_stats.max_size_per_cache, 1500); + let v9_info = parser.v9_cache_info(); + assert_eq!(v9_info.max_size_per_cache, 1500); - let ipfix_stats = parser.ipfix_cache_stats(); - assert_eq!(ipfix_stats.max_size_per_cache, 2500); + let ipfix_info = parser.ipfix_cache_info(); + assert_eq!(ipfix_info.max_size_per_cache, 2500); assert!(parser.allowed_versions()[5]); assert!(parser.allowed_versions()[9]); diff --git a/tests/pcap_integration.rs b/tests/pcap_integration.rs index 1803565..fd2f39d 100644 --- a/tests/pcap_integration.rs +++ b/tests/pcap_integration.rs @@ -128,12 +128,12 @@ fn test_pcap_template_caching() { } // Check that templates were cached - let ipfix_stats = parser.ipfix_cache_stats(); + let ipfix_info = parser.ipfix_cache_info(); let template_ids = parser.ipfix_template_ids(); // IPFIX.pcap should have templates assert!( - ipfix_stats.current_size > 0, + ipfix_info.current_size > 0, "IPFIX pcap should produce cached templates" ); assert!(!template_ids.is_empty(), "Should have cached template IDs"); @@ -172,12 +172,12 @@ fn test_pcap_cache_metrics() { } // Check cache metrics — verify we actually parsed IPFIX data from the pcap - let ipfix_stats = parser.ipfix_cache_stats(); - let metrics = &ipfix_stats.metrics; + let ipfix_info = parser.ipfix_cache_info(); + let metrics = &ipfix_info.metrics; // The test pcap should contain IPFIX templates, so the cache should be populated. assert!( - ipfix_stats.current_size > 0, + ipfix_info.current_size > 0, "PCAP should contain IPFIX templates that get cached" ); let total_lookups = metrics.hits + metrics.misses; diff --git a/tests/pending_flows.rs b/tests/pending_flows.rs index dc2138d..91ff29d 100644 --- a/tests/pending_flows.rs +++ b/tests/pending_flows.rs @@ -96,7 +96,7 @@ fn test_pending_flows_disabled_by_default() { ); // No pending flows should be cached (feature disabled) - let stats = parser.v9_cache_stats(); + let stats = parser.v9_cache_info(); assert_eq!( stats.pending_flow_count, 0, "No pending flows when feature is disabled" @@ -120,7 +120,7 @@ fn test_ipfix_pending_flow_replay() { assert!(result1.is_ok(), "Data packet should parse (NoTemplate)"); // Should have one pending flow cached - let stats = parser.ipfix_cache_stats(); + let stats = parser.ipfix_cache_info(); assert_eq!(stats.pending_flow_count, 1, "Should cache one pending flow"); assert_eq!( stats.metrics.pending_cached, 1, @@ -150,7 +150,7 @@ fn test_ipfix_pending_flow_replay() { ); // Pending flows should now be drained - let stats = parser.ipfix_cache_stats(); + let stats = parser.ipfix_cache_info(); assert_eq!( stats.pending_flow_count, 0, "Pending flows should be drained after replay" @@ -174,7 +174,7 @@ fn test_v9_pending_flow_replay() { assert!(result1.is_ok(), "Data packet should parse (NoTemplate)"); // Should have one pending flow cached - let stats = parser.v9_cache_stats(); + let stats = parser.v9_cache_info(); assert_eq!(stats.pending_flow_count, 1, "Should cache one pending flow"); // Step 2: Send template packet - pending flows should be replayed @@ -200,7 +200,7 @@ fn test_v9_pending_flow_replay() { ); // Pending flows should now be drained - let stats = parser.v9_cache_stats(); + let stats = parser.v9_cache_info(); assert_eq!( stats.pending_flow_count, 0, "Pending flows should be drained after replay" @@ -221,14 +221,14 @@ fn test_pending_flow_ttl_expiration() { // Send data packet before template let _ = parser.parse_bytes(&v9_data_packet()); - assert_eq!(parser.v9_cache_stats().pending_flow_count, 1); + assert_eq!(parser.v9_cache_info().pending_flow_count, 1); // Send template - pending flow should have already expired (TTL is zero) let result = parser.parse_bytes(&v9_template_packet()); assert!(result.is_ok()); // The pending flow should be dropped, not replayed - let stats = parser.v9_cache_stats(); + let stats = parser.v9_cache_info(); assert_eq!( stats.metrics.pending_replayed, 0, "Expired flow should not be replayed" @@ -263,7 +263,7 @@ fn test_pending_flow_lru_eviction() { let _ = parser.parse_bytes(&data_258); // Cache should have exactly 2 entries (257 and 258); 256 was evicted - let stats = parser.ipfix_cache_stats(); + let stats = parser.ipfix_cache_info(); assert_eq!(stats.pending_flow_count, 2); assert_eq!(stats.metrics.pending_cached, 3); // The evicted template 256 had 1 entry, which should be counted as dropped @@ -415,17 +415,17 @@ fn test_clear_pending_flows() { let _ = parser.parse_bytes(&v9_data_packet()); let _ = parser.parse_bytes(&ipfix_data_packet()); - assert!(parser.v9_cache_stats().pending_flow_count > 0); - assert!(parser.ipfix_cache_stats().pending_flow_count > 0); + assert_eq!(parser.v9_cache_info().pending_flow_count, 1); + assert_eq!(parser.ipfix_cache_info().pending_flow_count, 1); // Clear V9 pending flows parser.clear_v9_pending_flows(); - assert_eq!(parser.v9_cache_stats().pending_flow_count, 0); - assert!(parser.ipfix_cache_stats().pending_flow_count > 0); + assert_eq!(parser.v9_cache_info().pending_flow_count, 0); + assert_eq!(parser.ipfix_cache_info().pending_flow_count, 1); // Clear IPFIX pending flows parser.clear_ipfix_pending_flows(); - assert_eq!(parser.ipfix_cache_stats().pending_flow_count, 0); + assert_eq!(parser.ipfix_cache_info().pending_flow_count, 0); } // --------------------------------------------------------------------------- @@ -676,7 +676,7 @@ fn test_v9_multifield_pending_replay() { // Data before template let result1 = parser.parse_bytes(&v9_multifield_data_packet()); assert!(result1.is_ok()); - assert_eq!(parser.v9_cache_stats().pending_flow_count, 1); + assert_eq!(parser.v9_cache_info().pending_flow_count, 1); // Template arrives -> replay let result2 = parser.parse_bytes(&v9_multifield_template_packet()); @@ -696,7 +696,7 @@ fn test_v9_multifield_pending_replay() { }); assert!(has_data, "Replayed multi-field data should appear"); - let stats = parser.v9_cache_stats(); + let stats = parser.v9_cache_info(); assert_eq!(stats.pending_flow_count, 0); assert_eq!(stats.metrics.pending_replayed, 1); } @@ -711,7 +711,7 @@ fn test_ipfix_multifield_pending_replay() { let result1 = parser.parse_bytes(&ipfix_multifield_data_packet()); assert!(result1.is_ok()); - assert_eq!(parser.ipfix_cache_stats().pending_flow_count, 1); + assert_eq!(parser.ipfix_cache_info().pending_flow_count, 1); let result2 = parser.parse_bytes(&ipfix_multifield_template_packet()); assert!(result2.is_ok()); @@ -730,7 +730,7 @@ fn test_ipfix_multifield_pending_replay() { }); assert!(has_data, "Replayed multi-field data should appear"); - let stats = parser.ipfix_cache_stats(); + let stats = parser.ipfix_cache_info(); assert_eq!(stats.pending_flow_count, 0); assert_eq!(stats.metrics.pending_replayed, 1); } @@ -750,7 +750,7 @@ fn test_v9_multiple_records_pending_replay() { // Data with 2 records, before template let result1 = parser.parse_bytes(&v9_multifield_two_records_packet()); assert!(result1.is_ok()); - assert_eq!(parser.v9_cache_stats().pending_flow_count, 1); + assert_eq!(parser.v9_cache_info().pending_flow_count, 1); // Template arrives let result2 = parser.parse_bytes(&v9_multifield_template_packet()); @@ -780,7 +780,7 @@ fn test_v9_multiple_records_pending_replay() { record_count, 2, "Should replay 2 records from the pending flowset" ); - assert_eq!(parser.v9_cache_stats().metrics.pending_replayed, 1); + assert_eq!(parser.v9_cache_info().metrics.pending_replayed, 1); } /// IPFIX: pending flowset with 2 records replayed correctly @@ -793,7 +793,7 @@ fn test_ipfix_multiple_records_pending_replay() { let result1 = parser.parse_bytes(&ipfix_multifield_two_records_packet()); assert!(result1.is_ok()); - assert_eq!(parser.ipfix_cache_stats().pending_flow_count, 1); + assert_eq!(parser.ipfix_cache_info().pending_flow_count, 1); let result2 = parser.parse_bytes(&ipfix_multifield_template_packet()); assert!(result2.is_ok()); @@ -822,7 +822,7 @@ fn test_ipfix_multiple_records_pending_replay() { record_count, 2, "Should replay 2 records from the pending flowset" ); - assert_eq!(parser.ipfix_cache_stats().metrics.pending_replayed, 1); + assert_eq!(parser.ipfix_cache_info().metrics.pending_replayed, 1); } // =========================================================================== @@ -840,7 +840,7 @@ fn test_v9_options_pending_replay() { // Options data arrives before options template let result1 = parser.parse_bytes(&v9_options_data_packet()); assert!(result1.is_ok()); - assert_eq!(parser.v9_cache_stats().pending_flow_count, 1); + assert_eq!(parser.v9_cache_info().pending_flow_count, 1); // Options template arrives -> should replay as OptionsData let result2 = parser.parse_bytes(&v9_options_template_packet()); @@ -860,7 +860,7 @@ fn test_v9_options_pending_replay() { }); assert!(has_options_data, "Should replay as OptionsData flowset"); - let stats = parser.v9_cache_stats(); + let stats = parser.v9_cache_info(); assert_eq!(stats.pending_flow_count, 0); assert_eq!(stats.metrics.pending_replayed, 1); } @@ -876,7 +876,7 @@ fn test_ipfix_options_pending_replay() { // Options data arrives before options template let result1 = parser.parse_bytes(&ipfix_options_data_packet()); assert!(result1.is_ok()); - assert_eq!(parser.ipfix_cache_stats().pending_flow_count, 1); + assert_eq!(parser.ipfix_cache_info().pending_flow_count, 1); // Options template arrives -> should replay as OptionsData let result2 = parser.parse_bytes(&ipfix_options_template_packet()); @@ -896,7 +896,7 @@ fn test_ipfix_options_pending_replay() { }); assert!(has_options_data, "Should replay as OptionsData flowset"); - let stats = parser.ipfix_cache_stats(); + let stats = parser.ipfix_cache_info(); assert_eq!(stats.pending_flow_count, 0); assert_eq!(stats.metrics.pending_replayed, 1); } @@ -917,11 +917,11 @@ fn test_v9_multiple_pending_same_template() { let _ = parser.parse_bytes(&v9_multifield_data_packet()); let _ = parser.parse_bytes(&v9_multifield_two_records_packet()); assert_eq!( - parser.v9_cache_stats().pending_flow_count, + parser.v9_cache_info().pending_flow_count, 2, "Two entries cached under template 256" ); - assert_eq!(parser.v9_cache_stats().metrics.pending_cached, 2); + assert_eq!(parser.v9_cache_info().metrics.pending_cached, 2); // Template arrives -> both should be replayed let result = parser.parse_bytes(&v9_multifield_template_packet()); @@ -951,7 +951,7 @@ fn test_v9_multiple_pending_same_template() { "Both pending entries should be replayed as separate Data flowsets" ); - let stats = parser.v9_cache_stats(); + let stats = parser.v9_cache_info(); assert_eq!(stats.pending_flow_count, 0); assert_eq!(stats.metrics.pending_replayed, 2); } @@ -966,7 +966,7 @@ fn test_ipfix_multiple_pending_same_template() { let _ = parser.parse_bytes(&ipfix_multifield_data_packet()); let _ = parser.parse_bytes(&ipfix_multifield_two_records_packet()); - assert_eq!(parser.ipfix_cache_stats().pending_flow_count, 2); + assert_eq!(parser.ipfix_cache_info().pending_flow_count, 2); let result = parser.parse_bytes(&ipfix_multifield_template_packet()); assert!(result.is_ok()); @@ -995,7 +995,7 @@ fn test_ipfix_multiple_pending_same_template() { "Both pending entries should be replayed as separate Data flowsets" ); - let stats = parser.ipfix_cache_stats(); + let stats = parser.ipfix_cache_info(); assert_eq!(stats.pending_flow_count, 0); assert_eq!(stats.metrics.pending_replayed, 2); } @@ -1014,7 +1014,7 @@ fn test_v9_pending_replay_with_same_packet_data() { // Cache a pending flow for template 256 let _ = parser.parse_bytes(&v9_data_packet()); - assert_eq!(parser.v9_cache_stats().pending_flow_count, 1); + assert_eq!(parser.v9_cache_info().pending_flow_count, 1); // Send a packet containing BOTH a template definition for 256 AND data using 256 // V9 processes flowsets in order: template first, then data matches @@ -1066,7 +1066,7 @@ fn test_v9_pending_replay_with_same_packet_data() { "Should have exactly 1 in-packet Data + 1 replayed Data flowset" ); - let stats = parser.v9_cache_stats(); + let stats = parser.v9_cache_info(); assert_eq!(stats.pending_flow_count, 0, "Pending should be drained"); assert_eq!(stats.metrics.pending_replayed, 1); } @@ -1108,25 +1108,25 @@ fn test_v9_out_of_order_template_arrival() { let _ = parser.parse_bytes(&make_v9_data(0x01, 0x00)); // 256 let _ = parser.parse_bytes(&make_v9_data(0x01, 0x01)); // 257 let _ = parser.parse_bytes(&make_v9_data(0x01, 0x02)); // 258 - assert_eq!(parser.v9_cache_stats().pending_flow_count, 3); + assert_eq!(parser.v9_cache_info().pending_flow_count, 3); // Template 258 arrives first let r1 = parser.parse_bytes(&make_v9_template(0x01, 0x02)); assert!(r1.is_ok()); - assert_eq!(parser.v9_cache_stats().pending_flow_count, 2); - assert_eq!(parser.v9_cache_stats().metrics.pending_replayed, 1); + assert_eq!(parser.v9_cache_info().pending_flow_count, 2); + assert_eq!(parser.v9_cache_info().metrics.pending_replayed, 1); // Template 256 arrives second let r2 = parser.parse_bytes(&make_v9_template(0x01, 0x00)); assert!(r2.is_ok()); - assert_eq!(parser.v9_cache_stats().pending_flow_count, 1); - assert_eq!(parser.v9_cache_stats().metrics.pending_replayed, 2); + assert_eq!(parser.v9_cache_info().pending_flow_count, 1); + assert_eq!(parser.v9_cache_info().metrics.pending_replayed, 2); // Template 257 arrives last let r3 = parser.parse_bytes(&make_v9_template(0x01, 0x01)); assert!(r3.is_ok()); - assert_eq!(parser.v9_cache_stats().pending_flow_count, 0); - assert_eq!(parser.v9_cache_stats().metrics.pending_replayed, 3); + assert_eq!(parser.v9_cache_info().pending_flow_count, 0); + assert_eq!(parser.v9_cache_info().metrics.pending_replayed, 3); // Verify each result contained a replayed Data flowset for (i, result) in [r1, r2, r3].iter().enumerate() { @@ -1167,7 +1167,7 @@ fn test_max_entries_per_template_v9() { } // Only 2 should be cached; 2 should be dropped - let stats = parser.v9_cache_stats(); + let stats = parser.v9_cache_info(); assert_eq!(stats.pending_flow_count, 2); assert_eq!(stats.metrics.pending_cached, 2); assert_eq!(stats.metrics.pending_dropped, 2); @@ -1176,7 +1176,7 @@ fn test_max_entries_per_template_v9() { let result = parser.parse_bytes(&v9_template_packet()); assert!(result.is_ok()); - let stats = parser.v9_cache_stats(); + let stats = parser.v9_cache_info(); assert_eq!(stats.pending_flow_count, 0); assert_eq!(stats.metrics.pending_replayed, 2); } @@ -1198,7 +1198,7 @@ fn test_max_entries_per_template_ipfix() { } // Only 3 should be cached; 2 dropped - let stats = parser.ipfix_cache_stats(); + let stats = parser.ipfix_cache_info(); assert_eq!(stats.pending_flow_count, 3); assert_eq!(stats.metrics.pending_cached, 3); assert_eq!(stats.metrics.pending_dropped, 2); @@ -1207,7 +1207,7 @@ fn test_max_entries_per_template_ipfix() { let result = parser.parse_bytes(&ipfix_template_packet()); assert!(result.is_ok()); - let stats = parser.ipfix_cache_stats(); + let stats = parser.ipfix_cache_info(); assert_eq!(stats.pending_flow_count, 0); assert_eq!(stats.metrics.pending_replayed, 3); } @@ -1244,15 +1244,15 @@ fn test_max_entry_size_bytes_v9() { // Send a small data packet (4 bytes payload) - should be cached let _ = parser.parse_bytes(&v9_data_packet()); // 4 bytes of data - assert_eq!(parser.v9_cache_stats().pending_flow_count, 1); - assert_eq!(parser.v9_cache_stats().metrics.pending_cached, 1); - assert_eq!(parser.v9_cache_stats().metrics.pending_dropped, 0); + assert_eq!(parser.v9_cache_info().pending_flow_count, 1); + assert_eq!(parser.v9_cache_info().metrics.pending_cached, 1); + assert_eq!(parser.v9_cache_info().metrics.pending_dropped, 0); // Send a large data packet (16 bytes payload) - should be dropped let _ = parser.parse_bytes(&v9_data_packet_with_size(16)); - assert_eq!(parser.v9_cache_stats().pending_flow_count, 1); - assert_eq!(parser.v9_cache_stats().metrics.pending_cached, 1); - assert_eq!(parser.v9_cache_stats().metrics.pending_dropped, 1); + assert_eq!(parser.v9_cache_info().pending_flow_count, 1); + assert_eq!(parser.v9_cache_info().metrics.pending_cached, 1); + assert_eq!(parser.v9_cache_info().metrics.pending_dropped, 1); } /// Helper: build an IPFIX data packet with a large payload. @@ -1288,13 +1288,13 @@ fn test_max_entry_size_bytes_ipfix() { // Small packet (4 bytes) - cached let _ = parser.parse_bytes(&ipfix_data_packet()); - assert_eq!(parser.ipfix_cache_stats().pending_flow_count, 1); - assert_eq!(parser.ipfix_cache_stats().metrics.pending_cached, 1); + assert_eq!(parser.ipfix_cache_info().pending_flow_count, 1); + assert_eq!(parser.ipfix_cache_info().metrics.pending_cached, 1); // Large packet (16 bytes) - dropped let _ = parser.parse_bytes(&ipfix_data_packet_with_size(16)); - assert_eq!(parser.ipfix_cache_stats().pending_flow_count, 1); - assert_eq!(parser.ipfix_cache_stats().metrics.pending_dropped, 1); + assert_eq!(parser.ipfix_cache_info().pending_flow_count, 1); + assert_eq!(parser.ipfix_cache_info().metrics.pending_dropped, 1); } // Verify that building a parser with max_pending_flows=0 is rejected for all configurations @@ -1363,8 +1363,8 @@ fn test_dropped_cache_entry_keeps_no_template_in_output() { panic!("Expected V9 packet"); } - assert_eq!(parser.v9_cache_stats().metrics.pending_cached, 1); - assert_eq!(parser.v9_cache_stats().metrics.pending_dropped, 1); + assert_eq!(parser.v9_cache_info().metrics.pending_cached, 1); + assert_eq!(parser.v9_cache_info().metrics.pending_dropped, 1); } /// When a single packet contains multiple NoTemplate flowsets for the same @@ -1427,8 +1427,8 @@ fn test_partial_cache_same_template_in_single_packet() { ); } - assert_eq!(parser.v9_cache_stats().metrics.pending_cached, 1); - assert_eq!(parser.v9_cache_stats().metrics.pending_dropped, 1); + assert_eq!(parser.v9_cache_info().metrics.pending_cached, 1); + assert_eq!(parser.v9_cache_info().metrics.pending_dropped, 1); } /// Reconfiguring pending flows at runtime should trim entries that exceed the @@ -1448,9 +1448,9 @@ fn test_resize_trims_excess_entries_per_template() { for _ in 0..5 { let _ = parser.parse_bytes(&v9_data_packet()); } - assert_eq!(parser.v9_cache_stats().pending_flow_count, 5); - assert_eq!(parser.v9_cache_stats().metrics.pending_cached, 5); - assert_eq!(parser.v9_cache_stats().metrics.pending_dropped, 0); + assert_eq!(parser.v9_cache_info().pending_flow_count, 5); + assert_eq!(parser.v9_cache_info().metrics.pending_cached, 5); + assert_eq!(parser.v9_cache_info().metrics.pending_dropped, 0); // Shrink the per-template cap to 2. let mut stricter = PendingFlowsConfig::new(256); @@ -1461,8 +1461,8 @@ fn test_resize_trims_excess_entries_per_template() { .expect("reconfigure should succeed"); // 3 entries should have been dropped during resize. - assert_eq!(parser.v9_cache_stats().pending_flow_count, 2); - assert_eq!(parser.v9_cache_stats().metrics.pending_dropped, 3); + assert_eq!(parser.v9_cache_info().pending_flow_count, 2); + assert_eq!(parser.v9_cache_info().metrics.pending_dropped, 3); } /// Reconfiguring pending flows at runtime should drop entries whose raw data @@ -1481,8 +1481,8 @@ fn test_resize_trims_oversize_entries() { // Cache a small entry (4 bytes) and a larger entry (16 bytes). let _ = parser.parse_bytes(&v9_data_packet()); // 4-byte payload let _ = parser.parse_bytes(&v9_data_packet_with_size(16)); // 16-byte payload - assert_eq!(parser.v9_cache_stats().pending_flow_count, 2); - assert_eq!(parser.v9_cache_stats().metrics.pending_dropped, 0); + assert_eq!(parser.v9_cache_info().pending_flow_count, 2); + assert_eq!(parser.v9_cache_info().metrics.pending_dropped, 0); // Lower the size limit to 8 bytes — the 16-byte entry should be dropped. let mut stricter = PendingFlowsConfig::new(256); @@ -1492,8 +1492,8 @@ fn test_resize_trims_oversize_entries() { .set_pending_flows_config(Some(stricter)) .expect("reconfigure should succeed"); - assert_eq!(parser.v9_cache_stats().pending_flow_count, 1); - assert_eq!(parser.v9_cache_stats().metrics.pending_dropped, 1); + assert_eq!(parser.v9_cache_info().pending_flow_count, 1); + assert_eq!(parser.v9_cache_info().metrics.pending_dropped, 1); } /// Reconfiguring IPFIX pending flows at runtime should trim entries that @@ -1512,8 +1512,8 @@ fn test_resize_trims_ipfix_pending_flows() { for _ in 0..4 { let _ = parser.parse_bytes(&ipfix_data_packet()); } - assert_eq!(parser.ipfix_cache_stats().pending_flow_count, 4); - assert_eq!(parser.ipfix_cache_stats().metrics.pending_dropped, 0); + assert_eq!(parser.ipfix_cache_info().pending_flow_count, 4); + assert_eq!(parser.ipfix_cache_info().metrics.pending_dropped, 0); // Shrink cap to 1. let mut stricter = PendingFlowsConfig::new(256); @@ -1523,8 +1523,8 @@ fn test_resize_trims_ipfix_pending_flows() { .set_pending_flows_config(Some(stricter)) .expect("reconfigure should succeed"); - assert_eq!(parser.ipfix_cache_stats().pending_flow_count, 1); - assert_eq!(parser.ipfix_cache_stats().metrics.pending_dropped, 3); + assert_eq!(parser.ipfix_cache_info().pending_flow_count, 1); + assert_eq!(parser.ipfix_cache_info().metrics.pending_dropped, 3); } /// Caching into a template_id whose existing entries have all expired should @@ -1540,22 +1540,22 @@ fn test_cache_prunes_expired_entries_for_touched_template() { // First entry for template 256 — no existing entries to prune. let _ = parser.parse_bytes(&v9_data_packet()); - assert_eq!(parser.v9_cache_stats().pending_flow_count, 1); - assert_eq!(parser.v9_cache_stats().metrics.pending_cached, 1); - assert_eq!(parser.v9_cache_stats().metrics.pending_dropped, 0); + assert_eq!(parser.v9_cache_info().pending_flow_count, 1); + assert_eq!(parser.v9_cache_info().metrics.pending_cached, 1); + assert_eq!(parser.v9_cache_info().metrics.pending_dropped, 0); // Second entry — the first entry is now expired and should be pruned, // making room for the new one. let _ = parser.parse_bytes(&v9_data_packet()); - assert_eq!(parser.v9_cache_stats().pending_flow_count, 1); - assert_eq!(parser.v9_cache_stats().metrics.pending_cached, 2); - assert_eq!(parser.v9_cache_stats().metrics.pending_dropped, 1); + assert_eq!(parser.v9_cache_info().pending_flow_count, 1); + assert_eq!(parser.v9_cache_info().metrics.pending_cached, 2); + assert_eq!(parser.v9_cache_info().metrics.pending_dropped, 1); // Third entry — same: second entry expired, pruned, third inserted. let _ = parser.parse_bytes(&v9_data_packet()); - assert_eq!(parser.v9_cache_stats().pending_flow_count, 1); - assert_eq!(parser.v9_cache_stats().metrics.pending_cached, 3); - assert_eq!(parser.v9_cache_stats().metrics.pending_dropped, 2); + assert_eq!(parser.v9_cache_info().pending_flow_count, 1); + assert_eq!(parser.v9_cache_info().metrics.pending_cached, 3); + assert_eq!(parser.v9_cache_info().metrics.pending_dropped, 2); } /// When the cache is at its template-ID capacity and a new template_id is @@ -1578,9 +1578,9 @@ fn test_cache_purges_globally_before_lru_eviction() { pkt_257[21] = 0x01; // template 257 let _ = parser.parse_bytes(&pkt_257); - assert_eq!(parser.v9_cache_stats().pending_flow_count, 2); - assert_eq!(parser.v9_cache_stats().metrics.pending_cached, 2); - assert_eq!(parser.v9_cache_stats().metrics.pending_dropped, 0); + assert_eq!(parser.v9_cache_info().pending_flow_count, 2); + assert_eq!(parser.v9_cache_info().metrics.pending_cached, 2); + assert_eq!(parser.v9_cache_info().metrics.pending_dropped, 0); // Insert a third template ID. Both existing templates' entries are // expired (TTL=0), so the global purge should clean them up instead @@ -1592,9 +1592,9 @@ fn test_cache_purges_globally_before_lru_eviction() { // Only the fresh entry for 258 should remain; the two expired // template slots were purged. - assert_eq!(parser.v9_cache_stats().pending_flow_count, 1); - assert_eq!(parser.v9_cache_stats().metrics.pending_cached, 3); - assert_eq!(parser.v9_cache_stats().metrics.pending_dropped, 2); + assert_eq!(parser.v9_cache_info().pending_flow_count, 1); + assert_eq!(parser.v9_cache_info().metrics.pending_cached, 3); + assert_eq!(parser.v9_cache_info().metrics.pending_dropped, 2); } /// Oversized flowset bodies should be truncated at parse time rather than @@ -1657,9 +1657,9 @@ fn test_oversized_entry_truncated_at_parse_time_v9() { } // Nothing should have been cached; the drop should be recorded. - assert_eq!(parser.v9_cache_stats().pending_flow_count, 0); - assert_eq!(parser.v9_cache_stats().metrics.pending_cached, 0); - assert_eq!(parser.v9_cache_stats().metrics.pending_dropped, 1); + assert_eq!(parser.v9_cache_info().pending_flow_count, 0); + assert_eq!(parser.v9_cache_info().metrics.pending_cached, 0); + assert_eq!(parser.v9_cache_info().metrics.pending_dropped, 1); } /// Same as V9 test but for IPFIX: oversized flowset bodies are truncated @@ -1727,9 +1727,9 @@ fn test_oversized_entry_truncated_at_parse_time_ipfix() { ); } - assert_eq!(parser.ipfix_cache_stats().pending_flow_count, 0); - assert_eq!(parser.ipfix_cache_stats().metrics.pending_cached, 0); - assert_eq!(parser.ipfix_cache_stats().metrics.pending_dropped, 1); + assert_eq!(parser.ipfix_cache_info().pending_flow_count, 0); + assert_eq!(parser.ipfix_cache_info().metrics.pending_cached, 0); + assert_eq!(parser.ipfix_cache_info().metrics.pending_dropped, 1); } /// When the cache rejects an entry (per-template cap), the returned raw_data @@ -1870,17 +1870,17 @@ fn test_would_accept_accounts_for_ttl_v9() { // First entry cached normally. let _ = parser.parse_bytes(&v9_data_packet()); - assert_eq!(parser.v9_cache_stats().pending_flow_count, 1); - assert_eq!(parser.v9_cache_stats().metrics.pending_cached, 1); + assert_eq!(parser.v9_cache_info().pending_flow_count, 1); + assert_eq!(parser.v9_cache_info().metrics.pending_cached, 1); // Second entry: the first is expired (TTL=0). would_accept() should see // 0 live entries and allow the full clone. cache() prunes the expired // entry and inserts the new one. let _ = parser.parse_bytes(&v9_data_packet()); - assert_eq!(parser.v9_cache_stats().pending_flow_count, 1); - assert_eq!(parser.v9_cache_stats().metrics.pending_cached, 2); + assert_eq!(parser.v9_cache_info().pending_flow_count, 1); + assert_eq!(parser.v9_cache_info().metrics.pending_cached, 2); // 1 dropped = the pruned expired entry - assert_eq!(parser.v9_cache_stats().metrics.pending_dropped, 1); + assert_eq!(parser.v9_cache_info().metrics.pending_dropped, 1); } /// Same as the V9 test but for IPFIX: would_accept() accounts for TTL. @@ -1895,13 +1895,13 @@ fn test_would_accept_accounts_for_ttl_ipfix() { .expect("Failed to build parser"); let _ = parser.parse_bytes(&ipfix_data_packet()); - assert_eq!(parser.ipfix_cache_stats().pending_flow_count, 1); - assert_eq!(parser.ipfix_cache_stats().metrics.pending_cached, 1); + assert_eq!(parser.ipfix_cache_info().pending_flow_count, 1); + assert_eq!(parser.ipfix_cache_info().metrics.pending_cached, 1); let _ = parser.parse_bytes(&ipfix_data_packet()); - assert_eq!(parser.ipfix_cache_stats().pending_flow_count, 1); - assert_eq!(parser.ipfix_cache_stats().metrics.pending_cached, 2); - assert_eq!(parser.ipfix_cache_stats().metrics.pending_dropped, 1); + assert_eq!(parser.ipfix_cache_info().pending_flow_count, 1); + assert_eq!(parser.ipfix_cache_info().metrics.pending_cached, 2); + assert_eq!(parser.ipfix_cache_info().metrics.pending_dropped, 1); } /// Same as the V9 test but for IPFIX, verifying the per-template TTL @@ -1914,12 +1914,12 @@ fn test_ipfix_cache_prunes_expired_entries() { .expect("Failed to build parser"); let _ = parser.parse_bytes(&ipfix_data_packet()); - assert_eq!(parser.ipfix_cache_stats().pending_flow_count, 1); - assert_eq!(parser.ipfix_cache_stats().metrics.pending_dropped, 0); + assert_eq!(parser.ipfix_cache_info().pending_flow_count, 1); + assert_eq!(parser.ipfix_cache_info().metrics.pending_dropped, 0); // Second entry prunes the expired first entry. let _ = parser.parse_bytes(&ipfix_data_packet()); - assert_eq!(parser.ipfix_cache_stats().pending_flow_count, 1); - assert_eq!(parser.ipfix_cache_stats().metrics.pending_cached, 2); - assert_eq!(parser.ipfix_cache_stats().metrics.pending_dropped, 1); + assert_eq!(parser.ipfix_cache_info().pending_flow_count, 1); + assert_eq!(parser.ipfix_cache_info().metrics.pending_cached, 2); + assert_eq!(parser.ipfix_cache_info().metrics.pending_dropped, 1); } diff --git a/tests/template_cache.rs b/tests/template_cache.rs index 7fa3d48..e2d6a87 100644 --- a/tests/template_cache.rs +++ b/tests/template_cache.rs @@ -17,24 +17,24 @@ fn test_clear_templates() { ]; let _ = parser.parse_bytes(&v9_template_packet); - let v9_stats = parser.v9_cache_stats(); + let v9_info = parser.v9_cache_info(); assert!( - v9_stats.current_size > 0, + v9_info.current_size > 0, "V9 cache should have a template before clearing" ); parser.clear_v9_templates(); - let v9_stats = parser.v9_cache_stats(); + let v9_info = parser.v9_cache_info(); assert_eq!( - v9_stats.current_size, 0, + v9_info.current_size, 0, "V9 cache should be empty after clearing" ); parser.clear_ipfix_templates(); - let ipfix_stats = parser.ipfix_cache_stats(); - assert_eq!(ipfix_stats.current_size, 0); + let ipfix_info = parser.ipfix_cache_info(); + assert_eq!(ipfix_info.current_size, 0); } // Verify that with_cache_size sets the same max size for both V9 and IPFIX caches @@ -45,11 +45,11 @@ fn test_custom_cache_size() { .build() .expect("Failed to build parser"); - let v9_stats = parser.v9_cache_stats(); - assert_eq!(v9_stats.max_size_per_cache, 500); + let v9_info = parser.v9_cache_info(); + assert_eq!(v9_info.max_size_per_cache, 500); - let ipfix_stats = parser.ipfix_cache_stats(); - assert_eq!(ipfix_stats.max_size_per_cache, 500); + let ipfix_info = parser.ipfix_cache_info(); + assert_eq!(ipfix_info.max_size_per_cache, 500); } // Verify that V9 and IPFIX cache sizes can be configured independently @@ -61,11 +61,11 @@ fn test_different_cache_sizes() { .build() .expect("Failed to build parser"); - let v9_stats = parser.v9_cache_stats(); - assert_eq!(v9_stats.max_size_per_cache, 750); + let v9_info = parser.v9_cache_info(); + assert_eq!(v9_info.max_size_per_cache, 750); - let ipfix_stats = parser.ipfix_cache_stats(); - assert_eq!(ipfix_stats.max_size_per_cache, 1500); + let ipfix_info = parser.ipfix_cache_info(); + assert_eq!(ipfix_info.max_size_per_cache, 1500); } // Verify that template_ids() returns cached template IDs after parsing a template @@ -110,16 +110,249 @@ fn test_hit_rate_after_activity() { 0, 8, // length = 8 0, 0, 0, 42, // IN_BYTES = 42 ]; - let _ = parser.parse_bytes(&v9_data_packet); + let result = parser.parse_bytes(&v9_data_packet); + assert!( + result.error.is_none(), + "Data packet parse should succeed after template is cached" + ); - let stats = parser.v9_cache_stats(); + let stats = parser.v9_cache_info(); let hit_rate = stats.metrics.hit_rate(); assert!( hit_rate.is_some(), "hit_rate should return Some after cache activity" ); + assert_eq!( + hit_rate.unwrap(), + 1.0, + "hit_rate should be 1.0 after 1 hit and 0 misses" + ); +} + +// --------------------------------------------------------------------------- +// IPFIX template withdrawal tests (RFC 7011 §8.1) +// --------------------------------------------------------------------------- + +/// Helper: build an IPFIX packet with a template set containing a single template. +fn ipfix_template_packet(template_id: u16, fields: &[(u16, u16)]) -> Vec { + // Template record: template_id(2) + field_count(2) + fields(4 each) + let template_record_len = 4 + fields.len() * 4; + // Set: set_id(2) + set_length(2) + template record + let set_len = (4 + template_record_len) as u16; + // Message: header(16) + set + let msg_len = 16 + set_len; + + let mut pkt = Vec::with_capacity(msg_len as usize); + pkt.extend_from_slice(&0x000Au16.to_be_bytes()); // Version 10 + pkt.extend_from_slice(&msg_len.to_be_bytes()); + pkt.extend_from_slice(&1u32.to_be_bytes()); // Export Time + pkt.extend_from_slice(&1u32.to_be_bytes()); // Sequence + pkt.extend_from_slice(&1u32.to_be_bytes()); // Observation Domain + pkt.extend_from_slice(&2u16.to_be_bytes()); // Set ID = 2 (template) + pkt.extend_from_slice(&set_len.to_be_bytes()); + pkt.extend_from_slice(&template_id.to_be_bytes()); + pkt.extend_from_slice(&(fields.len() as u16).to_be_bytes()); + for &(field_type, field_length) in fields { + pkt.extend_from_slice(&field_type.to_be_bytes()); + pkt.extend_from_slice(&field_length.to_be_bytes()); + } + pkt +} + +/// Helper: build an IPFIX withdrawal packet (template with field_count=0). +fn ipfix_withdrawal_packet(template_id: u16) -> Vec { + let set_len: u16 = 8; // set header(4) + template_id(2) + field_count=0(2) + let msg_len: u16 = 16 + set_len; + + let mut pkt = Vec::with_capacity(msg_len as usize); + pkt.extend_from_slice(&0x000Au16.to_be_bytes()); + pkt.extend_from_slice(&msg_len.to_be_bytes()); + pkt.extend_from_slice(&1u32.to_be_bytes()); + pkt.extend_from_slice(&2u32.to_be_bytes()); // different sequence + pkt.extend_from_slice(&1u32.to_be_bytes()); + pkt.extend_from_slice(&2u16.to_be_bytes()); // Set ID = 2 (template) + pkt.extend_from_slice(&set_len.to_be_bytes()); + pkt.extend_from_slice(&template_id.to_be_bytes()); + pkt.extend_from_slice(&0u16.to_be_bytes()); // field_count = 0 (withdrawal) + pkt +} + +/// Helper: build an IPFIX options template withdrawal packet (field_count=0 in set ID 3). +fn ipfix_options_withdrawal_packet(template_id: u16) -> Vec { + // Options template withdrawal: template_id(2) + field_count=0(2) + scope_field_count=0(2) + let set_len: u16 = 10; // set header(4) + template_id(2) + field_count(2) + scope_count(2) + let msg_len: u16 = 16 + set_len; + + let mut pkt = Vec::with_capacity(msg_len as usize); + pkt.extend_from_slice(&0x000Au16.to_be_bytes()); + pkt.extend_from_slice(&msg_len.to_be_bytes()); + pkt.extend_from_slice(&1u32.to_be_bytes()); + pkt.extend_from_slice(&2u32.to_be_bytes()); + pkt.extend_from_slice(&1u32.to_be_bytes()); + pkt.extend_from_slice(&3u16.to_be_bytes()); // Set ID = 3 (options template) + pkt.extend_from_slice(&set_len.to_be_bytes()); + pkt.extend_from_slice(&template_id.to_be_bytes()); + pkt.extend_from_slice(&0u16.to_be_bytes()); // field_count = 0 (withdrawal) + pkt.extend_from_slice(&0u16.to_be_bytes()); // scope_field_count = 0 + pkt +} + +/// Helper: build an IPFIX options template packet. +fn ipfix_options_template_pkt(template_id: u16) -> Vec { + // Options template: scope(type=1,len=4) + option(type=3,len=4) + option(type=4,len=4) + let set_len: u16 = 22; // set header(4) + tmpl header(6) + 3 fields(12) + let msg_len: u16 = 16 + set_len; + + let mut pkt = Vec::with_capacity(msg_len as usize); + pkt.extend_from_slice(&0x000Au16.to_be_bytes()); + pkt.extend_from_slice(&msg_len.to_be_bytes()); + pkt.extend_from_slice(&1u32.to_be_bytes()); + pkt.extend_from_slice(&1u32.to_be_bytes()); + pkt.extend_from_slice(&1u32.to_be_bytes()); + pkt.extend_from_slice(&3u16.to_be_bytes()); // Set ID = 3 + pkt.extend_from_slice(&set_len.to_be_bytes()); + pkt.extend_from_slice(&template_id.to_be_bytes()); + pkt.extend_from_slice(&3u16.to_be_bytes()); // field_count = 3 + pkt.extend_from_slice(&1u16.to_be_bytes()); // scope_field_count = 1 + // Scope field: type=1, length=4 + pkt.extend_from_slice(&1u16.to_be_bytes()); + pkt.extend_from_slice(&4u16.to_be_bytes()); + // Option field: type=3, length=4 + pkt.extend_from_slice(&3u16.to_be_bytes()); + pkt.extend_from_slice(&4u16.to_be_bytes()); + // Option field: type=4, length=4 + pkt.extend_from_slice(&4u16.to_be_bytes()); + pkt.extend_from_slice(&4u16.to_be_bytes()); + pkt +} + +/// Individual IPFIX data template withdrawal removes that template. +#[test] +fn test_ipfix_individual_template_withdrawal() { + let mut parser = NetflowParser::default(); + + // Register two templates + let _ = parser.parse_bytes(&ipfix_template_packet(256, &[(1, 4)])); + let _ = parser.parse_bytes(&ipfix_template_packet(257, &[(2, 4)])); + assert!(parser.has_ipfix_template(256)); + assert!(parser.has_ipfix_template(257)); + + // Withdraw template 256 only + let _ = parser.parse_bytes(&ipfix_withdrawal_packet(256)); + assert!( + !parser.has_ipfix_template(256), + "Template 256 should be withdrawn" + ); + assert!(parser.has_ipfix_template(257), "Template 257 should remain"); +} + +/// IPFIX "withdraw all data templates" (template_id=2, field_count=0) clears +/// all data templates per RFC 7011 §8.1. +#[test] +fn test_ipfix_withdraw_all_data_templates() { + let mut parser = NetflowParser::default(); + + // Register 3 data templates + let _ = parser.parse_bytes(&ipfix_template_packet(256, &[(1, 4)])); + let _ = parser.parse_bytes(&ipfix_template_packet(257, &[(2, 4)])); + let _ = parser.parse_bytes(&ipfix_template_packet(258, &[(3, 4)])); + assert_eq!(parser.ipfix_cache_info().current_size, 3); + + // Also register an options template — should NOT be affected + let _ = parser.parse_bytes(&ipfix_options_template_pkt(259)); + assert_eq!(parser.ipfix_cache_info().current_size, 4); + + // Withdraw all data templates (template_id=2, field_count=0) + let _ = parser.parse_bytes(&ipfix_withdrawal_packet(2)); + + // All 3 data templates should be gone, options template should remain + assert!(!parser.has_ipfix_template(256)); + assert!(!parser.has_ipfix_template(257)); + assert!(!parser.has_ipfix_template(258)); + assert_eq!( + parser.ipfix_cache_info().current_size, + 1, + "Only the options template should remain after withdraw-all data" + ); +} + +/// IPFIX "withdraw all options templates" (template_id=3, field_count=0) clears +/// all options templates per RFC 7011 §8.1. +#[test] +fn test_ipfix_withdraw_all_options_templates() { + let mut parser = NetflowParser::default(); + + // Register a data template — should NOT be affected + let _ = parser.parse_bytes(&ipfix_template_packet(256, &[(1, 4)])); + + // Register 2 options templates + let _ = parser.parse_bytes(&ipfix_options_template_pkt(258)); + let _ = parser.parse_bytes(&ipfix_options_template_pkt(259)); + assert_eq!(parser.ipfix_cache_info().current_size, 3); + + // Withdraw all options templates (template_id=3, field_count=0) + let _ = parser.parse_bytes(&ipfix_options_withdrawal_packet(3)); + + // Options templates should be gone, data template should remain assert!( - hit_rate.unwrap() > 0.0, - "hit_rate should be positive after a cache hit" + parser.has_ipfix_template(256), + "Data template should survive options withdraw-all" + ); + assert_eq!( + parser.ipfix_cache_info().current_size, + 1, + "Only the data template should remain after withdraw-all options" + ); +} + +/// IPFIX withdraw-all with pending flows drains them and records metrics. +#[test] +fn test_ipfix_withdraw_all_drains_pending_flows() { + use netflow_parser::variable_versions::PendingFlowsConfig; + + let mut parser = NetflowParser::builder() + .with_ipfix_pending_flows(PendingFlowsConfig::default()) + .build() + .expect("valid config"); + + // Cache a pending flow for template 256 (no template registered yet) + let data_pkt = vec![ + 0x00, 0x0A, 0x00, 0x18, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, + 0x00, 0x01, 0x01, 0x00, // Set ID = 256 + 0x00, 0x08, // Set Length = 8 + 0x00, 0x00, 0x00, 0x42, + ]; + let _ = parser.parse_bytes(&data_pkt); + assert_eq!(parser.ipfix_cache_info().pending_flow_count, 1); + + // Register the template so it's in the cache + let _ = parser.parse_bytes(&ipfix_template_packet(256, &[(1, 4)])); + // Pending flow was replayed + assert_eq!(parser.ipfix_cache_info().pending_flow_count, 0); + assert_eq!(parser.ipfix_cache_info().metrics.pending_replayed, 1); + + // Cache another pending flow for a different template (257, not registered) + let mut data_257 = data_pkt.clone(); + data_257[16] = 0x01; + data_257[17] = 0x01; // Set ID = 257 + let _ = parser.parse_bytes(&data_257); + assert_eq!(parser.ipfix_cache_info().pending_flow_count, 1); + + // Withdraw all data templates — should also drain pending flows for 256 + // (template 257 pending flow stays because it was never in the template cache) + let _ = parser.parse_bytes(&ipfix_withdrawal_packet(2)); + assert!(!parser.has_ipfix_template(256)); + + // The pending flow for 257 should still be there (257 was never a data template) + // The withdraw-all only drains pending flows for IDs that were IN the template cache + assert_eq!( + parser.ipfix_cache_info().metrics.pending_dropped, + 0, + "No pending flows should be dropped (257 was not in template cache)" + ); + assert_eq!( + parser.ipfix_cache_info().pending_flow_count, + 1, + "Pending flow for template 257 should still be cached" ); }