Files
khadhroony-bot3/kb-logging/src/tracing_runtime.rs
2026-07-28 18:41:30 +02:00

1122 lines
40 KiB
Rust

// 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<tracing_appender::non_blocking::WorkerGuard>,
route_names: std::vec::Vec<std::string::String>,
}
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<tracing_subscriber::Registry>
+ std::marker::Send
+ std::marker::Sync,
>;
/// Initializes global tracing from a logging configuration.
pub fn init_logging(config: &crate::LoggingConfig) -> kb_core::Result<crate::LoggingGuard> {
let mut guards = std::vec::Vec::<tracing_appender::non_blocking::WorkerGuard>::new();
let mut route_names = std::vec::Vec::<std::string::String>::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<tracing_appender::non_blocking::WorkerGuard>,
route_names: &mut std::vec::Vec<std::string::String>,
) -> kb_core::Result<std::vec::Vec<BoxedLayer>> {
let mut layers = std::vec::Vec::<BoxedLayer>::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<std::vec::Vec<RouteWriterFilter>> {
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<BoxedLayer> {
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<tracing_appender::non_blocking::NonBlocking>,
) -> kb_core::Result<BoxedLayer> {
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<u8>,
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<RouteWriterFilter> {
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<u8> {
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::<std::string::String>::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<std::string::String> {
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::NonBlocking>,
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<W> {
inner: W,
filter: RouteWriterFilter,
}
impl<W> RouteMakeWriter<W> {
fn new(inner: W, filter: RouteWriterFilter) -> Self {
return Self { inner, filter };
}
}
enum RouteWriter<W> {
Enabled(W),
Disabled(std::io::Sink),
}
impl<'a, W> tracing_subscriber::fmt::MakeWriter<'a> for RouteMakeWriter<W>
where
W: tracing_subscriber::fmt::MakeWriter<'a>,
{
type Writer = RouteWriter<W::Writer>;
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<W> std::io::Write for RouteWriter<W>
where
W: std::io::Write,
{
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
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<W> {
inner: W,
}
impl<W> StripAnsiMakeWriter<W> {
fn new(inner: W) -> Self {
return Self { inner };
}
}
struct StripAnsiWriter<W> {
inner: W,
}
impl<'a, W> tracing_subscriber::fmt::MakeWriter<'a> for StripAnsiMakeWriter<W>
where
W: tracing_subscriber::fmt::MakeWriter<'a>,
{
type Writer = StripAnsiWriter<W::Writer>;
fn make_writer(&'a self) -> Self::Writer {
return StripAnsiWriter { inner: self.inner.make_writer() };
}
}
impl<W> std::io::Write for StripAnsiWriter<W>
where
W: std::io::Write,
{
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
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<u8> {
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(&current) {
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<std::sync::Mutex<std::vec::Vec<u8>>>,
}
#[derive(Debug)]
struct TestWriter {
output: std::sync::Arc<std::sync::Mutex<std::vec::Vec<u8>>>,
}
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<usize> {
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<std::string::String>,
) -> 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<std::string::String> {
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::<std::vec::Vec<&str>>();
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::<u8>::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::<u8>::new(),
};
assert_eq!(actual, b"ab");
}
}