camel_cli/commands/
bench_instrument.rs1use std::fs::{File, OpenOptions};
22use std::io::Write;
23use std::sync::Arc;
24use std::sync::Mutex;
25use std::sync::atomic::{AtomicU64, Ordering};
26use std::time::Instant;
27
28use camel_api::{BoxProcessor, BoxProcessorExt, Exchange, OpaqueProcessor};
29use camel_core::{BuilderStep, RouteDefinition};
30use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode};
31
32const BENCH_URI_SAFE: &AsciiSet = &CONTROLS.add(b' ');
36
37const BENCH_START: &str = "BenchStart";
39
40pub fn maybe_instrument_routes(defs: Vec<RouteDefinition>) -> Vec<RouteDefinition> {
44 let Ok(path) = std::env::var("BENCH_LATENCY_FILE") else {
45 return defs;
46 };
47
48 let file = match OpenOptions::new().create(true).append(true).open(&path) {
49 Ok(f) => f,
50 Err(e) => {
51 tracing::error!("bench_instrument: cannot open BENCH_LATENCY_FILE '{path}': {e}");
53 return defs;
54 }
55 };
56 let shared_file = Arc::new(Mutex::new(file));
57
58 tracing::info!("bench_instrument: wrapping top-level To steps (file={path})");
59
60 defs.into_iter()
61 .map(|def| {
62 let sf = Arc::clone(&shared_file);
63 let route_id = def.route_id().to_string();
64 def.map_steps(|steps| inject_timing(steps, route_id, sf))
65 })
66 .collect()
67}
68
69fn inject_timing(
71 steps: Vec<BuilderStep>,
72 route_id: String,
73 file: Arc<Mutex<File>>,
74) -> Vec<BuilderStep> {
75 let mut result = Vec::with_capacity(steps.len() * 3);
76 for step in steps {
77 if let BuilderStep::To(uri) = step {
78 let counter = Arc::new(AtomicU64::new(0)); result.push(make_start_processor());
80 result.push(BuilderStep::To(uri.clone()));
81 result.push(make_end_processor(
82 route_id.clone(),
83 uri,
84 counter,
85 Arc::clone(&file),
86 ));
87 } else {
88 result.push(step);
89 }
90 }
91 result
92}
93
94fn make_start_processor() -> BuilderStep {
96 BuilderStep::Processor(OpaqueProcessor(BoxProcessor::from_fn(
97 |mut exchange: Exchange| {
98 Box::pin(async move {
99 exchange.set_extension(BENCH_START, Arc::new(Instant::now()));
100 Ok(exchange)
101 })
102 },
103 )))
104}
105
106fn make_end_processor(
110 route_id: String,
111 uri: String,
112 counter: Arc<AtomicU64>,
113 file: Arc<Mutex<File>>,
114) -> BuilderStep {
115 BuilderStep::Processor(OpaqueProcessor(BoxProcessor::from_fn(
116 move |exchange: Exchange| {
117 let counter = Arc::clone(&counter);
118 let file = Arc::clone(&file);
119 let route_id = route_id.clone();
120 let uri = uri.clone();
121 Box::pin(async move {
122 let id = counter.fetch_add(1, Ordering::Relaxed) + 1;
123 let duration_ns = exchange
124 .get_extension::<Instant>(BENCH_START)
125 .map(|t| t.elapsed().as_nanos() as u64)
126 .unwrap_or(0);
127 let line = format_bench_line(&route_id, &uri, id, duration_ns);
128 if let Ok(mut f) = file.lock() {
129 let _ = f.write_all(line.as_bytes());
130 }
131 Ok(exchange)
132 })
133 },
134 )))
135}
136
137fn format_bench_line(route_id: &str, uri: &str, id: u64, duration_ns: u64) -> String {
148 let route = if route_id.trim().is_empty() {
149 "-"
150 } else {
151 route_id
152 };
153 let encoded = utf8_percent_encode(uri, BENCH_URI_SAFE);
154 format!("BENCH_LATENCY {id} {duration_ns} {route} {encoded}\n")
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use std::fs::File;
161
162 #[test]
163 fn inject_timing_wraps_each_top_level_to() {
164 let tmp = tempfile::NamedTempFile::new().unwrap();
165 let file = Arc::new(Mutex::new(File::create(tmp.path()).unwrap()));
166
167 let steps = vec![
168 BuilderStep::To("xslt:a".into()),
169 BuilderStep::Stop,
170 BuilderStep::To("xslt:b".into()),
171 ];
172 let out = inject_timing(steps, "test-route".to_string(), file);
173
174 assert_eq!(out.len(), 7);
176 assert!(matches!(out[0], BuilderStep::Processor(_)));
177 assert!(matches!(out[1], BuilderStep::To(_)));
178 assert!(matches!(out[2], BuilderStep::Processor(_)));
179 assert!(matches!(out[3], BuilderStep::Stop));
180 assert!(matches!(out[4], BuilderStep::Processor(_)));
181 assert!(matches!(out[5], BuilderStep::To(_)));
182 assert!(matches!(out[6], BuilderStep::Processor(_)));
183 }
184
185 #[test]
186 fn inject_timing_skips_non_to_steps() {
187 let tmp = tempfile::NamedTempFile::new().unwrap();
188 let file = Arc::new(Mutex::new(File::create(tmp.path()).unwrap()));
189
190 let steps = vec![BuilderStep::Stop, BuilderStep::Stop];
191 let out = inject_timing(steps, "test-route".to_string(), file);
192 assert_eq!(out.len(), 2);
193 }
194
195 #[test]
196 fn maybe_instrument_routes_noop_when_env_unset() {
197 unsafe {
200 std::env::remove_var("BENCH_LATENCY_FILE");
201 }
202 let def = camel_core::RouteDefinition::new(
203 "direct:test".to_string(),
204 vec![BuilderStep::To("mock:a".into())],
205 );
206 let defs = vec![def];
207 let out = maybe_instrument_routes(defs);
208 assert_eq!(out[0].steps().len(), 1);
209 }
210
211 #[test]
212 fn format_bench_line_emits_route_id_and_percent_encoded_uri() {
213 assert_eq!(
215 format_bench_line("nacional-chain", "sql:noop?ds=cartodb", 1, 821_852_517),
216 "BENCH_LATENCY 1 821852517 nacional-chain sql:noop?ds=cartodb\n"
217 );
218 assert_eq!(
221 format_bench_line("r2", "http:host?q=a b", 3, 1000),
222 "BENCH_LATENCY 3 1000 r2 http:host?q=a%20b\n"
223 );
224 assert_eq!(
227 format_bench_line("", "direct:foo", 2, 500),
228 "BENCH_LATENCY 2 500 - direct:foo\n"
229 );
230 }
231}