Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1011,9 +1011,9 @@ let mut scoped = RouterScopedParser::<String>::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");
}
```
Expand Down Expand Up @@ -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();
Expand Down
35 changes: 22 additions & 13 deletions RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)`**
Expand Down Expand Up @@ -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<u16>`**
- All variants (`Learned`, `Collision`, `Evicted`, `Expired`, `MissingTemplate`) now use `Option<u16>`
- `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
Expand All @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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`

Expand Down
12 changes: 6 additions & 6 deletions examples/multi_source_comparison.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!");
Expand Down
2 changes: 1 addition & 1 deletion examples/netflow_udp_listener_multi_threaded.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion examples/netflow_udp_listener_single_threaded.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions examples/netflow_udp_listener_tokio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
24 changes: 12 additions & 12 deletions examples/template_management_demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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: {}/{}",
Expand Down Expand Up @@ -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:");
Expand Down Expand Up @@ -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:");
Expand Down
56 changes: 9 additions & 47 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<variable_versions::ttl::TtlConfig>,
/// 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
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading