// file: kb-logging/src/tracing_runtime.rs // version: 16 //! Runtime initialization helpers for `tracing_subscriber`. use tracing_subscriber::Layer; // rust-rules: trait-import use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import use tracing_subscriber::util::SubscriberInitExt; // rust-rules: trait-import /// Guard keeping non-blocking logging workers alive. #[derive(Debug)] pub struct LoggingGuard { guards: std::vec::Vec, route_names: std::vec::Vec, } impl crate::LoggingGuard { /// Returns the number of active non-blocking writer guards. pub fn guard_count(&self) -> usize { return self.guards.len(); } /// Returns the number of enabled logging routes installed at startup. pub fn route_count(&self) -> usize { return self.route_names.len(); } /// Returns the configured names of the enabled routes installed at startup. pub fn route_names(&self) -> &[std::string::String] { return &self.route_names; } } type BoxedLayer = std::boxed::Box< dyn tracing_subscriber::Layer + std::marker::Send + std::marker::Sync, >; /// Initializes global tracing from a logging configuration. pub fn init_logging(config: &crate::LoggingConfig) -> kb_core::Result { let mut guards = std::vec::Vec::::new(); let mut route_names = std::vec::Vec::::new(); let layers_result = build_enabled_layers(config, &mut guards, &mut route_names); let layers = match layers_result { std::result::Result::Ok(layers) => layers, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if layers.is_empty() { return std::result::Result::Err(kb_core::Error::new( "logging_no_enabled_routes", "at least one logging target must be enabled", )); } let admission_filters_result = build_enabled_route_writer_filters(config); let admission_filters = match admission_filters_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let admission_filter = tracing_subscriber::filter::filter_fn(move |metadata| { return admission_filters.iter().any(|filter| return filter.allows(metadata)); }); let init_result = tracing_subscriber::registry() .with(layers.with_filter(admission_filter)) .try_init(); return match init_result { std::result::Result::Ok(()) => { tracing::debug!( target: crate::TRACING_TARGET, action = "initialize", route_count = route_names.len(), route_names = ?route_names, "tracing subscriber initialized" ); std::result::Result::Ok(crate::LoggingGuard { guards, route_names }) }, std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::new( "logging_subscriber_init_failed", format!("cannot initialize tracing subscriber: {error}"), )), }; } fn build_enabled_layers( config: &crate::LoggingConfig, guards: &mut std::vec::Vec, route_names: &mut std::vec::Vec, ) -> kb_core::Result> { let mut layers = std::vec::Vec::::new(); let enabled_routes = enabled_routes(config); for route in enabled_routes { let filter = match build_route_writer_filter(route, config) { std::result::Result::Ok(filter) => filter, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if route.sink == "console" { let (writer, guard) = tracing_appender::non_blocking(std::io::stdout()); let layer_result = build_console_layer(route, filter, writer); let layer = match layer_result { std::result::Result::Ok(layer) => layer, std::result::Result::Err(error) => return std::result::Result::Err(error), }; guards.push(guard); route_names.push(route.name.clone()); layers.push(layer); continue; } if route.sink == "file" { let writer_result = build_file_writer(route); let (writer, guard) = match writer_result { std::result::Result::Ok(parts) => parts, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let layer_result = build_file_layer(route, filter, writer); let layer = match layer_result { std::result::Result::Ok(layer) => layer, std::result::Result::Err(error) => return std::result::Result::Err(error), }; guards.push(guard); route_names.push(route.name.clone()); layers.push(layer); continue; } return std::result::Result::Err(kb_core::Error::new( "logging_unknown_sink", format!("unknown logging sink '{}' for route '{}'", route.sink, route.name), )); } return std::result::Result::Ok(layers); } fn enabled_routes(config: &crate::LoggingConfig) -> std::vec::Vec<&crate::LogTargetConfig> { let mut routes = std::vec::Vec::<&crate::LogTargetConfig>::new(); for route in &config.targets { if route.enabled { routes.push(route); } } return routes; } fn build_enabled_route_writer_filters( config: &crate::LoggingConfig, ) -> kb_core::Result> { let mut filters = std::vec::Vec::new(); for route in enabled_routes(config) { let result = build_route_writer_filter(route, config); match result { std::result::Result::Ok(value) => filters.push(value), std::result::Result::Err(error) => return std::result::Result::Err(error), } } return std::result::Result::Ok(filters); } fn build_console_layer( route: &crate::LogTargetConfig, filter: RouteWriterFilter, writer: tracing_appender::non_blocking::NonBlocking, ) -> kb_core::Result { let route_writer = RouteMakeWriter::new(writer, filter); return match route.format.as_str() { "compact" => std::result::Result::Ok( tracing_subscriber::fmt::layer() .compact() .with_timer(timer()) .with_ansi(route.ansi) .with_target(true) .with_thread_ids(false) .with_thread_names(false) .with_writer(route_writer) .boxed(), ), "pretty" => std::result::Result::Ok( tracing_subscriber::fmt::layer() .pretty() .with_timer(timer()) .with_ansi(route.ansi) .with_target(true) .with_thread_ids(false) .with_thread_names(false) .with_writer(route_writer) .boxed(), ), "json" => std::result::Result::Ok( tracing_subscriber::fmt::layer() .json() .with_timer(timer()) .with_ansi(false) .with_target(true) .with_thread_ids(false) .with_thread_names(false) .with_writer(route_writer) .boxed(), ), "human" => std::result::Result::Ok( tracing_subscriber::fmt::layer() .with_timer(timer()) .with_ansi(route.ansi) .with_target(true) .with_thread_ids(false) .with_thread_names(false) .with_writer(route_writer) .boxed(), ), _ => std::result::Result::Err(kb_core::Error::new( "logging_unknown_format", format!("unknown logging format '{}' for route '{}'", route.format, route.name), )), }; } fn build_file_layer( route: &crate::LogTargetConfig, filter: RouteWriterFilter, writer: StripAnsiMakeWriter, ) -> kb_core::Result { let route_writer = RouteMakeWriter::new(writer, filter); return match route.format.as_str() { "compact" => std::result::Result::Ok( tracing_subscriber::fmt::layer() .compact() .with_timer(timer()) .with_ansi(false) .with_target(true) .with_thread_ids(false) .with_thread_names(false) .with_writer(route_writer) .boxed(), ), "pretty" => std::result::Result::Ok( tracing_subscriber::fmt::layer() .pretty() .with_timer(timer()) .with_ansi(false) .with_target(true) .with_thread_ids(false) .with_thread_names(false) .with_writer(route_writer) .boxed(), ), "json" => std::result::Result::Ok( tracing_subscriber::fmt::layer() .json() .with_timer(timer()) .with_ansi(false) .with_target(true) .with_thread_ids(false) .with_thread_names(false) .with_writer(route_writer) .boxed(), ), "human" => std::result::Result::Ok( tracing_subscriber::fmt::layer() .with_timer(timer()) .with_ansi(false) .with_target(true) .with_thread_ids(false) .with_thread_names(false) .with_writer(route_writer) .boxed(), ), _ => std::result::Result::Err(kb_core::Error::new( "logging_unknown_format", format!("unknown logging format '{}' for route '{}'", route.format, route.name), )), }; } fn timer() -> tracing_subscriber::fmt::time::ChronoLocal { return tracing_subscriber::fmt::time::ChronoLocal::new("%Y-%m-%dT%H:%M:%S%.3f%:z".to_string()); } fn is_error_route(route: &crate::LogTargetConfig) -> bool { let name = route.name.to_ascii_lowercase(); return route.level == "error" || name.contains("error"); } #[derive(Clone, Debug)] struct RouteWriterFilter { default_level: std::option::Option, target_levels: std::vec::Vec<(std::string::String, u8)>, } impl RouteWriterFilter { fn allows(&self, metadata: &tracing::Metadata<'_>) -> bool { return self.allows_target_level(metadata.target(), metadata.level()); } fn allows_target_level(&self, target: &str, level: &tracing::Level) -> bool { let mut selected_level = self.default_level; let mut selected_target_length = 0_usize; for (configured_target, configured_level) in &self.target_levels { if target_matches(configured_target, target) && configured_target.len() >= selected_target_length { selected_level = std::option::Option::Some(*configured_level); selected_target_length = configured_target.len(); } } return match selected_level { std::option::Option::Some(maximum) => tracing_level_rank(level) <= maximum, std::option::Option::None => false, }; } } fn build_route_writer_filter( route: &crate::LogTargetConfig, config: &crate::LoggingConfig, ) -> kb_core::Result { let route_level_text = if is_error_route(route) { "error" } else { route.level.as_str() }; let route_level_result = parse_level_rank(route_level_text); let route_level = match route_level_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let mut default_level = std::option::Option::None; let mut target_levels = std::vec::Vec::new(); for target in &route.targets { match normalize_target_filter(target) { std::option::Option::Some(normalized) => { target_levels.push((normalized, route_level)); }, std::option::Option::None => default_level = std::option::Option::Some(route_level), } } if route.targets.is_empty() { default_level = std::option::Option::Some(route_level); } if default_level.is_some() && !is_error_route(route) { for filter in &config.target_filters { let normalized = match normalize_target_filter(&filter.target) { std::option::Option::Some(value) => value, std::option::Option::None => continue, }; let level_result = parse_level_rank(&filter.level); let level = match level_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; target_levels.push((normalized, level)); } } return std::result::Result::Ok(RouteWriterFilter { default_level, target_levels }); } fn parse_level_rank(level: &str) -> kb_core::Result { return match level.trim().to_ascii_lowercase().as_str() { "error" => std::result::Result::Ok(1), "warn" => std::result::Result::Ok(2), "info" => std::result::Result::Ok(3), "debug" => std::result::Result::Ok(4), "trace" => std::result::Result::Ok(5), _ => std::result::Result::Err(kb_core::Error::new( "logging_filter_build_failed", format!("unsupported tracing level '{level}'"), )), }; } fn tracing_level_rank(level: &tracing::Level) -> u8 { if *level == tracing::Level::ERROR { return 1; } if *level == tracing::Level::WARN { return 2; } if *level == tracing::Level::INFO { return 3; } if *level == tracing::Level::DEBUG { return 4; } return 5; } fn target_matches(configured: &str, observed: &str) -> bool { return observed == configured || observed .strip_prefix(configured) .is_some_and(|suffix| return suffix.starts_with("::")); } #[cfg(test)] fn build_route_filter_expression( route: &crate::LogTargetConfig, config: &crate::LoggingConfig, ) -> std::string::String { let mut directives = std::vec::Vec::::new(); let mut has_wildcard = false; let route_level = if is_error_route(route) { "error" } else { route.level.as_str() }; for target in &route.targets { let normalized_option = normalize_target_filter(target); match normalized_option { std::option::Option::Some(normalized) => { directives.push(format!("{}={}", normalized, route_level)) }, std::option::Option::None => has_wildcard = true, } } if route.targets.is_empty() || has_wildcard { directives.push(route_level.to_string()); } if (route.targets.is_empty() || has_wildcard) && !is_error_route(route) { for filter in &config.target_filters { let normalized_option = normalize_target_filter(&filter.target); if let std::option::Option::Some(normalized) = normalized_option { directives.push(format!("{}={}", normalized, filter.level)); } } } if directives.is_empty() { directives.push(config.default_level.clone()); } return directives.join(","); } fn normalize_target_filter(target: &str) -> std::option::Option { let trimmed = target.trim(); if trimmed.is_empty() || trimmed == "*" { return std::option::Option::None; } if trimmed.ends_with(".*") { let shortened = trimmed.trim_end_matches(".*"); if shortened.is_empty() { return std::option::Option::None; } return std::option::Option::Some(shortened.to_string()); } if trimmed.ends_with("::*") { let shortened = trimmed.trim_end_matches("::*"); if shortened.is_empty() { return std::option::Option::None; } return std::option::Option::Some(shortened.to_string()); } return std::option::Option::Some(trimmed.to_string()); } fn build_file_writer( route: &crate::LogTargetConfig, ) -> kb_core::Result<( StripAnsiMakeWriter, tracing_appender::non_blocking::WorkerGuard, )> { let path = resolve_workspace_relative_path(&route.path); let parent = match path.parent() { std::option::Option::Some(parent) => parent.to_path_buf(), std::option::Option::None => workspace_root_dir(), }; let create_result = std::fs::create_dir_all(&parent); if let std::result::Result::Err(error) = create_result { return std::result::Result::Err(kb_core::Error::new( "logging_directory_create_failed", format!("cannot create logging directory '{}': {error}", parent.display()), )); } let rotation = if route.rotation == "hourly" { tracing_appender::rolling::Rotation::HOURLY } else if route.rotation == "none" || route.rotation == "never" { tracing_appender::rolling::Rotation::NEVER } else { tracing_appender::rolling::Rotation::DAILY }; let file_name = match path.file_name() { std::option::Option::Some(value) => match value.to_str() { std::option::Option::Some(value) => value.to_string(), std::option::Option::None => route.name.clone(), }, std::option::Option::None => route.name.clone(), }; let (prefix, suffix) = split_file_name(&file_name); let appender_result = tracing_appender::rolling::Builder::default() .rotation(rotation) .filename_prefix(prefix) .filename_suffix(suffix) .build(&parent); let appender = match appender_result { std::result::Result::Ok(appender) => appender, std::result::Result::Err(error) => { return std::result::Result::Err(kb_core::Error::new( "logging_appender_build_failed", format!("cannot build rolling logging appender in '{}': {error}", parent.display()), )); }, }; let (writer, guard) = tracing_appender::non_blocking(appender); return std::result::Result::Ok((StripAnsiMakeWriter::new(writer), guard)); } fn split_file_name(file_name: &str) -> (std::string::String, std::string::String) { let path = std::path::Path::new(file_name); let stem = match path.file_stem() { std::option::Option::Some(value) => match value.to_str() { std::option::Option::Some(value) => value.to_string(), std::option::Option::None => file_name.to_string(), }, std::option::Option::None => file_name.to_string(), }; let suffix = match path.extension() { std::option::Option::Some(value) => match value.to_str() { std::option::Option::Some(value) => value.to_string(), std::option::Option::None => "log".to_string(), }, std::option::Option::None => "log".to_string(), }; return (stem, suffix); } fn workspace_root_dir() -> std::path::PathBuf { let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); return match manifest_dir.parent() { std::option::Option::Some(parent) => parent.to_path_buf(), std::option::Option::None => manifest_dir, }; } fn resolve_workspace_relative_path(path: &str) -> std::path::PathBuf { let input_path = std::path::PathBuf::from(path); if input_path.is_absolute() { return input_path; } return workspace_root_dir().join(input_path); } struct RouteMakeWriter { inner: W, filter: RouteWriterFilter, } impl RouteMakeWriter { fn new(inner: W, filter: RouteWriterFilter) -> Self { return Self { inner, filter }; } } enum RouteWriter { Enabled(W), Disabled(std::io::Sink), } impl<'a, W> tracing_subscriber::fmt::MakeWriter<'a> for RouteMakeWriter where W: tracing_subscriber::fmt::MakeWriter<'a>, { type Writer = RouteWriter; fn make_writer(&'a self) -> Self::Writer { return RouteWriter::Disabled(std::io::sink()); } fn make_writer_for(&'a self, metadata: &tracing::Metadata<'_>) -> Self::Writer { if self.filter.allows(metadata) { return RouteWriter::Enabled(self.inner.make_writer_for(metadata)); } return RouteWriter::Disabled(std::io::sink()); } } impl std::io::Write for RouteWriter where W: std::io::Write, { fn write(&mut self, buf: &[u8]) -> std::io::Result { return match self { Self::Enabled(writer) => writer.write(buf), Self::Disabled(writer) => writer.write(buf), }; } fn flush(&mut self) -> std::io::Result<()> { return match self { Self::Enabled(writer) => writer.flush(), Self::Disabled(writer) => writer.flush(), }; } } struct StripAnsiMakeWriter { inner: W, } impl StripAnsiMakeWriter { fn new(inner: W) -> Self { return Self { inner }; } } struct StripAnsiWriter { inner: W, } impl<'a, W> tracing_subscriber::fmt::MakeWriter<'a> for StripAnsiMakeWriter where W: tracing_subscriber::fmt::MakeWriter<'a>, { type Writer = StripAnsiWriter; fn make_writer(&'a self) -> Self::Writer { return StripAnsiWriter { inner: self.inner.make_writer() }; } } impl std::io::Write for StripAnsiWriter where W: std::io::Write, { fn write(&mut self, buf: &[u8]) -> std::io::Result { let stripped = strip_ansi_bytes(buf); let write_result = self.inner.write_all(&stripped); return match write_result { std::result::Result::Ok(()) => std::result::Result::Ok(buf.len()), std::result::Result::Err(error) => std::result::Result::Err(error), }; } fn flush(&mut self) -> std::io::Result<()> { return self.inner.flush(); } } fn strip_ansi_bytes(input: &[u8]) -> std::vec::Vec { let mut output = std::vec::Vec::with_capacity(input.len()); let mut index = 0usize; while index < input.len() { let byte = input[index]; if byte == 0x1B && index + 1 < input.len() && input[index + 1] == b'[' { index += 2; while index < input.len() { let current = input[index]; if (0x40..=0x7E).contains(¤t) { index += 1; break; } index += 1; } continue; } output.push(byte); index += 1; } return output; } #[cfg(test)] mod tests { use tracing_subscriber::Layer; // rust-rules: trait-import use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import #[derive(Clone, Debug)] struct TestMakeWriter { output: std::sync::Arc>>, } #[derive(Debug)] struct TestWriter { output: std::sync::Arc>>, } impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for TestMakeWriter { type Writer = TestWriter; fn make_writer(&'a self) -> Self::Writer { return TestWriter { output: self.output.clone() }; } } impl std::io::Write for TestWriter { fn write(&mut self, buf: &[u8]) -> std::io::Result { let lock_result = self.output.lock(); let mut output = match lock_result { std::result::Result::Ok(output) => output, std::result::Result::Err(_) => { return std::result::Result::Err(std::io::Error::other("poisoned test writer")); }, }; output.extend_from_slice(buf); return std::result::Result::Ok(buf.len()); } fn flush(&mut self) -> std::io::Result<()> { return std::result::Result::Ok(()); } } fn test_route( name: &str, sink: &str, level: &str, format: &str, enabled: bool, targets: std::vec::Vec, ) -> crate::LogTargetConfig { return crate::LogTargetConfig { name: name.to_string(), enabled, sink: sink.to_string(), level: level.to_string(), path: format!("logs/{name}.log"), rotation: "daily".to_string(), format: format.to_string(), ansi: false, targets, }; } fn sample_logging_config() -> crate::LoggingConfig { return crate::LoggingConfig { default_level: "info".to_string(), targets: std::vec![test_route( "console_main", "console", "debug", "human", true, std::vec!["kb-onchain-transport.*".to_string()] )], target_filters: std::vec![crate::LogTargetFilterConfig { target: "hyper".to_string(), level: "warn".to_string() }], }; } fn package_name(cargo_toml: &str) -> std::option::Option { let mut in_package = false; for line in cargo_toml.lines() { let trimmed = line.trim(); if trimmed == "[package]" { in_package = true; continue; } if in_package && trimmed.starts_with('[') { return std::option::Option::None; } if in_package && trimmed.starts_with("name = ") { let value = trimmed.trim_start_matches("name = ").trim_matches('"'); return std::option::Option::Some(value.to_string()); } } return std::option::Option::None; } fn count_tracing_target_constants(path: &std::path::Path) -> usize { let read_result = std::fs::read_dir(path); let entries = match read_result { std::result::Result::Ok(entries) => entries, std::result::Result::Err(_) => return 0, }; let mut count = 0_usize; for entry_result in entries { let entry = match entry_result { std::result::Result::Ok(entry) => entry, std::result::Result::Err(_) => continue, }; let entry_path = entry.path(); if entry_path.is_dir() { count += count_tracing_target_constants(&entry_path); continue; } if entry_path.extension().and_then(std::ffi::OsStr::to_str) != std::option::Option::Some("rs") { continue; } let content_result = std::fs::read_to_string(&entry_path); let content = match content_result { std::result::Result::Ok(content) => content, std::result::Result::Err(_) => continue, }; count += content .lines() .filter(|line| { let trimmed = line.trim(); return trimmed.starts_with("pub(crate) const ") && trimmed.contains("TRACING_") && trimmed.contains("TARGET") && trimmed.contains(": &str"); }) .count(); } return count; } #[test] fn every_tracing_crate_exposes_canonical_targets() { let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); let workspace_root = match manifest_dir.parent() { std::option::Option::Some(path) => path, std::option::Option::None => panic!("workspace root must exist"), }; let read_result = std::fs::read_dir(workspace_root); let entries = match read_result { std::result::Result::Ok(entries) => entries, std::result::Result::Err(error) => panic!("workspace must be readable: {error}"), }; let mut tracing_crate_count = 0_usize; for entry_result in entries { let entry = match entry_result { std::result::Result::Ok(entry) => entry, std::result::Result::Err(error) => { panic!("workspace entry must be readable: {error}") }, }; let crate_path = entry.path(); if !crate_path.is_dir() { continue; } let cargo_path = crate_path.join("Cargo.toml"); if !cargo_path.is_file() { continue; } let cargo_result = std::fs::read_to_string(&cargo_path); let cargo_toml = match cargo_result { std::result::Result::Ok(content) => content, std::result::Result::Err(error) => panic!("Cargo.toml must be readable: {error}"), }; if !cargo_toml.lines().any(|line| return line.trim() == "tracing.workspace = true") { continue; } tracing_crate_count += 1; let package_name = match package_name(&cargo_toml) { std::option::Option::Some(name) => name, std::option::Option::None => panic!("tracing crate must declare a package name"), }; let target_count = count_tracing_target_constants(&crate_path.join("src")); assert!( target_count > 0, "tracing crate {package_name} must declare at least one canonical target constant" ); let constants_path = crate_path.join("src/constants.rs"); if constants_path.is_file() { let constants_result = std::fs::read_to_string(&constants_path); let constants = match constants_result { std::result::Result::Ok(content) => content, std::result::Result::Err(error) => { panic!("tracing crate {package_name} constants must be readable: {error}") }, }; let expected = format!("pub(crate) const TRACING_TARGET: &str = \"{package_name}\";"); assert!(constants.contains(&expected), "invalid root target for {package_name}"); assert_eq!( target_count, 1, "tracing crate {package_name} with a root target must declare exactly one target constant" ); } } assert!(tracing_crate_count > 0); } #[test] fn route_filter_converts_wildcard_prefix() { let config = sample_logging_config(); let expression = super::build_route_filter_expression(&config.targets[0], &config); assert_eq!(expression, "kb-onchain-transport=debug"); } #[test] fn route_filter_converts_rust_wildcard_prefix() { let mut config = sample_logging_config(); config.targets[0].targets = std::vec!["kb-onchain-transport::*".to_string()]; let expression = super::build_route_filter_expression(&config.targets[0], &config); assert_eq!(expression, "kb-onchain-transport=debug"); } #[test] fn route_filter_keeps_exact_target() { let mut config = sample_logging_config(); config.targets[0].targets = std::vec!["kb-onchain-transport::client".to_string()]; let expression = super::build_route_filter_expression(&config.targets[0], &config); assert_eq!(expression, "kb-onchain-transport::client=debug"); } #[test] fn route_filter_adds_target_filters_for_wildcard_routes() { let mut config = sample_logging_config(); config.targets[0].targets = std::vec!["*".to_string()]; let expression = super::build_route_filter_expression(&config.targets[0], &config); assert_eq!(expression, "debug,hyper=warn"); } #[test] fn route_filter_uses_route_level_for_empty_targets() { let mut config = sample_logging_config(); config.targets[0].targets = std::vec![]; let expression = super::build_route_filter_expression(&config.targets[0], &config); assert_eq!(expression, "debug,hyper=warn"); } #[test] fn target_filters_accept_rust_wildcard_prefix() { let mut config = sample_logging_config(); config.targets[0].targets = std::vec!["*".to_string()]; config.target_filters = std::vec![crate::LogTargetFilterConfig { target: "kb_lib::executor::spl::token_2022::*".to_string(), level: "trace".to_string(), }]; let expression = super::build_route_filter_expression(&config.targets[0], &config); assert_eq!(expression, "debug,kb_lib::executor::spl::token_2022=trace"); } #[test] fn route_writer_filter_preserves_target_and_level_semantics() { let mut config = sample_logging_config(); config.targets[0].targets = std::vec!["*".to_string()]; let result = super::build_route_writer_filter(&config.targets[0], &config); let filter = match result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("route writer filter failed: {error}"), }; assert!(filter.allows_target_level("kb-onchain-transport", &tracing::Level::DEBUG)); assert!(!filter.allows_target_level("kb-onchain-transport", &tracing::Level::TRACE)); assert!(filter.allows_target_level("hyper", &tracing::Level::WARN)); assert!(!filter.allows_target_level("hyper", &tracing::Level::INFO)); } #[test] fn more_than_64_routes_compose_without_filtered_layer_ids() { let mut routes = std::vec::Vec::new(); for index in 0..65_u32 { let route_name = format!("console_{index}"); routes.push(test_route( route_name.as_str(), "console", "info", "human", true, std::vec![format!("target_{index}")], )); } let config = crate::LoggingConfig { default_level: "info".to_string(), targets: routes, target_filters: std::vec::Vec::new(), }; let mut guards = std::vec::Vec::new(); let mut route_names = std::vec::Vec::new(); let result = super::build_enabled_layers(&config, &mut guards, &mut route_names); let layers = match result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("layer construction failed: {error}"), }; assert_eq!(layers.len(), 65); assert_eq!(route_names.len(), 65); let filter_result = super::build_enabled_route_writer_filters(&config); let filters = match filter_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("admission filters failed: {error}"), }; let admission_filter = tracing_subscriber::filter::filter_fn(move |metadata| { return filters.iter().any(|filter| return filter.allows(metadata)); }); let _subscriber = tracing_subscriber::registry().with(layers.with_filter(admission_filter)); } #[test] fn error_route_forces_error_level() { let config = crate::LoggingConfig { default_level: "info".to_string(), targets: std::vec![test_route( "file_errors", "file", "debug", "human", true, std::vec!["*".to_string()] )], target_filters: std::vec![], }; let expression = super::build_route_filter_expression(&config.targets[0], &config); assert_eq!(expression, "error"); } #[test] fn error_route_does_not_append_target_filters() { let config = crate::LoggingConfig { default_level: "info".to_string(), targets: std::vec![test_route( "file_errors", "file", "debug", "human", true, std::vec!["*".to_string()] )], target_filters: std::vec![crate::LogTargetFilterConfig { target: "hyper".to_string(), level: "warn".to_string() }], }; let expression = super::build_route_filter_expression(&config.targets[0], &config); assert_eq!(expression, "error"); } #[test] fn disabled_routes_are_not_selected() { let config = crate::LoggingConfig { default_level: "info".to_string(), targets: std::vec![ test_route( "console_main", "console", "info", "human", false, std::vec!["*".to_string()] ), test_route( "file_human", "file", "info", "human", false, std::vec!["*".to_string()] ) ], target_filters: std::vec![], }; let routes = super::enabled_routes(&config); assert!(routes.is_empty()); } #[test] fn enabled_routes_keep_all_configured_outputs() { let config = crate::LoggingConfig { default_level: "info".to_string(), targets: std::vec![ test_route( "console_main", "console", "debug", "compact", true, std::vec!["*".to_string()] ), test_route( "file_kb_core", "file", "debug", "human", true, std::vec!["kb_core::*".to_string()] ), test_route( "file_kb_config", "file", "debug", "human", true, std::vec!["kb_config::*".to_string()] ), test_route( "file_errors", "file", "debug", "json", true, std::vec!["*".to_string()] ) ], target_filters: std::vec![], }; let routes = super::enabled_routes(&config); let route_names = routes .iter() .map(|route| return route.name.as_str()) .collect::>(); assert_eq!( route_names, std::vec!["console_main", "file_kb_core", "file_kb_config", "file_errors"] ); } #[test] fn init_logging_rejects_configuration_without_enabled_routes() { let config = crate::LoggingConfig { default_level: "info".to_string(), targets: std::vec![test_route( "console_main", "console", "info", "human", false, std::vec!["*".to_string()] )], target_filters: std::vec![], }; let result = super::init_logging(&config); assert!(result.is_err()); } #[test] fn split_file_name_keeps_stem_and_suffix() { let parts = super::split_file_name("khadhroony.bot3.log"); assert_eq!(parts, ("khadhroony.bot3".to_string(), "log".to_string())); } #[test] fn ansi_stripper_removes_escape_sequences() { let stripped = super::strip_ansi_bytes(b"a\x1b[31mb"); assert_eq!(stripped, b"ab"); } #[test] fn ansi_stripping_writer_removes_escape_sequences_before_inner_writer() { let output = std::sync::Arc::new(std::sync::Mutex::new(std::vec::Vec::::new())); let make_writer = super::StripAnsiMakeWriter::new(TestMakeWriter { output: output.clone() }); let mut writer = tracing_subscriber::fmt::MakeWriter::make_writer(&make_writer); let write_result = std::io::Write::write_all(&mut writer, b"a\x1b[32mb"); assert!(write_result.is_ok()); drop(writer); let actual = match output.lock() { std::result::Result::Ok(output) => output.clone(), std::result::Result::Err(_) => std::vec::Vec::::new(), }; assert_eq!(actual, b"ab"); } }