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}