onemoney_protocol/client/
hooks.rs1use std::str;
4
5pub type RedactionCallback = Box<dyn Fn(&str) -> String + Send + Sync>;
8
9pub trait Hook: Send + Sync {
11 fn before_request(&self, method: &str, url: &str, body: Option<&str>);
13
14 fn after_response(&self, method: &str, url: &str, status: u16, body: Option<&str>);
16}
17
18pub trait Logger: Send + Sync {
20 fn log(&self, level: LogLevel, message: &str);
22}
23
24#[derive(Debug, Clone, Copy)]
26pub enum LogLevel {
27 Trace,
28 Debug,
29 Info,
30 Warn,
31 Error,
32}
33
34pub struct ConsoleLogger;
36
37impl Logger for ConsoleLogger {
38 fn log(&self, level: LogLevel, message: &str) {
39 match level {
40 LogLevel::Trace => {} LogLevel::Debug => {} LogLevel::Info => println!("[INFO] {}", message),
43 LogLevel::Warn => println!("[WARN] {}", message),
44 LogLevel::Error => println!("[ERROR] {}", message),
45 }
46 }
47}
48
49pub struct LoggingHook {
51 logger: Box<dyn Logger>,
52 redaction_callback: Option<RedactionCallback>,
53}
54
55impl LoggingHook {
56 pub fn new(logger: Box<dyn Logger>) -> Self {
57 Self {
58 logger,
59 redaction_callback: None,
60 }
61 }
62
63 pub fn with_redaction(logger: Box<dyn Logger>, redaction_callback: RedactionCallback) -> Self {
65 Self {
66 logger,
67 redaction_callback: Some(redaction_callback),
68 }
69 }
70
71 fn create_safe_preview(&self, body: &str) -> String {
74 if body.is_empty() {
75 return String::new();
76 }
77
78 let processed_body = if let Some(ref redactor) = self.redaction_callback {
80 redactor(body)
81 } else {
82 body.to_string()
83 };
84
85 if processed_body.chars().count() <= 100 {
88 processed_body
89 } else {
90 format!("{}...", processed_body.chars().take(100).collect::<String>())
91 }
92 }
93}
94
95impl Hook for LoggingHook {
96 fn before_request(&self, method: &str, url: &str, body: Option<&str>) {
97 if let Some(body) = body {
98 let safe_preview = self.create_safe_preview(body);
99 if safe_preview.is_empty() {
100 self.logger
101 .log(LogLevel::Debug, &format!("-> {} {} with body: <empty>", method, url));
102 } else {
103 self.logger.log(
104 LogLevel::Debug,
105 &format!("-> {} {} with body: {}", method, url, safe_preview),
106 );
107 }
108 } else {
109 self.logger.log(LogLevel::Debug, &format!("-> {} {}", method, url));
110 }
111 }
112
113 fn after_response(&self, method: &str, url: &str, status: u16, body: Option<&str>) {
114 if let Some(body) = body {
115 let safe_preview = self.create_safe_preview(body);
116 if safe_preview.is_empty() {
117 self.logger.log(
118 LogLevel::Debug,
119 &format!("<- {} {} [{}] body: <empty>", method, url, status),
120 );
121 } else {
122 self.logger.log(
123 LogLevel::Debug,
124 &format!("<- {} {} [{}] body: {}", method, url, status, safe_preview),
125 );
126 }
127 } else {
128 self.logger
129 .log(LogLevel::Debug, &format!("<- {} {} [{}]", method, url, status));
130 }
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use std::sync::{Arc, Mutex};
137
138 use super::*;
139
140 #[derive(Clone)]
141 struct TestLogger {
142 messages: Arc<Mutex<Vec<(LogLevel, String)>>>,
143 }
144
145 impl TestLogger {
146 fn new() -> Self {
147 Self {
148 messages: Arc::new(Mutex::new(Vec::new())),
149 }
150 }
151
152 fn get_messages(&self) -> Vec<(LogLevel, String)> {
153 self.messages.lock().expect("Failed to lock messages mutex").clone()
154 }
155 }
156
157 impl Logger for TestLogger {
158 fn log(&self, level: LogLevel, message: &str) {
159 self.messages
160 .lock()
161 .expect("Failed to lock messages mutex")
162 .push((level, message.to_string()));
163 }
164 }
165
166 impl Logger for Arc<TestLogger> {
167 fn log(&self, level: LogLevel, message: &str) {
168 self.as_ref().log(level, message);
169 }
170 }
171
172 #[test]
173 fn test_safe_preview_short_body() {
174 let logger = Box::new(TestLogger::new());
175 let hook = LoggingHook::new(logger);
176
177 let short_body = "short message";
178 let preview = hook.create_safe_preview(short_body);
179 assert_eq!(preview, "short message");
180 }
181
182 #[test]
183 fn test_safe_preview_long_body() {
184 let logger = Box::new(TestLogger::new());
185 let hook = LoggingHook::new(logger);
186
187 let long_body = "a".repeat(150);
188 let preview = hook.create_safe_preview(&long_body);
189 assert_eq!(preview.len(), 103); assert!(preview.ends_with("..."));
191 assert_eq!(&preview[..100], &"a".repeat(100));
192 }
193
194 #[test]
195 fn test_safe_preview_empty_body() {
196 let logger = Box::new(TestLogger::new());
197 let hook = LoggingHook::new(logger);
198
199 let preview = hook.create_safe_preview("");
200 assert_eq!(preview, "");
201 }
202
203 #[test]
204 fn test_redaction_callback() {
205 let logger = Box::new(TestLogger::new());
206 let redactor = Box::new(|body: &str| {
207 body.replace("secret123", "***REDACTED***")
208 .replace("token123", "***REDACTED***")
209 .replace("password", "***REDACTED***")
210 .replace("Authorization", "***REDACTED***")
211 });
212 let hook = LoggingHook::with_redaction(logger, redactor);
213
214 let sensitive_body = r#"{"username": "john", "password": "secret123", "Authorization": "Bearer token123"}"#;
215 let preview = hook.create_safe_preview(sensitive_body);
216
217 assert!(!preview.contains("secret123"));
218 assert!(!preview.contains("token123"));
219 assert!(preview.contains("***REDACTED***"));
220 }
221
222 #[test]
223 fn test_before_request_with_empty_body() {
224 let logger = Arc::new(TestLogger::new());
225 let hook = LoggingHook::new(Box::new(logger.clone()));
226
227 hook.before_request("GET", "https://api.example.com", Some(""));
228
229 let messages = logger.get_messages();
230 assert_eq!(messages.len(), 1);
231 assert!(messages[0].1.contains("<empty>"));
232 }
233
234 #[test]
235 fn test_before_request_with_long_body() {
236 let logger = Arc::new(TestLogger::new());
237 let hook = LoggingHook::new(Box::new(logger.clone()));
238
239 let long_body = "x".repeat(150);
240 hook.before_request("POST", "https://api.example.com", Some(&long_body));
241
242 let messages = logger.get_messages();
243 assert_eq!(messages.len(), 1);
244 assert!(messages[0].1.contains("..."));
245 assert!(messages[0].1.len() < long_body.len() + 50); }
247
248 #[test]
249 fn test_after_response_with_redaction() {
250 let logger = Arc::new(TestLogger::new());
251 let redactor = Box::new(|body: &str| {
252 body.replace("0x123456789abcdef", "***REDACTED***")
253 .replace("private_key", "***REDACTED***")
254 });
255 let hook = LoggingHook::with_redaction(Box::new(logger.clone()), redactor);
256
257 let response_body = r#"{"success": true, "private_key": "0x123456789abcdef"}"#;
258 hook.after_response("POST", "https://api.example.com", 200, Some(response_body));
259
260 let messages = logger.get_messages();
261 assert_eq!(messages.len(), 1);
262 assert!(!messages[0].1.contains("0x123456789abcdef"));
263 assert!(messages[0].1.contains("***REDACTED***"));
264 }
265
266 #[test]
267 fn test_safe_preview_with_multibyte_characters() {
268 let logger = Box::new(TestLogger::new());
269 let hook = LoggingHook::new(logger);
270
271 let multibyte_body = "Hello world with accents: café résumé naïve".repeat(5); let preview = hook.create_safe_preview(&multibyte_body);
274
275 assert!(preview.ends_with("..."));
277 assert!(preview.chars().count() <= 103); assert!(preview.is_ascii() || str::from_utf8(preview.as_bytes()).is_ok());
281
282 let exactly_100_chars = "a".repeat(97) + "xyz"; let preview_exact = hook.create_safe_preview(&exactly_100_chars);
285 assert_eq!(preview_exact, exactly_100_chars);
286 assert!(!preview_exact.contains("..."));
287
288 let short_multibyte = "Hello world!";
290 let preview_short = hook.create_safe_preview(short_multibyte);
291 assert_eq!(preview_short, short_multibyte);
292 assert!(!preview_short.contains("..."));
293 }
294}