use serde::{Deserialize, Serialize};
use std::time::{Duration, Instant};
use tracing::{Span, info, warn};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CorrelationId(String);
impl CorrelationId {
pub fn new() -> Self {
Self(Uuid::new_v4().to_string())
}
pub fn from_string(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Default for CorrelationId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for CorrelationId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone)]
pub struct RpcLogContext {
pub correlation_id: CorrelationId,
pub method: String,
pub txid: Option<String>,
pub address: Option<String>,
pub start_time: Instant,
}
impl RpcLogContext {
pub fn new(method: impl Into<String>) -> Self {
Self {
correlation_id: CorrelationId::new(),
method: method.into(),
txid: None,
address: None,
start_time: Instant::now(),
}
}
pub fn with_txid(mut self, txid: impl Into<String>) -> Self {
self.txid = Some(txid.into());
self
}
pub fn with_address(mut self, address: impl Into<String>) -> Self {
self.address = Some(address.into());
self
}
pub fn elapsed(&self) -> Duration {
self.start_time.elapsed()
}
pub fn log_request(&self, params: &str) {
info!(
correlation_id = %self.correlation_id,
method = %self.method,
txid = ?self.txid,
address = ?self.address,
params = %params,
"RPC request started"
);
}
pub fn log_response(&self, response_size: usize) {
let duration_ms = self.elapsed().as_millis();
info!(
correlation_id = %self.correlation_id,
method = %self.method,
duration_ms = duration_ms,
response_size = response_size,
"RPC request completed successfully"
);
}
pub fn log_error(&self, error: &str) {
let duration_ms = self.elapsed().as_millis();
warn!(
correlation_id = %self.correlation_id,
method = %self.method,
duration_ms = duration_ms,
error = %error,
"RPC request failed"
);
}
}
pub struct PerformanceSpan {
name: String,
correlation_id: CorrelationId,
start_time: Instant,
_span: Span,
}
impl PerformanceSpan {
pub fn new(name: impl Into<String>) -> Self {
let name = name.into();
let correlation_id = CorrelationId::new();
let _span = tracing::info_span!(
"performance",
operation = %name,
correlation_id = %correlation_id
);
Self {
name,
correlation_id,
start_time: Instant::now(),
_span,
}
}
pub fn with_correlation_id(name: impl Into<String>, correlation_id: CorrelationId) -> Self {
let name = name.into();
let _span = tracing::info_span!(
"performance",
operation = %name,
correlation_id = %correlation_id
);
Self {
name,
correlation_id,
start_time: Instant::now(),
_span,
}
}
pub fn correlation_id(&self) -> &CorrelationId {
&self.correlation_id
}
pub fn end(self) {
let duration_ms = self.start_time.elapsed().as_millis();
info!(
operation = %self.name,
correlation_id = %self.correlation_id,
duration_ms = duration_ms,
"Operation completed"
);
}
pub fn end_with_error(self, error: &str) {
let duration_ms = self.start_time.elapsed().as_millis();
warn!(
operation = %self.name,
correlation_id = %self.correlation_id,
duration_ms = duration_ms,
error = %error,
"Operation failed"
);
}
}
#[derive(Debug)]
pub struct RequestResponseLogger {
pub log_requests: bool,
pub log_responses: bool,
pub max_payload_size: usize,
}
impl Default for RequestResponseLogger {
fn default() -> Self {
Self {
log_requests: true,
log_responses: true,
max_payload_size: 1024, }
}
}
impl RequestResponseLogger {
pub fn new() -> Self {
Self::default()
}
#[allow(dead_code)]
pub fn with_requests(mut self, enabled: bool) -> Self {
self.log_requests = enabled;
self
}
#[allow(dead_code)]
pub fn with_responses(mut self, enabled: bool) -> Self {
self.log_responses = enabled;
self
}
#[allow(dead_code)]
pub fn with_max_payload_size(mut self, size: usize) -> Self {
self.max_payload_size = size;
self
}
pub fn log_request(&self, context: &RpcLogContext, method: &str, params: &str) {
if !self.log_requests {
return;
}
let truncated_params = if params.len() > self.max_payload_size {
format!("{}... (truncated)", ¶ms[..self.max_payload_size])
} else {
params.to_string()
};
info!(
correlation_id = %context.correlation_id,
method = %method,
params = %truncated_params,
"Bitcoin RPC request"
);
}
pub fn log_response(&self, context: &RpcLogContext, success: bool, response_data: &str) {
if !self.log_responses {
return;
}
let truncated_response = if response_data.len() > self.max_payload_size {
format!("{}... (truncated)", &response_data[..self.max_payload_size])
} else {
response_data.to_string()
};
let duration_ms = context.elapsed().as_millis();
if success {
info!(
correlation_id = %context.correlation_id,
method = %context.method,
duration_ms = duration_ms,
response = %truncated_response,
"Bitcoin RPC response (success)"
);
} else {
warn!(
correlation_id = %context.correlation_id,
method = %context.method,
duration_ms = duration_ms,
error = %truncated_response,
"Bitcoin RPC response (error)"
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_correlation_id_generation() {
let id1 = CorrelationId::new();
let id2 = CorrelationId::new();
assert_ne!(id1.as_str(), id2.as_str());
}
#[test]
fn test_correlation_id_from_string() {
let id = CorrelationId::from_string("test-123");
assert_eq!(id.as_str(), "test-123");
}
#[test]
fn test_rpc_log_context_creation() {
let context = RpcLogContext::new("getblock");
assert_eq!(context.method, "getblock");
assert!(context.txid.is_none());
assert!(context.address.is_none());
}
#[test]
fn test_rpc_log_context_with_txid() {
let context = RpcLogContext::new("getrawtransaction").with_txid("abc123");
assert_eq!(context.txid, Some("abc123".to_string()));
}
#[test]
fn test_rpc_log_context_with_address() {
let context = RpcLogContext::new("getreceivedbyaddress")
.with_address("bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh");
assert!(context.address.is_some());
}
#[test]
fn test_request_response_logger_default() {
let logger = RequestResponseLogger::default();
assert!(logger.log_requests);
assert!(logger.log_responses);
assert_eq!(logger.max_payload_size, 1024);
}
#[test]
fn test_performance_span_correlation_id() {
let span = PerformanceSpan::new("test_operation");
assert!(!span.correlation_id().as_str().is_empty());
}
#[test]
fn test_performance_span_with_correlation_id() {
let corr_id = CorrelationId::from_string("test-correlation-id");
let span = PerformanceSpan::with_correlation_id("test_op", corr_id.clone());
assert_eq!(span.correlation_id().as_str(), corr_id.as_str());
}
}