netflow_parser/
template_events.rs

1//! Template lifecycle events and hooks for monitoring template cache behavior.
2//!
3//! This module provides an event system for tracking template operations in real-time.
4//! Users can register callbacks to be notified when templates are learned, evicted,
5//! expire, collide, or when data arrives for missing templates.
6//!
7//! # Use Cases
8//!
9//! - **Monitoring**: Track template learning and eviction patterns
10//! - **Alerting**: Detect template collisions that indicate configuration issues
11//! - **Metrics**: Integrate with observability systems (Prometheus, StatsD, etc.)
12//! - **Debugging**: Log template lifecycle events for troubleshooting
13//! - **Dynamic behavior**: React to template events with custom logic
14//!
15//! # Examples
16//!
17//! ```rust
18//! use netflow_parser::{NetflowParser, TemplateEvent, TemplateProtocol};
19//!
20//! let parser = NetflowParser::builder()
21//!     .on_template_event(|event| {
22//!         match event {
23//!             TemplateEvent::Learned { template_id, protocol } => {
24//!                 println!("Learned template {} for {:?}", template_id, protocol);
25//!             }
26//!             TemplateEvent::Collision { template_id, protocol } => {
27//!                 eprintln!("⚠️  Collision on template {} for {:?}", template_id, protocol);
28//!             }
29//!             _ => {}
30//!         }
31//!     })
32//!     .build()
33//!     .unwrap();
34//! ```
35
36use std::sync::Arc;
37
38/// Protocol type for template events.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
40pub enum TemplateProtocol {
41    /// NetFlow v9 template
42    V9,
43    /// IPFIX template
44    Ipfix,
45}
46
47/// Template lifecycle events.
48///
49/// These events are emitted during template cache operations and allow
50/// users to monitor and react to template state changes.
51#[derive(Debug, Clone)]
52pub enum TemplateEvent {
53    /// A new template was learned and added to the cache.
54    ///
55    /// This event fires when a template definition packet is successfully parsed
56    /// and the template is inserted into the cache for the first time.
57    Learned {
58        /// The template ID that was learned
59        template_id: u16,
60        /// The protocol (V9 or IPFIX)
61        protocol: TemplateProtocol,
62    },
63
64    /// A template ID was reused, potentially with a different definition.
65    ///
66    /// This event indicates that a template ID already in the cache was
67    /// encountered again. This could be:
68    /// - The same template being re-sent (normal refresh)
69    /// - A different template using the same ID (collision - problematic)
70    ///
71    /// In multi-source deployments without proper scoping, collisions indicate
72    /// that different routers are using the same template ID with potentially
73    /// different schemas. Use `AutoScopedParser` to avoid this issue.
74    Collision {
75        /// The template ID that collided
76        template_id: u16,
77        /// The protocol (V9 or IPFIX)
78        protocol: TemplateProtocol,
79    },
80
81    /// A template was evicted from the cache due to LRU policy.
82    ///
83    /// When the cache reaches its maximum size, the least recently used
84    /// template is evicted to make room for new templates. Frequent evictions
85    /// may indicate that the cache size is too small or that there are too
86    /// many active templates.
87    Evicted {
88        /// The template ID that was evicted
89        template_id: u16,
90        /// The protocol (V9 or IPFIX)
91        protocol: TemplateProtocol,
92    },
93
94    /// A template expired due to TTL timeout.
95    ///
96    /// When TTL-based template expiration is enabled, templates that haven't
97    /// been used within the configured timeout are automatically removed from
98    /// the cache. This is useful for handling exporters that may change their
99    /// template definitions without notification.
100    Expired {
101        /// The template ID that expired
102        template_id: u16,
103        /// The protocol (V9 or IPFIX)
104        protocol: TemplateProtocol,
105    },
106
107    /// Data packet arrived for a template that isn't in the cache.
108    ///
109    /// This typically occurs when:
110    /// - Data packets arrive before their template definition (out-of-order)
111    /// - Template was evicted from cache before data arrived
112    /// - Template definition packet was lost in transit
113    ///
114    /// Users can implement retry logic or buffering strategies based on this event.
115    MissingTemplate {
116        /// The template ID that was not found
117        template_id: u16,
118        /// The protocol (V9 or IPFIX)
119        protocol: TemplateProtocol,
120    },
121}
122
123/// Type alias for template event hooks.
124///
125/// Hooks are functions that receive a reference to a `TemplateEvent` and
126/// can perform any side effects (logging, metrics, etc.).
127///
128/// Hooks must be:
129/// - `Send + Sync` for thread safety
130/// - `'static` lifetime to be stored in the parser
131pub type TemplateHook = Arc<dyn Fn(&TemplateEvent) + Send + Sync + 'static>;
132
133/// Container for registered template event hooks.
134#[derive(Clone, Default)]
135pub struct TemplateHooks {
136    hooks: Vec<TemplateHook>,
137}
138
139// Custom Debug implementation to avoid printing closures
140impl std::fmt::Debug for TemplateHooks {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        f.debug_struct("TemplateHooks")
143            .field("hook_count", &self.hooks.len())
144            .finish()
145    }
146}
147
148impl TemplateHooks {
149    /// Creates a new empty hook container.
150    pub fn new() -> Self {
151        Self { hooks: Vec::new() }
152    }
153
154    /// Registers a new hook.
155    pub fn register<F>(&mut self, hook: F)
156    where
157        F: Fn(&TemplateEvent) + Send + Sync + 'static,
158    {
159        self.hooks.push(Arc::new(hook));
160    }
161
162    /// Triggers all registered hooks with the given event.
163    pub fn trigger(&self, event: &TemplateEvent) {
164        for hook in &self.hooks {
165            hook(event);
166        }
167    }
168
169    /// Returns the number of registered hooks.
170    pub fn len(&self) -> usize {
171        self.hooks.len()
172    }
173
174    /// Returns true if no hooks are registered.
175    pub fn is_empty(&self) -> bool {
176        self.hooks.is_empty()
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use std::sync::Arc;
184    use std::sync::atomic::{AtomicUsize, Ordering};
185
186    #[test]
187    fn test_hook_registration() {
188        let mut hooks = TemplateHooks::new();
189        assert_eq!(hooks.len(), 0);
190        assert!(hooks.is_empty());
191
192        hooks.register(|_| {});
193        assert_eq!(hooks.len(), 1);
194        assert!(!hooks.is_empty());
195    }
196
197    #[test]
198    fn test_hook_triggering() {
199        let mut hooks = TemplateHooks::new();
200        let counter = Arc::new(AtomicUsize::new(0));
201        let counter_clone = counter.clone();
202
203        hooks.register(move |_| {
204            counter_clone.fetch_add(1, Ordering::SeqCst);
205        });
206
207        let event = TemplateEvent::Learned {
208            template_id: 256,
209            protocol: TemplateProtocol::V9,
210        };
211
212        hooks.trigger(&event);
213        assert_eq!(counter.load(Ordering::SeqCst), 1);
214
215        hooks.trigger(&event);
216        assert_eq!(counter.load(Ordering::SeqCst), 2);
217    }
218
219    #[test]
220    fn test_multiple_hooks() {
221        let mut hooks = TemplateHooks::new();
222        let counter1 = Arc::new(AtomicUsize::new(0));
223        let counter2 = Arc::new(AtomicUsize::new(0));
224
225        let c1 = counter1.clone();
226        let c2 = counter2.clone();
227
228        hooks.register(move |_| {
229            c1.fetch_add(1, Ordering::SeqCst);
230        });
231
232        hooks.register(move |_| {
233            c2.fetch_add(10, Ordering::SeqCst);
234        });
235
236        let event = TemplateEvent::Collision {
237            template_id: 300,
238            protocol: TemplateProtocol::Ipfix,
239        };
240
241        hooks.trigger(&event);
242
243        assert_eq!(counter1.load(Ordering::SeqCst), 1);
244        assert_eq!(counter2.load(Ordering::SeqCst), 10);
245    }
246
247    #[test]
248    fn test_hook_event_matching() {
249        let mut hooks = TemplateHooks::new();
250        let learned_count = Arc::new(AtomicUsize::new(0));
251        let collision_count = Arc::new(AtomicUsize::new(0));
252
253        let lc = learned_count.clone();
254        let cc = collision_count.clone();
255
256        hooks.register(move |event| match event {
257            TemplateEvent::Learned { .. } => {
258                lc.fetch_add(1, Ordering::SeqCst);
259            }
260            TemplateEvent::Collision { .. } => {
261                cc.fetch_add(1, Ordering::SeqCst);
262            }
263            _ => {}
264        });
265
266        hooks.trigger(&TemplateEvent::Learned {
267            template_id: 256,
268            protocol: TemplateProtocol::V9,
269        });
270        hooks.trigger(&TemplateEvent::Collision {
271            template_id: 300,
272            protocol: TemplateProtocol::Ipfix,
273        });
274        hooks.trigger(&TemplateEvent::Learned {
275            template_id: 400,
276            protocol: TemplateProtocol::V9,
277        });
278
279        assert_eq!(learned_count.load(Ordering::SeqCst), 2);
280        assert_eq!(collision_count.load(Ordering::SeqCst), 1);
281    }
282
283    #[test]
284    fn test_template_event_clone() {
285        let event = TemplateEvent::Evicted {
286            template_id: 500,
287            protocol: TemplateProtocol::Ipfix,
288        };
289
290        let cloned = event.clone();
291
292        match (event, cloned) {
293            (
294                TemplateEvent::Evicted {
295                    template_id: id1,
296                    protocol: p1,
297                },
298                TemplateEvent::Evicted {
299                    template_id: id2,
300                    protocol: p2,
301                },
302            ) => {
303                assert_eq!(id1, id2);
304                assert_eq!(p1, p2);
305            }
306            _ => panic!("Event didn't match after clone"),
307        }
308    }
309}