1122 lines
40 KiB
Rust
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(¤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<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");
|
|
}
|
|
}
|