v0.1.2-pre.003

This commit is contained in:
2026-08-14 18:24:28 +02:00
parent 697527675a
commit 6e06802e38
10 changed files with 631 additions and 26 deletions

View File

@@ -1,5 +1,9 @@
// file: crates/ksp-logging-lib/src/error.rs
// version: 1
// version: 2
/// Error code used when runtime logging settings are invalid.
pub const ERROR_CODE_INVALID_SETTINGS: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "invalid_settings");
/// Error code used when a global logging subscriber is already installed.
pub const ERROR_CODE_ALREADY_INITIALIZED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "already_initialized");
/// Error code used when a hot reload cannot replace the active runtime layers.
pub const ERROR_CODE_RELOAD_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "reload_failed");

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-logging-lib/src/lib.rs
// version: 1
// version: 2
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
@@ -7,15 +7,27 @@
//! KSP-owned logging and tracing facade.
//!
//! This crate owns the KSP runtime logging contract. Behavioral KSP crates emit events and spans through this facade rather than depending directly on the
//! `tracing` stack. Runtime subscriber initialization, filtering, outputs and hot reload are added by the following `0.1.2` prereleases.
//! `tracing` stack. `0.1.2-pre.003` installs the single global subscriber, applies KSP takeover filtering, provides the initial console layer and establishes
//! hot reload. Non-blocking writers, file output, ANSI stripping and writer guards are completed by the following prerelease.
mod error;
mod macros;
mod runtime;
mod settings;
mod span;
/// Error code used when a global logging subscriber is already installed.
pub use self::error::ERROR_CODE_ALREADY_INITIALIZED;
/// Error code used when runtime logging settings are invalid.
pub use self::error::ERROR_CODE_INVALID_SETTINGS;
/// Error code used when a hot reload cannot replace the active runtime layers.
pub use self::error::ERROR_CODE_RELOAD_FAILED;
/// Guard owning the mutable runtime state of the installed KSP logging subscriber.
pub use self::runtime::LoggingGuard;
/// Installs the global KSP tracing subscriber.
pub use self::runtime::initialize;
/// Replaces the active KSP logging settings without reinstalling the global subscriber.
pub use self::runtime::reinitialize;
/// Console stream selected for human-readable logs.
pub use self::settings::ConsoleOutput;
/// Runtime settings for the optional console output.

View File

