data_hook/lib.rs
1//! # Data Hook
2//!
3//! A lightweight data collection facade similar to the `log` crate.
4//!
5//! The `data_hook` crate provides a simple API for collecting data that abstracts
6//! over the actual data processing implementation. Libraries can use the data collection
7//! API provided by this crate, and the consumer can choose the collector implementation
8//! that is most suitable for their use case.
9//!
10//! If no collector implementation is selected, the facade falls back to a "noop"
11//! implementation that ignores all data collection requests. The overhead in this case
12//! is very small - just an integer load, comparison and jump.
13//!
14//! # Usage
15//!
16//! The basic use of the data_hook crate is through the `data_hook!` macro:
17//!
18//! ```
19//! use data_hook::data_hook;
20//!
21//! data_hook!("user_action", "User clicked button: {}", "submit");
22//! data_hook!("performance", "Request took {} ms", 150);
23//! ```
24//!
25//! # Collectors
26//!
27//! Data hook supports pluggable collectors through the `Collect` trait.
28//! The consumer of libraries using data_hook should provide a collector implementation.
29
30use std::sync::OnceLock;
31
32/// A data record containing collected information
33#[derive(Debug, Clone)]
34pub struct Record {
35 /// Category or type of the data
36 pub category: String,
37 /// The actual data content
38 pub data: String,
39}
40
41impl Record {
42 /// Create a new data record
43 pub fn new(category: String, data: String) -> Self {
44 Self {
45 category,
46 data,
47 }
48 }
49}
50
51/// Trait for data collectors
52pub trait Collect: Send + Sync {
53 /// Process a collected data record
54 fn collect(&self, record: &Record);
55
56 /// Flush any buffered data (optional)
57 fn flush(&self) {}
58}
59
60/// A no-op collector that discards all data
61struct NoOpCollector;
62
63impl Collect for NoOpCollector {
64 fn collect(&self, _record: &Record) {
65 // Do nothing
66 }
67}
68
69/// Global collector instance
70static COLLECTOR: OnceLock<&'static dyn Collect> = OnceLock::new();
71
72/// Set the global data collector
73///
74/// This function should be called once at the start of the program to set up
75/// the data collection backend.
76pub fn set_collector(collector: &'static dyn Collect) {
77 let _ = COLLECTOR.set(collector);
78}
79
80/// Get the global collector, returning a no-op collector if none is set
81fn collector() -> &'static dyn Collect {
82 static NO_OP: NoOpCollector = NoOpCollector;
83 match COLLECTOR.get() {
84 Some(collector) => *collector,
85 None => &NO_OP,
86 }
87}
88
89/// Collect data using the global collector
90pub fn collect(record: Record) {
91 collector().collect(&record);
92}
93
94/// The main data collection macro
95///
96/// # Examples
97///
98/// Basic usage:
99/// ```
100/// use data_hook::data_hook;
101///
102/// data_hook!("user_action", "Button clicked");
103/// ```
104///
105/// With formatting:
106/// ```
107/// use data_hook::data_hook;
108///
109/// let user_id = 123;
110/// data_hook!("user_action", "User {} logged in", user_id);
111/// ```
112#[macro_export]
113macro_rules! data_hook {
114 ($category:expr, $($arg:tt)+) => {
115 $crate::collect($crate::Record::new(
116 $category.to_string(),
117 format!($($arg)+)
118 ))
119 };
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use std::sync::{Arc, Mutex};
126
127 struct TestCollector {
128 records: Arc<Mutex<Vec<Record>>>,
129 }
130
131 impl TestCollector {
132 fn new() -> (Self, Arc<Mutex<Vec<Record>>>) {
133 let records = Arc::new(Mutex::new(Vec::new()));
134 (Self { records: records.clone() }, records)
135 }
136 }
137
138 impl Collect for TestCollector {
139 fn collect(&self, record: &Record) {
140 if let Ok(mut records) = self.records.lock() {
141 records.push(record.clone());
142 }
143 }
144 }
145
146 #[test]
147 fn test_data_hook_macro() {
148 let (collector, records) = TestCollector::new();
149 let collector: &'static TestCollector = Box::leak(Box::new(collector));
150 set_collector(collector);
151
152 data_hook!("test", "Hello, world!");
153 data_hook!("test", "Number: {}", 42);
154
155 let records = records.lock().unwrap();
156 assert_eq!(records.len(), 2);
157 assert_eq!(records[0].category, "test");
158 assert_eq!(records[0].data, "Hello, world!");
159 assert_eq!(records[1].data, "Number: 42");
160 }
161
162 #[test]
163 fn test_record_creation() {
164 let record = Record::new("test_category".to_string(), "test data".to_string());
165 assert_eq!(record.category, "test_category");
166 assert_eq!(record.data, "test data");
167 }
168}