kaccy_bitcoin/
structured_logging.rs

1//! Structured logging enhancements for Bitcoin operations
2//!
3//! This module provides enhanced logging capabilities including:
4//! - Request/response logging with correlation IDs
5//! - Performance tracing spans
6//! - Structured context for Bitcoin RPC operations
7
8use serde::{Deserialize, Serialize};
9use std::time::{Duration, Instant};
10use tracing::{Span, info, warn};
11use uuid::Uuid;
12
13/// Correlation ID for tracing requests across the system
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct CorrelationId(String);
16
17impl CorrelationId {
18    /// Generate a new correlation ID
19    pub fn new() -> Self {
20        Self(Uuid::new_v4().to_string())
21    }
22
23    /// Create from an existing string
24    pub fn from_string(id: impl Into<String>) -> Self {
25        Self(id.into())
26    }
27
28    /// Get the correlation ID as a string
29    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/// Context for logging Bitcoin RPC operations
47#[derive(Debug, Clone)]
48pub struct RpcLogContext {
49    /// Correlation ID for this request
50    pub correlation_id: CorrelationId,
51    /// RPC method name
52    pub method: String,
53    /// Optional transaction ID being operated on
54    pub txid: Option<String>,
55    /// Optional address being queried
56    pub address: Option<String>,
57    /// Start time of the operation
58    pub start_time: Instant,
59}
60
61impl RpcLogContext {
62    /// Create a new RPC log context
63    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    /// Set the transaction ID
74    pub fn with_txid(mut self, txid: impl Into<String>) -> Self {
75        self.txid = Some(txid.into());
76        self
77    }
78
79    /// Set the address
80    pub fn with_address(mut self, address: impl Into<String>) -> Self {
81        self.address = Some(address.into());
82        self
83    }
84
85    /// Get elapsed time since context creation
86    pub fn elapsed(&self) -> Duration {
87        self.start_time.elapsed()
88    }
89
90    /// Log the start of an RPC request
91    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    /// Log a successful RPC response
103    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    /// Log an RPC error
115    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
127/// Performance tracing span for Bitcoin operations
128pub struct PerformanceSpan {
129    /// Name of the operation
130    name: String,
131    /// Correlation ID
132    correlation_id: CorrelationId,
133    /// Start time
134    start_time: Instant,
135    /// Tracing span
136    _span: Span,
137}
138
139impl PerformanceSpan {
140    /// Create a new performance span
141    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    /// Create a span with an existing correlation ID
159    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    /// Get the correlation ID
176    pub fn correlation_id(&self) -> &CorrelationId {
177        &self.correlation_id
178    }
179
180    /// End the span and log the duration
181    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    /// End with error logging
192    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/// Request/response logger for Bitcoin operations
205#[derive(Debug)]
206pub struct RequestResponseLogger {
207    /// Whether to log request details
208    pub log_requests: bool,
209    /// Whether to log response details
210    pub log_responses: bool,
211    /// Maximum size of logged payloads (in bytes)
212    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, // 1 KB
221        }
222    }
223}
224
225impl RequestResponseLogger {
226    /// Create a new request/response logger
227    pub fn new() -> Self {
228        Self::default()
229    }
230
231    /// Configure request logging
232    #[allow(dead_code)]
233    pub fn with_requests(mut self, enabled: bool) -> Self {
234        self.log_requests = enabled;
235        self
236    }
237
238    /// Configure response logging
239    #[allow(dead_code)]
240    pub fn with_responses(mut self, enabled: bool) -> Self {
241        self.log_responses = enabled;
242        self
243    }
244
245    /// Configure maximum payload size to log
246    #[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    /// Log a request
253    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)", &params[..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    /// Log a response
273    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}