1use chrono::Utc;
10use std::{
11 env,
12 fs::{create_dir_all, metadata, File, OpenOptions},
13 path::Path,
14};
15use tracing::{info, subscriber::Interest, Metadata};
16use tracing_appender::non_blocking;
17use tracing_error::ErrorLayer;
18use tracing_subscriber::{
19 filter::LevelFilter,
20 fmt,
21 layer::{Context, Layer},
22 prelude::*,
23 EnvFilter,
24};
25
26use crate::constants::{
27 DEFAULT_LOG_DIR, DEFAULT_LOG_FORMAT, DEFAULT_LOG_LEVEL, DEFAULT_LOG_MODE,
28 DEFAULT_MAX_LOG_FILE_SIZE, DOCKER_LOG_DIR, LOG_FILE_NAME,
29};
30
31pub fn compute_rolled_file_path(base_file_path: &str, date_str: &str, index: u32) -> String {
33 if base_file_path.ends_with(".log") {
34 let trimmed = base_file_path.strip_suffix(".log").unwrap();
35 format!("{trimmed}-{date_str}.{index}.log")
36 } else {
37 format!("{base_file_path}-{date_str}.{index}.log")
38 }
39}
40
41pub fn time_based_rolling(base_file_path: &str, date_str: &str, index: u32) -> String {
44 compute_rolled_file_path(base_file_path, date_str, index)
45}
46
47pub fn space_based_rolling(
55 file_path: &str,
56 base_file_path: &str,
57 date_str: &str,
58 max_size: u64,
59) -> String {
60 let mut final_path = file_path.to_string();
61 let mut index = 1;
62 while let Ok(metadata) = metadata(&final_path) {
63 if metadata.len() > max_size {
64 final_path = compute_rolled_file_path(base_file_path, date_str, index);
65 index += 1;
66 } else {
67 break;
68 }
69 }
70 final_path
71}
72
73fn parse_level_filter(level: &str) -> LevelFilter {
76 match level.to_lowercase().as_str() {
77 "trace" => LevelFilter::TRACE,
78 "debug" => LevelFilter::DEBUG,
79 "info" => LevelFilter::INFO,
80 "warn" => LevelFilter::WARN,
81 "error" => LevelFilter::ERROR,
82 "off" => LevelFilter::OFF,
83 _ => {
84 tracing::warn!("Invalid log level '{level}', falling back to INFO");
85 LevelFilter::INFO
86 }
87 }
88}
89
90#[derive(Default)]
92struct SpanKeepaliveLayer;
93
94impl<S> Layer<S> for SpanKeepaliveLayer
95where
96 S: tracing::Subscriber,
97{
98 fn register_callsite(&self, metadata: &'static Metadata<'static>) -> Interest {
99 if metadata.is_span() {
100 Interest::always()
102 } else {
103 Interest::sometimes()
105 }
106 }
107
108 fn enabled(&self, _metadata: &Metadata<'_>, _ctx: Context<'_, S>) -> bool {
109 true
111 }
112}
113
114fn build_filter_directives() -> String {
117 const NOISY_CRATES: &[&str] = &[
118 "reqwest",
119 "hyper",
120 "rustls",
121 "h2",
122 "alloy_transport_http",
123 "aws_sdk_kms",
124 "solana_client",
125 "solana_program",
126 ];
127
128 let rust_log = env::var("RUST_LOG").unwrap_or_else(|_| DEFAULT_LOG_LEVEL.to_string());
129
130 let suppressions: Vec<String> = NOISY_CRATES
131 .iter()
132 .filter(|&crate_name| !rust_log.contains(crate_name))
133 .map(|name| format!("{name}=warn"))
134 .collect();
135
136 let mut directives = suppressions;
137 directives.push(rust_log);
138 directives.join(",")
139}
140
141fn build_env_filter(default_level: LevelFilter, directives: &str) -> EnvFilter {
142 EnvFilter::builder()
143 .with_default_directive(default_level.into())
144 .parse_lossy(directives)
145}
146
147pub fn setup_logging() {
149 if std::env::var_os("RUST_LOG").is_none() {
151 if let Ok(level) = env::var("LOG_LEVEL") {
152 std::env::set_var("RUST_LOG", level);
153 }
154 }
155
156 let default_level = parse_level_filter(DEFAULT_LOG_LEVEL);
159 let filter_directives = build_filter_directives();
160
161 let format = env::var("LOG_FORMAT").unwrap_or_else(|_| DEFAULT_LOG_FORMAT.to_string());
162 let log_mode = env::var("LOG_MODE").unwrap_or_else(|_| DEFAULT_LOG_MODE.to_string());
163
164 if log_mode.eq_ignore_ascii_case("file") {
166 let log_dir = if env::var("IN_DOCKER").ok().as_deref() == Some("true") {
168 DOCKER_LOG_DIR.to_string()
169 } else {
170 env::var("LOG_DATA_DIR").unwrap_or_else(|_| DEFAULT_LOG_DIR.to_string())
171 };
172 let log_dir = format!("{}/", log_dir.trim_end_matches('/'));
173
174 let now = Utc::now();
175 let date_str = now.format("%Y-%m-%d").to_string();
176 let base_file_path = format!("{log_dir}{LOG_FILE_NAME}");
177
178 if let Some(parent) = Path::new(&base_file_path).parent() {
179 create_dir_all(parent).expect("Failed to create log directory");
180 }
181
182 let time_based_path = time_based_rolling(&base_file_path, &date_str, 1);
183 let max_size = match env::var("LOG_MAX_SIZE") {
184 Ok(value) => value.parse().unwrap_or_else(|_| {
185 panic!("LOG_MAX_SIZE must be a valid u64 if set");
186 }),
187 Err(_) => DEFAULT_MAX_LOG_FILE_SIZE,
188 };
189 let final_path =
190 space_based_rolling(&time_based_path, &base_file_path, &date_str, max_size);
191
192 let file = if Path::new(&final_path).exists() {
193 OpenOptions::new()
194 .append(true)
195 .open(&final_path)
196 .expect("Failed to open log file")
197 } else {
198 File::create(&final_path).expect("Failed to create log file")
199 };
200
201 let (non_blocking_writer, guard) = non_blocking(file);
202 Box::leak(Box::new(guard)); match format.as_str() {
205 "pretty" => {
206 tracing_subscriber::registry()
207 .with(SpanKeepaliveLayer)
208 .with(ErrorLayer::default())
209 .with(
210 fmt::layer()
211 .with_writer(non_blocking_writer)
212 .with_ansi(false)
213 .pretty()
214 .with_thread_ids(true)
215 .with_file(true)
216 .with_line_number(true)
217 .with_filter(build_env_filter(default_level, &filter_directives)),
218 )
219 .init();
220 }
221 "json" => {
222 tracing_subscriber::registry()
223 .with(SpanKeepaliveLayer)
224 .with(ErrorLayer::default())
225 .with(
226 fmt::layer()
227 .with_writer(non_blocking_writer)
228 .with_ansi(false)
229 .json()
230 .with_current_span(true)
231 .with_span_list(true)
232 .with_thread_ids(true)
233 .with_file(true)
234 .with_line_number(true)
235 .with_filter(build_env_filter(default_level, &filter_directives)),
236 )
237 .init();
238 }
239 _ => {
240 tracing_subscriber::registry()
242 .with(SpanKeepaliveLayer)
243 .with(ErrorLayer::default())
244 .with(
245 fmt::layer()
246 .with_writer(non_blocking_writer)
247 .with_ansi(false)
248 .compact()
249 .with_target(false)
250 .with_filter(build_env_filter(default_level, &filter_directives)),
251 )
252 .init();
253 }
254 }
255 } else {
256 match format.as_str() {
258 "pretty" => {
259 tracing_subscriber::registry()
260 .with(SpanKeepaliveLayer)
261 .with(ErrorLayer::default())
262 .with(
263 fmt::layer()
264 .pretty()
265 .with_thread_ids(true)
266 .with_file(true)
267 .with_line_number(true)
268 .with_filter(build_env_filter(default_level, &filter_directives)),
269 )
270 .init();
271 }
272 "json" => {
273 tracing_subscriber::registry()
274 .with(SpanKeepaliveLayer)
275 .with(ErrorLayer::default())
276 .with(
277 fmt::layer()
278 .json()
279 .with_current_span(true)
280 .with_span_list(true)
281 .with_thread_ids(true)
282 .with_file(true)
283 .with_line_number(true)
284 .with_filter(build_env_filter(default_level, &filter_directives)),
285 )
286 .init();
287 }
288 _ => {
289 tracing_subscriber::registry()
291 .with(SpanKeepaliveLayer)
292 .with(ErrorLayer::default())
293 .with(
294 fmt::layer()
295 .compact()
296 .with_target(false)
297 .with_filter(build_env_filter(default_level, &filter_directives)),
298 )
299 .init();
300 }
301 }
302 }
303
304 info!(mode=%log_mode, format=%format, "logging configured");
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310 use std::fs::File;
311 use std::io::Write;
312 use std::sync::Mutex as StdMutex;
313 use std::sync::{Arc, Mutex, Once};
314 use tempfile::tempdir;
315 use tracing_subscriber::fmt::MakeWriter;
316
317 static INIT_LOGGER: Once = Once::new();
319
320 static ENV_MUTEX: Mutex<()> = Mutex::new(());
323
324 mod test_helpers {
326 use super::*;
327
328 pub struct TestWriter(pub Arc<StdMutex<Vec<u8>>>);
330
331 impl std::io::Write for TestWriter {
332 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
333 self.0.lock().unwrap().write(buf)
334 }
335 fn flush(&mut self) -> std::io::Result<()> {
336 self.0.lock().unwrap().flush()
337 }
338 }
339
340 pub struct TestMakeWriter(pub Arc<StdMutex<Vec<u8>>>);
343
344 impl<'a> MakeWriter<'a> for TestMakeWriter {
345 type Writer = TestWriter;
346 fn make_writer(&'a self) -> Self::Writer {
347 TestWriter(self.0.clone())
348 }
349 }
350
351 pub fn create_test_subscriber(
356 filter: EnvFilter,
357 ) -> (tracing::subscriber::DefaultGuard, Arc<StdMutex<Vec<u8>>>) {
358 let buffer = Arc::new(StdMutex::new(Vec::new()));
359 let buffer_clone = buffer.clone();
360
361 let subscriber = tracing_subscriber::fmt()
362 .with_env_filter(filter)
363 .with_writer(TestMakeWriter(buffer_clone))
364 .with_ansi(false)
365 .finish();
366
367 let guard = tracing::subscriber::set_default(subscriber);
368 (guard, buffer)
369 }
370
371 pub fn get_output(buffer: &Arc<StdMutex<Vec<u8>>>) -> String {
373 String::from_utf8(buffer.lock().unwrap().clone()).unwrap()
374 }
375 }
376
377 #[test]
378 fn test_compute_rolled_file_path() {
379 let result = compute_rolled_file_path("app.log", "2023-01-01", 1);
381 assert_eq!(result, "app-2023-01-01.1.log");
382
383 let result = compute_rolled_file_path("app", "2023-01-01", 2);
385 assert_eq!(result, "app-2023-01-01.2.log");
386
387 let result = compute_rolled_file_path("logs/app.log", "2023-01-01", 3);
389 assert_eq!(result, "logs/app-2023-01-01.3.log");
390 }
391
392 #[test]
393 fn test_time_based_rolling() {
394 let result = time_based_rolling("app.log", "2023-01-01", 1);
396 assert_eq!(result, "app-2023-01-01.1.log");
397 }
398
399 #[test]
400 fn test_space_based_rolling() {
401 let temp_dir = tempdir().expect("Failed to create temp directory");
403 let base_path = temp_dir
404 .path()
405 .join("test.log")
406 .to_str()
407 .unwrap()
408 .to_string();
409
410 let result = space_based_rolling(&base_path, &base_path, "2023-01-01", 100);
412 assert_eq!(result, base_path);
413
414 {
416 let mut file = File::create(&base_path).expect("Failed to create test file");
417 file.write_all(&[0; 200])
418 .expect("Failed to write to test file");
419 }
420
421 let expected_path = compute_rolled_file_path(&base_path, "2023-01-01", 1);
423 let result = space_based_rolling(&base_path, &base_path, "2023-01-01", 100);
424 assert_eq!(result, expected_path);
425
426 {
428 let mut file = File::create(&expected_path).expect("Failed to create test file");
429 file.write_all(&[0; 200])
430 .expect("Failed to write to test file");
431 }
432
433 let expected_path2 = compute_rolled_file_path(&base_path, "2023-01-01", 2);
435 let result = space_based_rolling(&base_path, &base_path, "2023-01-01", 100);
436 assert_eq!(result, expected_path2);
437 }
438
439 #[test]
440 fn test_logging_configuration() {
441 let _guard = ENV_MUTEX.lock().unwrap();
443
444 {
448 env::set_var("LOG_MODE", "stdout");
450 env::set_var("LOG_LEVEL", "debug");
451
452 INIT_LOGGER.call_once(|| {
454 setup_logging();
455 });
456
457 env::remove_var("LOG_MODE");
459 env::remove_var("LOG_LEVEL");
460 }
461
462 {
464 let temp_dir = tempdir().expect("Failed to create temp directory");
466 let log_path = temp_dir
467 .path()
468 .join("test_logs")
469 .to_str()
470 .unwrap()
471 .to_string();
472
473 env::set_var("LOG_MODE", "file");
475 env::set_var("LOG_LEVEL", "info");
476 env::set_var("LOG_DATA_DIR", &log_path);
477 env::set_var("LOG_MAX_SIZE", "1024"); if let Some(parent) = Path::new(&format!("{log_path}/relayer.log")).parent() {
481 create_dir_all(parent).expect("Failed to create log directory");
482 }
483
484 assert!(Path::new(&log_path).exists());
486
487 env::remove_var("LOG_MODE");
489 env::remove_var("LOG_LEVEL");
490 env::remove_var("LOG_DATA_DIR");
491 env::remove_var("LOG_MAX_SIZE");
492 }
493 }
494
495 #[test]
496 fn test_env_filter_with_no_rust_log() {
497 let _guard = ENV_MUTEX.lock().unwrap();
499
500 let original_rust_log = env::var("RUST_LOG").ok();
502
503 env::remove_var("RUST_LOG");
505
506 let default_level = parse_level_filter(DEFAULT_LOG_LEVEL);
508 let filter = EnvFilter::builder()
509 .with_default_directive(default_level.into())
510 .parse_lossy(build_filter_directives());
511
512 let (_subscriber_guard, buffer) = test_helpers::create_test_subscriber(filter);
514
515 tracing::info!(target: "openzeppelin_relayer", "app info message");
517 tracing::debug!(target: "openzeppelin_relayer", "app debug - should be suppressed (default is info)");
518 tracing::debug!(target: "reqwest", "reqwest debug - should be suppressed");
519 tracing::warn!(target: "reqwest", "reqwest warn - should be visible");
520 tracing::debug!(target: "hyper", "hyper debug - should be suppressed");
521 tracing::warn!(target: "hyper", "hyper warn - should be visible");
522
523 let output = test_helpers::get_output(&buffer);
525
526 assert!(
529 output.contains("app info message"),
530 "App info logs should be captured (default level is info)"
531 );
532 assert!(
533 !output.contains("app debug"),
534 "App debug logs should be suppressed (default level is info)"
535 );
536
537 assert!(
539 !output.contains("reqwest debug"),
540 "Reqwest debug logs should be suppressed"
541 );
542 assert!(
543 output.contains("reqwest warn"),
544 "Reqwest warn logs should be visible"
545 );
546 assert!(
547 !output.contains("hyper debug"),
548 "Hyper debug logs should be suppressed"
549 );
550 assert!(
551 output.contains("hyper warn"),
552 "Hyper warn logs should be visible"
553 );
554
555 if let Some(val) = original_rust_log {
557 env::set_var("RUST_LOG", val);
558 }
559 }
560
561 #[test]
562 fn test_env_filter_with_rust_log_set() {
563 let _guard = ENV_MUTEX.lock().unwrap();
565
566 let original_rust_log = env::var("RUST_LOG").ok();
568
569 env::set_var("RUST_LOG", "info");
571
572 let default_level = parse_level_filter(DEFAULT_LOG_LEVEL);
574 let filter = EnvFilter::builder()
575 .with_default_directive(default_level.into())
576 .parse_lossy(build_filter_directives());
577
578 let (_subscriber_guard, buffer) = test_helpers::create_test_subscriber(filter);
580
581 tracing::info!(target: "openzeppelin_relayer", "test app info");
583 tracing::debug!(target: "openzeppelin_relayer", "test app debug - should be suppressed");
584 tracing::debug!(target: "hyper", "test hyper debug - should be suppressed");
585 tracing::info!(target: "hyper", "test hyper info - should be suppressed");
586 tracing::warn!(target: "hyper", "test hyper warn - should be visible");
587
588 let output = test_helpers::get_output(&buffer);
590
591 assert!(
593 output.contains("test app info"),
594 "App info logs should be captured (RUST_LOG=info)"
595 );
596 assert!(
597 !output.contains("test app debug"),
598 "App debug logs should be suppressed (RUST_LOG=info)"
599 );
600 assert!(
601 !output.contains("test hyper debug"),
602 "Hyper debug logs should be suppressed"
603 );
604 assert!(
605 !output.contains("test hyper info"),
606 "Hyper info logs should be suppressed (hyper=warn)"
607 );
608 assert!(
609 output.contains("test hyper warn"),
610 "Hyper warn logs should be visible"
611 );
612
613 env::remove_var("RUST_LOG");
615 if let Some(val) = original_rust_log {
616 env::set_var("RUST_LOG", val);
617 }
618 }
619
620 #[test]
621 fn test_env_filter_with_explicit_reqwest_config() {
622 let _guard = ENV_MUTEX.lock().unwrap();
624
625 let original_rust_log = env::var("RUST_LOG").ok();
627
628 env::set_var("RUST_LOG", "info,reqwest=debug");
630
631 let default_level = parse_level_filter(DEFAULT_LOG_LEVEL);
633 let filter = EnvFilter::builder()
634 .with_default_directive(default_level.into())
635 .parse_lossy(build_filter_directives());
636
637 let (_subscriber_guard, buffer) = test_helpers::create_test_subscriber(filter);
639
640 tracing::debug!(target: "reqwest", "reqwest debug - user override");
642 tracing::debug!(target: "hyper", "hyper debug - should be suppressed");
643 tracing::warn!(target: "hyper", "hyper warn - should be visible");
644
645 let output = test_helpers::get_output(&buffer);
647
648 assert!(
650 output.contains("reqwest debug"),
651 "User's explicit reqwest=debug should allow debug logs"
652 );
653
654 assert!(
656 !output.contains("hyper debug"),
657 "Hyper debug logs should still be suppressed"
658 );
659 assert!(
660 output.contains("hyper warn"),
661 "Hyper warn logs should be visible"
662 );
663
664 env::remove_var("RUST_LOG");
666 if let Some(val) = original_rust_log {
667 env::set_var("RUST_LOG", val);
668 }
669 }
670
671 #[test]
672 fn test_env_filter_respects_user_overrides() {
673 let _guard = ENV_MUTEX.lock().unwrap();
675
676 let original_rust_log = env::var("RUST_LOG").ok();
678
679 env::set_var("RUST_LOG", "info,hyper=trace");
681
682 let default_level = parse_level_filter(DEFAULT_LOG_LEVEL);
684 let filter = EnvFilter::builder()
685 .with_default_directive(default_level.into())
686 .parse_lossy(build_filter_directives());
687
688 let (_subscriber_guard, buffer) = test_helpers::create_test_subscriber(filter);
690
691 tracing::trace!(target: "hyper", "test hyper trace - user override");
693 tracing::debug!(target: "hyper", "test hyper debug - user override");
694 tracing::debug!(target: "reqwest", "test reqwest debug - should be suppressed");
695 tracing::info!(target: "reqwest", "test reqwest info - should be suppressed");
696 tracing::warn!(target: "reqwest", "test reqwest warn - should be visible");
697
698 let output = test_helpers::get_output(&buffer);
700
701 assert!(
703 output.contains("test hyper trace"),
704 "User's explicit hyper=trace should allow trace logs"
705 );
706 assert!(
707 output.contains("test hyper debug"),
708 "User's explicit hyper=trace should allow debug logs"
709 );
710
711 assert!(
713 !output.contains("test reqwest debug"),
714 "Reqwest debug logs should be suppressed"
715 );
716 assert!(
717 !output.contains("test reqwest info"),
718 "Reqwest info logs should be suppressed"
719 );
720 assert!(
721 output.contains("test reqwest warn"),
722 "Reqwest warn logs should be visible"
723 );
724
725 env::remove_var("RUST_LOG");
727 if let Some(val) = original_rust_log {
728 env::set_var("RUST_LOG", val);
729 }
730 }
731
732 #[test]
733 fn test_env_filter_mixed_user_and_default_suppressions() {
734 let _guard = ENV_MUTEX.lock().unwrap();
736
737 let original_rust_log = env::var("RUST_LOG").ok();
739
740 env::set_var("RUST_LOG", "debug,reqwest=error,rustls=info");
742
743 let default_level = parse_level_filter(DEFAULT_LOG_LEVEL);
745 let filter = EnvFilter::builder()
746 .with_default_directive(default_level.into())
747 .parse_lossy(build_filter_directives());
748
749 let (_subscriber_guard, buffer) = test_helpers::create_test_subscriber(filter);
751
752 tracing::warn!(target: "reqwest", "test reqwest warn - user set error");
754 tracing::error!(target: "reqwest", "test reqwest error - should be visible");
755 tracing::debug!(target: "rustls", "test rustls debug - user set info");
756 tracing::info!(target: "rustls", "test rustls info - should be visible");
757 tracing::debug!(target: "hyper", "test hyper debug - should be suppressed");
758 tracing::info!(target: "hyper", "test hyper info - should be suppressed");
759 tracing::warn!(target: "hyper", "test hyper warn - should be visible");
760 tracing::debug!(target: "h2", "test h2 debug - should be suppressed");
761 tracing::warn!(target: "h2", "test h2 warn - should be visible");
762
763 let output = test_helpers::get_output(&buffer);
765
766 assert!(
768 !output.contains("test reqwest warn"),
769 "User's reqwest=error should reject warn logs"
770 );
771 assert!(
772 output.contains("test reqwest error"),
773 "User's reqwest=error should accept error logs"
774 );
775
776 assert!(
778 !output.contains("test rustls debug"),
779 "User's rustls=info should reject debug logs"
780 );
781 assert!(
782 output.contains("test rustls info"),
783 "User's rustls=info should accept info logs"
784 );
785
786 assert!(
788 !output.contains("test hyper debug"),
789 "Default hyper=warn should reject debug logs"
790 );
791 assert!(
792 !output.contains("test hyper info"),
793 "Default hyper=warn should reject info logs"
794 );
795 assert!(
796 output.contains("test hyper warn"),
797 "Default hyper=warn should accept warn logs"
798 );
799
800 assert!(
802 !output.contains("test h2 debug"),
803 "Default h2=warn should reject debug logs"
804 );
805 assert!(
806 output.contains("test h2 warn"),
807 "Default h2=warn should accept warn logs"
808 );
809
810 env::remove_var("RUST_LOG");
812 if let Some(val) = original_rust_log {
813 env::set_var("RUST_LOG", val);
814 }
815 }
816
817 #[test]
818 fn test_directive_parsing() {
819 assert!("reqwest=warn"
821 .parse::<tracing_subscriber::filter::Directive>()
822 .is_ok());
823 assert!("hyper=warn"
824 .parse::<tracing_subscriber::filter::Directive>()
825 .is_ok());
826 assert!("rustls=warn"
827 .parse::<tracing_subscriber::filter::Directive>()
828 .is_ok());
829 assert!("h2=warn"
830 .parse::<tracing_subscriber::filter::Directive>()
831 .is_ok());
832 assert!("alloy_transport_http=warn"
833 .parse::<tracing_subscriber::filter::Directive>()
834 .is_ok());
835 assert!("aws_sdk_kms=warn"
836 .parse::<tracing_subscriber::filter::Directive>()
837 .is_ok());
838 assert!("solana_client=warn"
839 .parse::<tracing_subscriber::filter::Directive>()
840 .is_ok());
841 assert!("solana_program=warn"
842 .parse::<tracing_subscriber::filter::Directive>()
843 .is_ok());
844 }
845
846 #[test]
847 fn test_parse_level_filter() {
848 assert_eq!(parse_level_filter("trace"), LevelFilter::TRACE);
850 assert_eq!(parse_level_filter("debug"), LevelFilter::DEBUG);
851 assert_eq!(parse_level_filter("info"), LevelFilter::INFO);
852 assert_eq!(parse_level_filter("warn"), LevelFilter::WARN);
853 assert_eq!(parse_level_filter("error"), LevelFilter::ERROR);
854 assert_eq!(parse_level_filter("off"), LevelFilter::OFF);
855
856 assert_eq!(parse_level_filter("TRACE"), LevelFilter::TRACE);
858 assert_eq!(parse_level_filter("Debug"), LevelFilter::DEBUG);
859 assert_eq!(parse_level_filter("INFO"), LevelFilter::INFO);
860 assert_eq!(parse_level_filter("WaRn"), LevelFilter::WARN);
861
862 assert_eq!(parse_level_filter("invalid"), LevelFilter::INFO);
864 assert_eq!(parse_level_filter(""), LevelFilter::INFO);
865 assert_eq!(parse_level_filter("warning"), LevelFilter::INFO);
866 }
867
868 #[test]
869 fn test_keepalive_layer_preserves_debug_span_context_at_warn_output() {
870 let _subscriber_guard = tracing::subscriber::set_default(
871 tracing_subscriber::registry()
872 .with(SpanKeepaliveLayer)
873 .with(
874 fmt::layer()
875 .with_writer(std::io::sink)
876 .with_filter(EnvFilter::new("warn")),
877 ),
878 );
879
880 let span = tracing::debug_span!("debug_ctx");
881 let _guard = span.enter();
882
883 crate::observability::request_id::set_request_id("keepalive-request-id");
884 assert_eq!(
885 crate::observability::request_id::get_request_id().as_deref(),
886 Some("keepalive-request-id")
887 );
888 }
889
890 #[test]
891 fn test_keepalive_layer_does_not_suppress_warn_events() {
892 let buffer = Arc::new(StdMutex::new(Vec::new()));
893 let subscriber = tracing_subscriber::registry()
894 .with(SpanKeepaliveLayer)
895 .with(
896 fmt::layer()
897 .with_writer(test_helpers::TestMakeWriter(buffer.clone()))
898 .with_ansi(false)
899 .with_filter(EnvFilter::new("warn")),
900 );
901 let _subscriber_guard = tracing::subscriber::set_default(subscriber);
902
903 tracing::warn!(target: "openzeppelin_relayer", "keepalive warn event");
904 let output = test_helpers::get_output(&buffer);
905 assert!(
906 output.contains("keepalive warn event"),
907 "Warn event should be captured when keepalive layer is used"
908 );
909 }
910
911 #[test]
912 fn test_build_filter_directives_no_rust_log() {
913 let _guard = ENV_MUTEX.lock().unwrap();
915
916 let original_rust_log = env::var("RUST_LOG").ok();
918
919 env::remove_var("RUST_LOG");
921
922 let result = build_filter_directives();
923
924 assert!(result.contains("reqwest=warn"));
926 assert!(result.contains("hyper=warn"));
927 assert!(result.contains("rustls=warn"));
928 assert!(result.contains("h2=warn"));
929 assert!(result.contains("alloy_transport_http=warn"));
930 assert!(result.contains("aws_sdk_kms=warn"));
931 assert!(result.contains("solana_client=warn"));
932 assert!(result.contains("solana_program=warn"));
933
934 assert!(result.ends_with(DEFAULT_LOG_LEVEL));
936
937 env::remove_var("RUST_LOG");
939 if let Some(val) = original_rust_log {
940 env::set_var("RUST_LOG", val);
941 }
942 }
943
944 #[test]
945 fn test_build_filter_directives_with_rust_log() {
946 let _guard = ENV_MUTEX.lock().unwrap();
948
949 let original_rust_log = env::var("RUST_LOG").ok();
951
952 env::set_var("RUST_LOG", "debug");
954
955 let result = build_filter_directives();
956
957 assert!(result.contains("reqwest=warn"));
959 assert!(result.contains("hyper=warn"));
960
961 assert!(result.ends_with("debug"));
963
964 env::remove_var("RUST_LOG");
966 if let Some(val) = original_rust_log {
967 env::set_var("RUST_LOG", val);
968 }
969 }
970
971 #[test]
972 fn test_build_filter_directives_respects_user_overrides() {
973 let _guard = ENV_MUTEX.lock().unwrap();
975
976 let original_rust_log = env::var("RUST_LOG").ok();
978
979 env::set_var("RUST_LOG", "info,reqwest=debug");
981
982 let result = build_filter_directives();
983
984 assert!(
986 !result.contains("reqwest=warn"),
987 "Should not add reqwest=warn when user configured reqwest. Result: {result}"
988 );
989
990 assert!(result.contains("hyper=warn"));
992 assert!(result.contains("rustls=warn"));
993
994 assert!(result.contains("info,reqwest=debug"));
996
997 env::remove_var("RUST_LOG");
999 if let Some(val) = original_rust_log {
1000 env::set_var("RUST_LOG", val);
1001 }
1002 }
1003
1004 #[test]
1005 fn test_build_filter_directives_multiple_user_overrides() {
1006 let _guard = ENV_MUTEX.lock().unwrap();
1008
1009 let original_rust_log = env::var("RUST_LOG").ok();
1011
1012 env::set_var("RUST_LOG", "debug,reqwest=error,hyper=trace,rustls=info");
1014
1015 let result = build_filter_directives();
1016
1017 assert!(
1019 !result.contains("reqwest=warn"),
1020 "Should not add reqwest=warn when user configured it"
1021 );
1022 assert!(
1023 !result.contains("hyper=warn"),
1024 "Should not add hyper=warn when user configured it"
1025 );
1026 assert!(
1027 !result.contains("rustls=warn"),
1028 "Should not add rustls=warn when user configured it"
1029 );
1030
1031 assert!(result.contains("h2=warn"));
1033 assert!(result.contains("alloy_transport_http=warn"));
1034 assert!(result.contains("aws_sdk_kms=warn"));
1035 assert!(result.contains("solana_client=warn"));
1036 assert!(result.contains("solana_program=warn"));
1037
1038 assert!(result.contains("debug,reqwest=error,hyper=trace,rustls=info"));
1040
1041 env::remove_var("RUST_LOG");
1043 if let Some(val) = original_rust_log {
1044 env::set_var("RUST_LOG", val);
1045 }
1046 }
1047
1048 #[test]
1049 fn test_build_filter_directives_format() {
1050 let _guard = ENV_MUTEX.lock().unwrap();
1052
1053 let original_rust_log = env::var("RUST_LOG").ok();
1055
1056 env::remove_var("RUST_LOG");
1058
1059 let result = build_filter_directives();
1060
1061 let parts: Vec<&str> = result.split(',').collect();
1063 assert!(
1064 parts.len() >= 8,
1065 "Should have at least 8 parts (suppressions)"
1066 );
1067
1068 for part in &parts {
1070 assert!(!part.is_empty(), "Should not have empty parts");
1071 }
1072
1073 assert!(!result.starts_with(','));
1075 assert!(!result.ends_with(','));
1076
1077 env::remove_var("RUST_LOG");
1079 if let Some(val) = original_rust_log {
1080 env::set_var("RUST_LOG", val);
1081 }
1082 }
1083}