Skip to main content

ops_rs/wrappers/
logging.rs

1use crate::prelude::*;
2// LoggingWrapper implementation with full Java reference functionality
3// Implements LoggingOpWrapper.java patterns with Rust enhancements
4
5use crate::error::OpError;
6use crate::op::Op;
7use crate::{DryContext, OpMetadata, WetContext};
8use async_trait::async_trait;
9use std::time::Instant;
10use tracing;
11
12// ANSI color codes for console output (matches Java CONST class)
13const YELLOW: &str = "\x1b[33m";
14const GREEN: &str = "\x1b[32m";
15const RED: &str = "\x1b[31m";
16const RESET: &str = "\x1b[0m";
17
18pub struct LoggingWrapper<T> {
19    wrapped_op: Box<dyn Op<T>>,
20    trigger_name: String,
21    logger_name: Option<String>,
22}
23
24impl<T> LoggingWrapper<T> {
25    /// Create new logging wrapper with op name
26    pub fn new(op: Box<dyn Op<T>>, name: String) -> Self {
27        Self {
28            wrapped_op: op,
29            trigger_name: name,
30            logger_name: None,
31        }
32    }
33
34    /// Create logging wrapper with custom logger name
35    /// Equivalent to dynamic logger creation in Java
36    pub fn with_logger(op: Box<dyn Op<T>>, name: String, logger_name: String) -> Self {
37        Self {
38            wrapped_op: op,
39            trigger_name: name,
40            logger_name: Some(logger_name),
41        }
42    }
43
44    /// Get the effective logger name (for context-aware logging)
45    fn get_logger_name(&self) -> &str {
46        self.logger_name.as_deref().unwrap_or("LoggingWrapper")
47    }
48
49    /// Log op start with ANSI colors
50    fn log_op_start(&self) {
51        tracing::info!(
52            logger = self.get_logger_name(),
53            "{}Starting op: {}{}",
54            YELLOW,
55            self.trigger_name,
56            RESET
57        );
58    }
59
60    /// Log op completion with timing
61    fn log_op_success(&self, duration: std::time::Duration) {
62        let seconds = duration.as_secs_f64();
63        tracing::info!(
64            logger = self.get_logger_name(),
65            "{}Op '{}' completed in {:.3} seconds{}",
66            GREEN,
67            self.trigger_name,
68            seconds,
69            RESET
70        );
71    }
72
73    /// Log op failure with full error context
74    fn log_op_failure(&self, error: &OpError, duration: std::time::Duration) {
75        let seconds = duration.as_secs_f64();
76        tracing::error!(
77            logger = self.get_logger_name(),
78            "{}Op '{}' failed after {:.3} seconds: {:?}{}",
79            RED,
80            self.trigger_name,
81            seconds,
82            error,
83            RESET
84        );
85    }
86}
87
88#[async_trait]
89impl<T> Op<T> for LoggingWrapper<T>
90where
91    T: Send + 'static,
92{
93    async fn perform(&self, dry: &mut DryContext, wet: &mut WetContext) -> OpResult<T> {
94        let start_time = Instant::now();
95
96        // Log op start with yellow color
97        self.log_op_start();
98
99        // Execute wrapped op
100        let result = self.wrapped_op.perform(dry, wet).await;
101
102        let duration = start_time.elapsed();
103
104        // Log result with appropriate color and timing
105        match &result {
106            Ok(_) => self.log_op_success(duration),
107            Err(error) => {
108                self.log_op_failure(error, duration);
109                // Re-wrap error with op context (matches Java behavior)
110                return Err(crate::ops::wrap_nested_op_exception(
111                    &self.trigger_name,
112                    OpError::ExecutionFailed(format!("{:?}", error)),
113                ));
114            }
115        }
116
117        result
118    }
119
120    fn metadata(&self) -> OpMetadata {
121        // Pass through metadata from wrapped op
122        self.wrapped_op.metadata()
123    }
124}
125
126/// Create logger with dynamic name based on caller context
127/// Equivalent to Java's getCallerCallerClassName() usage
128pub fn create_context_aware_logger<T>(op: Box<dyn Op<T>>) -> LoggingWrapper<T>
129where
130    T: Send + 'static,
131{
132    let caller_name = crate::ops::get_caller_trigger_name();
133    LoggingWrapper::with_logger(op, caller_name.clone(), caller_name)
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use crate::op::Op;
140
141    struct TestOp;
142
143    #[async_trait]
144    impl Op<i32> for TestOp {
145        async fn perform(&self, _dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<i32> {
146            Ok(42)
147        }
148
149        fn metadata(&self) -> OpMetadata {
150            OpMetadata::builder("TestOp").build()
151        }
152    }
153
154    // TEST0029: Wrap a successful op in LoggingWrapper and verify it passes through the result unchanged
155    #[tokio::test]
156    async fn test0029_logging_wrapper_success() {
157        tracing_subscriber::fmt::try_init().ok(); // Initialize tracing for tests
158
159        let mut dry = DryContext::new();
160        let mut wet = WetContext::new();
161
162        let op = Box::new(TestOp);
163
164        let logging_wrapper = LoggingWrapper::new(op, "TestOp".to_string());
165        let result = logging_wrapper.perform(&mut dry, &mut wet).await;
166
167        assert!(result.is_ok());
168        assert_eq!(result.unwrap(), 42);
169    }
170
171    struct FailingOp;
172
173    #[async_trait]
174    impl Op<i32> for FailingOp {
175        async fn perform(&self, _dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<i32> {
176            Err(OpError::ExecutionFailed("test error".to_string()))
177        }
178
179        fn metadata(&self) -> OpMetadata {
180            OpMetadata::builder("FailingOp").build()
181        }
182    }
183
184    // TEST0030: Wrap a failing op in LoggingWrapper and verify the error includes the op name context
185    #[tokio::test]
186    async fn test0030_logging_wrapper_failure() {
187        tracing_subscriber::fmt::try_init().ok();
188
189        let mut dry = DryContext::new();
190        let mut wet = WetContext::new();
191
192        let op = Box::new(FailingOp);
193
194        let logging_wrapper: LoggingWrapper<i32> = LoggingWrapper::new(op, "FailingOp".to_string());
195        let result = logging_wrapper.perform(&mut dry, &mut wet).await;
196
197        assert!(result.is_err());
198        match result.unwrap_err() {
199            OpError::ExecutionFailed(msg) => {
200                assert!(msg.contains("FailingOp"));
201            }
202            _ => panic!("Expected ExecutionFailed error"),
203        }
204    }
205
206    struct StringOp;
207
208    #[async_trait]
209    impl Op<String> for StringOp {
210        async fn perform(&self, _dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<String> {
211            Ok("test".to_string())
212        }
213
214        fn metadata(&self) -> OpMetadata {
215            OpMetadata::builder("StringOp").build()
216        }
217    }
218
219    // TEST0031: Use create_context_aware_logger helper and verify the wrapped op returns its result
220    #[tokio::test]
221    async fn test0031_context_aware_logger() {
222        let mut dry = DryContext::new();
223        let mut wet = WetContext::new();
224
225        let op = Box::new(StringOp);
226
227        let logging_wrapper = create_context_aware_logger(op);
228        let result = logging_wrapper.perform(&mut dry, &mut wet).await;
229
230        assert!(result.is_ok());
231        assert_eq!(result.unwrap(), "test");
232    }
233
234    // TEST0032: Verify ANSI color escape code constants have the expected ANSI sequence values
235    #[test]
236    fn test0032_ansi_color_constants() {
237        assert_eq!(YELLOW, "\x1b[33m");
238        assert_eq!(GREEN, "\x1b[32m");
239        assert_eq!(RED, "\x1b[31m");
240        assert_eq!(RESET, "\x1b[0m");
241    }
242}