ops_rs/wrappers/
logging.rs1use crate::prelude::*;
2use crate::error::OpError;
6use crate::op::Op;
7use crate::{DryContext, OpMetadata, WetContext};
8use async_trait::async_trait;
9use std::time::Instant;
10use tracing;
11
12const 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 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 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 fn get_logger_name(&self) -> &str {
46 self.logger_name.as_deref().unwrap_or("LoggingWrapper")
47 }
48
49 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 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 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 self.log_op_start();
98
99 let result = self.wrapped_op.perform(dry, wet).await;
101
102 let duration = start_time.elapsed();
103
104 match &result {
106 Ok(_) => self.log_op_success(duration),
107 Err(error) => {
108 self.log_op_failure(error, duration);
109 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 self.wrapped_op.metadata()
123 }
124}
125
126pub 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 #[tokio::test]
156 async fn test0029_logging_wrapper_success() {
157 tracing_subscriber::fmt::try_init().ok(); 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 #[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 #[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 #[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}