@@ -0,0 +1,126 @@
// file: crates/ksp-logging-lib/src/runtime.rs
// version: 1
use tracing_subscriber::Layer; // rust-rules: trait-import
use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import
/// Guard owning the mutable runtime state of the installed KSP logging subscriber.
pub struct LoggingGuard {
reload_handle: RuntimeReloadHandle,
settings: crate::LoggingSettings,
}
impl LoggingGuard {
/// Returns the settings currently active in the KSP logging runtime.
#[must_use]
pub fn settings(&self) -> &crate::LoggingSettings {
return &self.settings;
}
}
type BoxedRuntimeLayer = std::boxed::Box<dyn tracing_subscriber::Layer<tracing_subscriber::Registry> + std::marker::Send + std::marker::Sync + 'static>;
type RuntimeLayers = std::vec::Vec<BoxedRuntimeLayer>;
type RuntimeReloadHandle = tracing_subscriber::reload::Handle<RuntimeLayers, tracing_subscriber::Registry>;
/// Installs the global KSP tracing subscriber.
///
/// This function may succeed only once for the lifetime of the process. The returned guard is then used by [`crate::reinitialize`] to replace the active
/// KSP logging configuration without installing a second global subscriber.
pub fn initialize(settings: &crate::LoggingSettings) -> ksp_core_lib::Result<crate::LoggingGuard> {
return prepare_runtime_layers(settings).and_then(|runtime_layers| -> ksp_core_lib::Result<crate::LoggingGuard> {
let (reload_layer, reload_handle) = tracing_subscriber::reload::Layer::new(runtime_layers);
let subscriber = tracing_subscriber::registry().with(reload_layer);
let install_result = tracing::subscriber::set_global_default(subscriber);
return match install_result {
std::result::Result::Ok(()) => std::result::Result::Ok(crate::LoggingGuard { reload_handle, settings: settings.clone() }),
std::result::Result::Err(error) => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_ALREADY_INITIALIZED, "the global KSP tracing subscriber is already installed").with_source(error),
),
};
});
}
/// Replaces the active KSP logging settings without reinstalling the global subscriber.
///
/// New runtime layers are fully prepared before the reload is attempted. If validation or preparation fails, the currently active configuration is left
/// unchanged.
pub fn reinitialize(guard: &mut crate::LoggingGuard, settings: &crate::LoggingSettings) -> ksp_core_lib::Result<()> {
return prepare_runtime_layers(settings).and_then(|runtime_layers| -> ksp_core_lib::Result<()> {
let reload_result = guard.reload_handle.reload(runtime_layers);
return match reload_result {
std::result::Result::Ok(()) => {
guard.settings = settings.clone();
std::result::Result::Ok(())
},
std::result::Result::Err(error) => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_RELOAD_FAILED, "unable to reload the KSP logging runtime").with_source(error),
),
};
});
}
fn prepare_runtime_layers(settings: &crate::LoggingSettings) -> ksp_core_lib::Result<RuntimeLayers> {
let validation_error = settings.validate().err();
if let std::option::Option::Some(error) = validation_error {
return std::result::Result::Err(error);
}
if settings.file().is_some() {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "file output is not available until the file backend is introduced")
.with_context("field", "file"),
);
}
let mut layers = RuntimeLayers::new();
if let std::option::Option::Some(console) = settings.console() {
layers.push(build_console_layer(console, settings));
}
return std::result::Result::Ok(layers);
}
fn build_console_layer(console: &crate::ConsoleSettings, settings: &crate::LoggingSettings) -> BoxedRuntimeLayer {
let writer = match console.output() {
crate::ConsoleOutput::Stdout => tracing_subscriber::fmt::writer::BoxMakeWriter::new(std::io::stdout),
crate::ConsoleOutput::Stderr => tracing_subscriber::fmt::writer::BoxMakeWriter::new(std::io::stderr),
};
let layer = tracing_subscriber::fmt::layer()
.with_writer(writer)
.with_ansi(false)
.with_target(true)
.with_span_events(map_span_events(settings.span_events()))
.with_filter(build_target_filter(settings))
.boxed();
return layer;
}
fn build_target_filter(settings: &crate::LoggingSettings) -> tracing_subscriber::filter::Targets {
let mut filter = tracing_subscriber::filter::Targets::new()
.with_default(tracing_subscriber::filter::LevelFilter::OFF)
.with_target("ksp-", map_filter_level(settings.default_filter()));
for target_filter in settings.target_filters() {
filter = filter.with_target(target_filter.target_prefix(), map_filter_level(target_filter.level()));
}
return filter;
}
const fn map_filter_level(level: crate::LogFilterLevel) -> tracing_subscriber::filter::LevelFilter {
return match level {
crate::LogFilterLevel::Off => tracing_subscriber::filter::LevelFilter::OFF,
crate::LogFilterLevel::Error => tracing_subscriber::filter::LevelFilter::ERROR,
crate::LogFilterLevel::Warn => tracing_subscriber::filter::LevelFilter::WARN,
crate::LogFilterLevel::Info => tracing_subscriber::filter::LevelFilter::INFO,
crate::LogFilterLevel::Debug => tracing_subscriber::filter::LevelFilter::DEBUG,
crate::LogFilterLevel::Trace => tracing_subscriber::filter::LevelFilter::TRACE,
};
}
fn map_span_events(span_events: crate::SpanEvents) -> tracing_subscriber::fmt::format::FmtSpan {
return match span_events {
crate::SpanEvents::Off => tracing_subscriber::fmt::format::FmtSpan::NONE,
crate::SpanEvents::NewAndClose => tracing_subscriber::fmt::format::FmtSpan::NEW | tracing_subscriber::fmt::format::FmtSpan::CLOSE,
crate::SpanEvents::Full => tracing_subscriber::fmt::format::FmtSpan::FULL,
};
}
#[cfg(test)]
#[path = "../unit_tests/runtime.rs"]
mod tests;