1use serde::{Deserialize, Serialize};
9use std::time::{Duration, Instant};
10use tracing::{Span, info, warn};
11use uuid::Uuid;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct CorrelationId(String);
16
17impl CorrelationId {
18 pub fn new() -> Self {
20 Self(Uuid::new_v4().to_string())
21 }
22
23 pub fn from_string(id: impl Into<String>) -> Self {
25 Self(id.into())
26 }
27
28 pub fn as_str(&self) -> &str {
30 &self.0
31 }
32}
33
34impl Default for CorrelationId {
35 fn default() -> Self {
36 Self::new()
37 }
38}
39
40impl std::fmt::Display for CorrelationId {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 write!(f, "{}", self.0)
43 }
44}
45
46#[derive(Debug, Clone)]
48pub struct RpcLogContext {
49 pub correlation_id: CorrelationId,
51 pub method: String,
53 pub txid: Option<String>,
55 pub address: Option<String>,
57 pub start_time: Instant,
59}
60
61impl RpcLogContext {
62 pub fn new(method: impl Into<String>) -> Self {
64 Self {
65 correlation_id: CorrelationId::new(),
66 method: method.into(),
67 txid: None,
68 address: None,
69 start_time: Instant::now(),
70 }
71 }
72
73 pub fn with_txid(mut self, txid: impl Into<String>) -> Self {
75 self.txid = Some(txid.into());
76 self
77 }
78
79 pub fn with_address(mut self, address: impl Into<String>) -> Self {
81 self.address = Some(address.into());
82 self
83 }
84
85 pub fn elapsed(&self) -> Duration {
87 self.start_time.elapsed()
88 }
89
90 pub fn log_request(&self, params: &str) {
92 info!(
93 correlation_id = %self.correlation_id,
94 method = %self.method,
95 txid = ?self.txid,
96 address = ?self.address,
97 params = %params,
98 "RPC request started"
99 );
100 }
101
102 pub fn log_response(&self, response_size: usize) {
104 let duration_ms = self.elapsed().as_millis();
105 info!(
106 correlation_id = %self.correlation_id,
107 method = %self.method,
108 duration_ms = duration_ms,
109 response_size = response_size,
110 "RPC request completed successfully"
111 );
112 }
113
114 pub fn log_error(&self, error: &str) {
116 let duration_ms = self.elapsed().as_millis();
117 warn!(
118 correlation_id = %self.correlation_id,
119 method = %self.method,
120 duration_ms = duration_ms,
121 error = %error,
122 "RPC request failed"
123 );
124 }
125}
126
127pub struct PerformanceSpan {
129 name: String,
131 correlation_id: CorrelationId,
133 start_time: Instant,
135 _span: Span,
137}
138
139impl PerformanceSpan {
140 pub fn new(name: impl Into<String>) -> Self {
142 let name = name.into();
143 let correlation_id = CorrelationId::new();
144 let _span = tracing::info_span!(
145 "performance",
146 operation = %name,
147 correlation_id = %correlation_id
148 );
149
150 Self {
151 name,
152 correlation_id,
153 start_time: Instant::now(),
154 _span,
155 }
156 }
157
158 pub fn with_correlation_id(name: impl Into<String>, correlation_id: CorrelationId) -> Self {
160 let name = name.into();
161 let _span = tracing::info_span!(
162 "performance",
163 operation = %name,
164 correlation_id = %correlation_id
165 );
166
167 Self {
168 name,
169 correlation_id,
170 start_time: Instant::now(),
171 _span,
172 }
173 }
174
175 pub fn correlation_id(&self) -> &CorrelationId {
177 &self.correlation_id
178 }
179
180 pub fn end(self) {
182 let duration_ms = self.start_time.elapsed().as_millis();
183 info!(
184 operation = %self.name,
185 correlation_id = %self.correlation_id,
186 duration_ms = duration_ms,
187 "Operation completed"
188 );
189 }
190
191 pub fn end_with_error(self, error: &str) {
193 let duration_ms = self.start_time.elapsed().as_millis();
194 warn!(
195 operation = %self.name,
196 correlation_id = %self.correlation_id,
197 duration_ms = duration_ms,
198 error = %error,
199 "Operation failed"
200 );
201 }
202}
203
204#[derive(Debug)]
206pub struct RequestResponseLogger {
207 pub log_requests: bool,
209 pub log_responses: bool,
211 pub max_payload_size: usize,
213}
214
215impl Default for RequestResponseLogger {
216 fn default() -> Self {
217 Self {
218 log_requests: true,
219 log_responses: true,
220 max_payload_size: 1024, }
222 }
223}
224
225impl RequestResponseLogger {
226 pub fn new() -> Self {
228 Self::default()
229 }
230
231 #[allow(dead_code)]
233 pub fn with_requests(mut self, enabled: bool) -> Self {
234 self.log_requests = enabled;
235 self
236 }
237
238 #[allow(dead_code)]
240 pub fn with_responses(mut self, enabled: bool) -> Self {
241 self.log_responses = enabled;
242 self
243 }
244
245 #[allow(dead_code)]
247 pub fn with_max_payload_size(mut self, size: usize) -> Self {
248 self.max_payload_size = size;
249 self
250 }
251
252 pub fn log_request(&self, context: &RpcLogContext, method: &str, params: &str) {
254 if !self.log_requests {
255 return;
256 }
257
258 let truncated_params = if params.len() > self.max_payload_size {
259 format!("{}... (truncated)", ¶ms[..self.max_payload_size])
260 } else {
261 params.to_string()
262 };
263
264 info!(
265 correlation_id = %context.correlation_id,
266 method = %method,
267 params = %truncated_params,
268 "Bitcoin RPC request"
269 );
270 }
271
272 pub fn log_response(&self, context: &RpcLogContext, success: bool, response_data: &str) {
274 if !self.log_responses {
275 return;
276 }
277
278 let truncated_response = if response_data.len() > self.max_payload_size {
279 format!("{}... (truncated)", &response_data[..self.max_payload_size])
280 } else {
281 response_data.to_string()
282 };
283
284 let duration_ms = context.elapsed().as_millis();
285
286 if success {
287 info!(
288 correlation_id = %context.correlation_id,
289 method = %context.method,
290 duration_ms = duration_ms,
291 response = %truncated_response,
292 "Bitcoin RPC response (success)"
293 );
294 } else {
295 warn!(
296 correlation_id = %context.correlation_id,
297 method = %context.method,
298 duration_ms = duration_ms,
299 error = %truncated_response,
300 "Bitcoin RPC response (error)"
301 );
302 }
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309
310 #[test]
311 fn test_correlation_id_generation() {
312 let id1 = CorrelationId::new();
313 let id2 = CorrelationId::new();
314 assert_ne!(id1.as_str(), id2.as_str());
315 }
316
317 #[test]
318 fn test_correlation_id_from_string() {
319 let id = CorrelationId::from_string("test-123");
320 assert_eq!(id.as_str(), "test-123");
321 }
322
323 #[test]
324 fn test_rpc_log_context_creation() {
325 let context = RpcLogContext::new("getblock");
326 assert_eq!(context.method, "getblock");
327 assert!(context.txid.is_none());
328 assert!(context.address.is_none());
329 }
330
331 #[test]
332 fn test_rpc_log_context_with_txid() {
333 let context = RpcLogContext::new("getrawtransaction").with_txid("abc123");
334 assert_eq!(context.txid, Some("abc123".to_string()));
335 }
336
337 #[test]
338 fn test_rpc_log_context_with_address() {
339 let context = RpcLogContext::new("getreceivedbyaddress")
340 .with_address("bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh");
341 assert!(context.address.is_some());
342 }
343
344 #[test]
345 fn test_request_response_logger_default() {
346 let logger = RequestResponseLogger::default();
347 assert!(logger.log_requests);
348 assert!(logger.log_responses);
349 assert_eq!(logger.max_payload_size, 1024);
350 }
351
352 #[test]
353 fn test_performance_span_correlation_id() {
354 let span = PerformanceSpan::new("test_operation");
355 assert!(!span.correlation_id().as_str().is_empty());
356 }
357
358 #[test]
359 fn test_performance_span_with_correlation_id() {
360 let corr_id = CorrelationId::from_string("test-correlation-id");
361 let span = PerformanceSpan::with_correlation_id("test_op", corr_id.clone());
362 assert_eq!(span.correlation_id().as_str(), corr_id.as_str());
363 }
364}