1use camel_api::{CamelError, Exchange, IdentityProcessor};
2use std::future::Future;
3use std::pin::Pin;
4use std::task::{Context, Poll};
5use tower::Service;
6use tracing::{debug, info, trace, warn};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum LogLevel {
10 Trace,
11 Debug,
12 Info,
13 Warn,
14 Error,
15}
16
17#[derive(Clone)]
18pub struct LogProcessor {
19 inner: IdentityProcessor,
20 level: LogLevel,
21 message: String,
22}
23
24impl LogProcessor {
29 pub fn new(level: LogLevel, message: String) -> Self {
30 Self {
31 inner: IdentityProcessor,
32 level,
33 message,
34 }
35 }
36}
37
38impl Service<Exchange> for LogProcessor {
39 type Response = Exchange;
40 type Error = CamelError;
41 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
42
43 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
44 self.inner.poll_ready(cx)
45 }
46
47 fn call(&mut self, exchange: Exchange) -> Self::Future {
48 let msg = self.message.clone();
49 let exchange_id = exchange.correlation_id.clone();
50 let body_preview = sanitize_preview(
51 &exchange
52 .input
53 .body
54 .as_text()
55 .unwrap_or("")
56 .chars()
57 .take(64)
58 .collect::<String>(),
59 );
60 debug!(exchange_id = %exchange_id, body_preview = %body_preview, "LogProcessor processing exchange");
61 match self.level {
62 LogLevel::Trace => trace!(exchange_id = %exchange_id, "{}", msg),
63 LogLevel::Debug => debug!(exchange_id = %exchange_id, "{}", msg),
64 LogLevel::Info => info!(exchange_id = %exchange_id, "{}", msg),
65 LogLevel::Warn => warn!(exchange_id = %exchange_id, "{}", msg),
66 LogLevel::Error => warn!(exchange_id = %exchange_id, "{}", msg),
68 }
69 self.inner.call(exchange)
70 }
71}
72
73#[derive(Clone)]
76pub struct DynamicLog<F> {
77 inner: IdentityProcessor,
78 level: LogLevel,
79 expr: F,
80}
81
82impl<F> DynamicLog<F>
83where
84 F: Fn(&Exchange) -> String + Clone + Send + Sync + 'static,
85{
86 pub fn new(level: LogLevel, expr: F) -> Self {
87 Self {
88 inner: IdentityProcessor,
89 level,
90 expr,
91 }
92 }
93}
94
95impl<F> Service<Exchange> for DynamicLog<F>
96where
97 F: Fn(&Exchange) -> String + Clone + Send + Sync + 'static,
98{
99 type Response = Exchange;
100 type Error = CamelError;
101 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
102
103 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
104 self.inner.poll_ready(cx)
105 }
106
107 fn call(&mut self, exchange: Exchange) -> Self::Future {
108 let exchange_id = exchange.correlation_id.clone();
109 let msg = (self.expr)(&exchange);
110 match self.level {
111 LogLevel::Trace => trace!(exchange_id = %exchange_id, "{}", msg),
112 LogLevel::Debug => debug!(exchange_id = %exchange_id, "{}", msg),
113 LogLevel::Info => info!(exchange_id = %exchange_id, "{}", msg),
114 LogLevel::Warn => warn!(exchange_id = %exchange_id, "{}", msg),
115 LogLevel::Error => warn!(exchange_id = %exchange_id, "{}", msg),
117 }
118 self.inner.call(exchange)
119 }
120}
121
122fn sanitize_preview(s: &str) -> String {
126 s.chars()
127 .map(|c| if c.is_control() { '\u{FFFD}' } else { c })
128 .collect()
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use camel_api::body::Body;
135 use camel_api::{Message, Value};
136 use tower::ServiceExt;
137
138 #[tokio::test]
139 async fn test_log_processor_passes_exchange_through() {
140 let mut processor = LogProcessor::new(LogLevel::Info, "test message".into());
141 let exchange = Exchange::default();
142 let result = processor.call(exchange).await;
143 assert!(result.is_ok());
144 }
145
146 #[tokio::test]
147 async fn test_log_processor_preserves_exchange_body() {
148 let mut processor = LogProcessor::new(LogLevel::Debug, "debug message".into());
149 let mut exchange = Exchange::default();
150 exchange.input.body = Body::Text("test body".into());
151 let result = processor.call(exchange).await.unwrap();
152 assert_eq!(result.input.body.as_text(), Some("test body"));
153 }
154
155 #[tokio::test]
156 async fn test_dynamic_log_evaluates_body() {
157 let svc = DynamicLog::new(LogLevel::Info, |ex: &Exchange| {
158 format!("body={}", ex.input.body.as_text().unwrap_or(""))
159 });
160 let exchange = Exchange::new(Message::new("hello"));
161 let result = svc.oneshot(exchange).await.unwrap();
162 assert_eq!(result.input.body.as_text(), Some("hello"));
164 }
165
166 #[tokio::test]
167 async fn test_dynamic_log_evaluates_header() {
168 let svc = DynamicLog::new(LogLevel::Info, |ex: &Exchange| {
169 let counter = ex
170 .input
171 .header("CamelTimerCounter")
172 .and_then(|v| v.as_i64())
173 .unwrap_or(0);
174 format!("{} World", counter)
175 });
176 let mut msg = Message::new("");
177 msg.set_header("CamelTimerCounter", Value::Number(42.into()));
178 let exchange = Exchange::new(msg);
179 let result = svc.oneshot(exchange).await.unwrap();
180 assert_eq!(
182 result.input.header("CamelTimerCounter"),
183 Some(&Value::Number(42.into()))
184 );
185 }
186
187 #[test]
188 fn body_preview_strips_control_chars() {
189 let body_with_injection = "no\ttab\nnl\rcr\0null\u{1B}esc";
190 let preview: String = body_with_injection.chars().take(64).collect();
191 let sanitized = sanitize_preview(&preview);
192 assert!(!sanitized.contains('\n'), "newlines must be replaced");
193 assert!(
194 !sanitized.contains('\r'),
195 "carriage returns must be replaced"
196 );
197 assert!(!sanitized.contains('\t'), "tabs must be replaced");
198 assert!(!sanitized.contains('\0'), "null must be replaced");
199 assert!(
200 !sanitized.contains('\u{1B}'),
201 "ESC (ANSI injection) must be replaced"
202 );
203 assert!(
205 sanitized.contains('\u{FFFD}'),
206 "control chars must be replaced with U+FFFD, not deleted"
207 );
208 }
209}