v0.1.2-pre.004
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# file: crates/ksp-logging-lib/Cargo.toml
|
||||
# version: 2
|
||||
# version: 3
|
||||
|
||||
[package]
|
||||
name = "ksp-logging-lib"
|
||||
@@ -11,6 +11,7 @@ repository.workspace = true
|
||||
ksp-core-lib = { path = "../ksp-core-lib" }
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
tracing-appender.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-logging-lib/src/error.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
/// 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");
|
||||
@@ -7,3 +7,5 @@ pub const ERROR_CODE_INVALID_SETTINGS: ksp_core_lib::ErrorCode = ksp_core_lib::E
|
||||
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");
|
||||
/// Error code used when the rolling file output cannot be initialized.
|
||||
pub const ERROR_CODE_FILE_OUTPUT_INITIALIZATION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "file_output_initialization_failed");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-logging-lib/src/lib.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
@@ -7,22 +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. `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.
|
||||
//! `tracing` stack. `0.1.2-pre.004` owns the single global subscriber, KSP takeover filtering, hot reload, non-blocking console/file outputs, rolling file
|
||||
//! appenders, ANSI stripping, dropped-line counters and the worker guards required to flush active queues.
|
||||
|
||||
mod error;
|
||||
mod macros;
|
||||
mod runtime;
|
||||
mod settings;
|
||||
mod span;
|
||||
mod writer;
|
||||
|
||||
/// Error code used when a global logging subscriber is already installed.
|
||||
pub use self::error::ERROR_CODE_ALREADY_INITIALIZED;
|
||||
/// Error code used when the rolling file output cannot be initialized.
|
||||
pub use self::error::ERROR_CODE_FILE_OUTPUT_INITIALIZATION_FAILED;
|
||||
/// 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.
|
||||
/// Cumulative number of log lines dropped by non-blocking KSP outputs.
|
||||
pub use self::runtime::DroppedLines;
|
||||
/// Guard owning the mutable runtime state and non-blocking writers of the installed KSP logging subscriber.
|
||||
pub use self::runtime::LoggingGuard;
|
||||
/// Installs the global KSP tracing subscriber.
|
||||
pub use self::runtime::initialize;
|
||||
|
||||
@@ -1,13 +1,52 @@
|
||||
// file: crates/ksp-logging-lib/src/runtime.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
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.
|
||||
/// Cumulative number of log lines dropped by non-blocking KSP outputs.
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub struct DroppedLines {
|
||||
console: usize,
|
||||
file: usize,
|
||||
}
|
||||
|
||||
impl DroppedLines {
|
||||
/// Returns an empty dropped-line snapshot.
|
||||
#[must_use]
|
||||
pub const fn zero() -> Self {
|
||||
return Self { console: 0, file: 0 };
|
||||
}
|
||||
|
||||
/// Returns the number of console lines dropped since Logging initialization.
|
||||
#[must_use]
|
||||
pub const fn console(&self) -> usize {
|
||||
return self.console;
|
||||
}
|
||||
|
||||
/// Returns the number of file lines dropped since Logging initialization.
|
||||
#[must_use]
|
||||
pub const fn file(&self) -> usize {
|
||||
return self.file;
|
||||
}
|
||||
|
||||
/// Returns the total number of dropped lines across console and file outputs.
|
||||
#[must_use]
|
||||
pub const fn total(&self) -> usize {
|
||||
return self.console.saturating_add(self.file);
|
||||
}
|
||||
|
||||
const fn saturating_add(self, other: Self) -> Self {
|
||||
return Self { console: self.console.saturating_add(other.console), file: self.file.saturating_add(other.file) };
|
||||
}
|
||||
}
|
||||
|
||||
/// Guard owning the mutable runtime state and non-blocking writers of the installed KSP logging subscriber.
|
||||
pub struct LoggingGuard {
|
||||
reload_handle: RuntimeReloadHandle,
|
||||
settings: crate::LoggingSettings,
|
||||
outputs: RuntimeOutputs,
|
||||
retired_dropped_lines: crate::DroppedLines,
|
||||
}
|
||||
|
||||
impl LoggingGuard {
|
||||
@@ -16,23 +55,76 @@ impl LoggingGuard {
|
||||
pub fn settings(&self) -> &crate::LoggingSettings {
|
||||
return &self.settings;
|
||||
}
|
||||
|
||||
/// Returns cumulative dropped-line counters across active and previously reloaded outputs.
|
||||
#[must_use]
|
||||
pub fn dropped_lines(&self) -> crate::DroppedLines {
|
||||
return self.retired_dropped_lines.saturating_add(self.outputs.dropped_lines());
|
||||
}
|
||||
}
|
||||
|
||||
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>;
|
||||
|
||||
struct PreparedRuntime {
|
||||
layers: RuntimeLayers,
|
||||
outputs: RuntimeOutputs,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct RuntimeOutputs {
|
||||
console: std::option::Option<RuntimeOutput>,
|
||||
file: std::option::Option<RuntimeOutput>,
|
||||
}
|
||||
|
||||
impl RuntimeOutputs {
|
||||
fn dropped_lines(&self) -> crate::DroppedLines {
|
||||
let console = match self.console.as_ref() {
|
||||
std::option::Option::Some(output) => output.dropped_lines(),
|
||||
std::option::Option::None => 0,
|
||||
};
|
||||
let file = match self.file.as_ref() {
|
||||
std::option::Option::Some(output) => output.dropped_lines(),
|
||||
std::option::Option::None => 0,
|
||||
};
|
||||
return crate::DroppedLines { console, file };
|
||||
}
|
||||
}
|
||||
|
||||
struct RuntimeOutput {
|
||||
_worker_guard: tracing_appender::non_blocking::WorkerGuard,
|
||||
error_counter: tracing_appender::non_blocking::ErrorCounter,
|
||||
}
|
||||
|
||||
impl RuntimeOutput {
|
||||
fn dropped_lines(&self) -> usize {
|
||||
return self.error_counter.dropped_lines();
|
||||
}
|
||||
}
|
||||
|
||||
struct PreparedOutput {
|
||||
layer: BoxedRuntimeLayer,
|
||||
output: RuntimeOutput,
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// This function may succeed only once for the lifetime of the process. The returned guard owns all non-blocking writer guards and 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);
|
||||
return prepare_runtime(settings).and_then(|prepared| -> ksp_core_lib::Result<crate::LoggingGuard> {
|
||||
let PreparedRuntime { layers, outputs } = prepared;
|
||||
let (reload_layer, reload_handle) = tracing_subscriber::reload::Layer::new(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::Ok(()) => std::result::Result::Ok(crate::LoggingGuard {
|
||||
reload_handle,
|
||||
settings: settings.clone(),
|
||||
outputs,
|
||||
retired_dropped_lines: crate::DroppedLines::zero(),
|
||||
}),
|
||||
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),
|
||||
),
|
||||
@@ -40,16 +132,21 @@ pub fn initialize(settings: &crate::LoggingSettings) -> ksp_core_lib::Result<cra
|
||||
});
|
||||
}
|
||||
|
||||
/// Replaces the active KSP logging settings without reinstalling the global subscriber.
|
||||
/// Replaces the active KSP logging settings and non-blocking outputs 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.
|
||||
/// New runtime layers, writers and guards are fully prepared before the reload is attempted. If validation or preparation fails, the currently active
|
||||
/// configuration remains unchanged. After a successful layer swap, dropped-line counters from the retired outputs are retained cumulatively and the old
|
||||
/// worker guards are dropped so their queues can be flushed.
|
||||
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 prepare_runtime(settings).and_then(|prepared| -> ksp_core_lib::Result<()> {
|
||||
let PreparedRuntime { layers, outputs } = prepared;
|
||||
let reload_result = guard.reload_handle.reload(layers);
|
||||
return match reload_result {
|
||||
std::result::Result::Ok(()) => {
|
||||
guard.retired_dropped_lines = guard.retired_dropped_lines.saturating_add(guard.outputs.dropped_lines());
|
||||
let retired_outputs = std::mem::replace(&mut guard.outputs, outputs);
|
||||
guard.settings = settings.clone();
|
||||
drop(retired_outputs);
|
||||
std::result::Result::Ok(())
|
||||
},
|
||||
std::result::Result::Err(error) => std::result::Result::Err(
|
||||
@@ -59,38 +156,84 @@ pub fn reinitialize(guard: &mut crate::LoggingGuard, settings: &crate::LoggingSe
|
||||
});
|
||||
}
|
||||
|
||||
fn prepare_runtime_layers(settings: &crate::LoggingSettings) -> ksp_core_lib::Result<RuntimeLayers> {
|
||||
fn prepare_runtime(settings: &crate::LoggingSettings) -> ksp_core_lib::Result<PreparedRuntime> {
|
||||
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"),
|
||||
);
|
||||
if settings.console().is_none() && settings.file().is_none() {
|
||||
return std::result::Result::Ok(PreparedRuntime { layers: RuntimeLayers::new(), outputs: RuntimeOutputs::default() });
|
||||
}
|
||||
let prepared_file = match settings.file() {
|
||||
std::option::Option::Some(file) => match build_file_output(file, settings) {
|
||||
std::result::Result::Ok(output) => std::option::Option::Some(output),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
},
|
||||
std::option::Option::None => std::option::Option::None,
|
||||
};
|
||||
let prepared_console = settings.console().map(|console| -> PreparedOutput {
|
||||
return build_console_output(console, settings);
|
||||
});
|
||||
let mut layers = RuntimeLayers::new();
|
||||
if let std::option::Option::Some(console) = settings.console() {
|
||||
let takeover_filter: BoxedRuntimeLayer = std::boxed::Box::new(build_target_filter(settings));
|
||||
layers.push(takeover_filter);
|
||||
layers.push(build_console_layer(console, settings));
|
||||
layers.push(std::boxed::Box::new(build_target_filter(settings)));
|
||||
let mut outputs = RuntimeOutputs::default();
|
||||
if let std::option::Option::Some(console) = prepared_console {
|
||||
layers.push(console.layer);
|
||||
outputs.console = std::option::Option::Some(console.output);
|
||||
}
|
||||
return std::result::Result::Ok(layers);
|
||||
if let std::option::Option::Some(file) = prepared_file {
|
||||
layers.push(file.layer);
|
||||
outputs.file = std::option::Option::Some(file.output);
|
||||
}
|
||||
return std::result::Result::Ok(PreparedRuntime { layers, outputs });
|
||||
}
|
||||
|
||||
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),
|
||||
fn build_console_output(console: &crate::ConsoleSettings, settings: &crate::LoggingSettings) -> PreparedOutput {
|
||||
return match console.output() {
|
||||
crate::ConsoleOutput::Stdout => build_non_blocking_output(std::io::stdout(), "ksp-logging-console", settings),
|
||||
crate::ConsoleOutput::Stderr => build_non_blocking_output(std::io::stderr(), "ksp-logging-console", settings),
|
||||
};
|
||||
let layer = tracing_subscriber::fmt::layer()
|
||||
}
|
||||
|
||||
fn build_file_output(file: &crate::FileSettings, settings: &crate::LoggingSettings) -> ksp_core_lib::Result<PreparedOutput> {
|
||||
let appender_result = tracing_appender::rolling::RollingFileAppender::builder()
|
||||
.rotation(map_file_rotation(file.rotation()))
|
||||
.filename_prefix(file.file_name_prefix())
|
||||
.build(file.directory());
|
||||
let appender = match appender_result {
|
||||
std::result::Result::Ok(appender) => appender,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_FILE_OUTPUT_INITIALIZATION_FAILED, "unable to initialize the KSP rolling file appender")
|
||||
.with_context("directory", file.directory().display().to_string())
|
||||
.with_context("file_name_prefix", file.file_name_prefix())
|
||||
.with_source(error),
|
||||
);
|
||||
},
|
||||
};
|
||||
let stripped_writer = crate::writer::StripAnsiWriter::new(appender);
|
||||
return std::result::Result::Ok(build_non_blocking_output(stripped_writer, "ksp-logging-file", settings));
|
||||
}
|
||||
|
||||
fn build_non_blocking_output<W>(writer: W, thread_name: &str, settings: &crate::LoggingSettings) -> PreparedOutput
|
||||
where
|
||||
W: std::io::Write + std::marker::Send + 'static,
|
||||
{
|
||||
let (non_blocking, worker_guard) = tracing_appender::non_blocking::NonBlockingBuilder::default().lossy(true).thread_name(thread_name).finish(writer);
|
||||
let error_counter = non_blocking.error_counter();
|
||||
let layer = build_format_layer(non_blocking, settings);
|
||||
return PreparedOutput { layer, output: RuntimeOutput { _worker_guard: worker_guard, error_counter } };
|
||||
}
|
||||
|
||||
fn build_format_layer(writer: tracing_appender::non_blocking::NonBlocking, settings: &crate::LoggingSettings) -> BoxedRuntimeLayer {
|
||||
return tracing_subscriber::fmt::layer()
|
||||
.with_writer(writer)
|
||||
.with_ansi(false)
|
||||
.with_target(true)
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.with_span_events(map_span_events(settings.span_events()))
|
||||
.boxed();
|
||||
return layer;
|
||||
}
|
||||
|
||||
fn build_target_filter(settings: &crate::LoggingSettings) -> tracing_subscriber::filter::Targets {
|
||||
@@ -114,6 +257,14 @@ const fn map_filter_level(level: crate::LogFilterLevel) -> tracing_subscriber::f
|
||||
};
|
||||
}
|
||||
|
||||
const fn map_file_rotation(rotation: crate::FileRotation) -> tracing_appender::rolling::Rotation {
|
||||
return match rotation {
|
||||
crate::FileRotation::Never => tracing_appender::rolling::Rotation::NEVER,
|
||||
crate::FileRotation::Hourly => tracing_appender::rolling::Rotation::HOURLY,
|
||||
crate::FileRotation::Daily => tracing_appender::rolling::Rotation::DAILY,
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
105
crates/ksp-logging-lib/src/writer.rs
Normal file
105
crates/ksp-logging-lib/src/writer.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
// file: crates/ksp-logging-lib/src/writer.rs
|
||||
// version: 1
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum StripAnsiState {
|
||||
Text,
|
||||
Escape,
|
||||
Csi,
|
||||
Osc,
|
||||
OscEscape,
|
||||
String,
|
||||
StringEscape,
|
||||
}
|
||||
|
||||
pub(crate) struct StripAnsiWriter<W> {
|
||||
inner: W,
|
||||
state: StripAnsiState,
|
||||
}
|
||||
|
||||
impl<W> StripAnsiWriter<W> {
|
||||
pub(crate) const fn new(inner: W) -> Self {
|
||||
return Self { inner, state: StripAnsiState::Text };
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn into_inner(self) -> W {
|
||||
return self.inner;
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> std::io::Write for StripAnsiWriter<W>
|
||||
where
|
||||
W: std::io::Write,
|
||||
{
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
let mut stripped = std::vec::Vec::with_capacity(buf.len());
|
||||
for byte in buf {
|
||||
self.consume_byte(*byte, &mut stripped);
|
||||
}
|
||||
let write_result = std::io::Write::write_all(&mut self.inner, stripped.as_slice());
|
||||
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 std::io::Write::flush(&mut self.inner);
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> StripAnsiWriter<W> {
|
||||
fn consume_byte(&mut self, byte: u8, output: &mut std::vec::Vec<u8>) {
|
||||
self.state = match self.state {
|
||||
StripAnsiState::Text => {
|
||||
if byte == 0x1B {
|
||||
StripAnsiState::Escape
|
||||
} else {
|
||||
output.push(byte);
|
||||
StripAnsiState::Text
|
||||
}
|
||||
},
|
||||
StripAnsiState::Escape => match byte {
|
||||
b'[' => StripAnsiState::Csi,
|
||||
b']' => StripAnsiState::Osc,
|
||||
b'P' | b'X' | b'^' | b'_' => StripAnsiState::String,
|
||||
0x1B => StripAnsiState::Escape,
|
||||
_ => StripAnsiState::Text,
|
||||
},
|
||||
StripAnsiState::Csi => {
|
||||
if (0x40..=0x7E).contains(&byte) {
|
||||
StripAnsiState::Text
|
||||
} else {
|
||||
StripAnsiState::Csi
|
||||
}
|
||||
},
|
||||
StripAnsiState::Osc => match byte {
|
||||
0x07 => StripAnsiState::Text,
|
||||
0x1B => StripAnsiState::OscEscape,
|
||||
_ => StripAnsiState::Osc,
|
||||
},
|
||||
StripAnsiState::OscEscape => match byte {
|
||||
b'\\' => StripAnsiState::Text,
|
||||
0x1B => StripAnsiState::OscEscape,
|
||||
_ => StripAnsiState::Osc,
|
||||
},
|
||||
StripAnsiState::String => {
|
||||
if byte == 0x1B {
|
||||
StripAnsiState::StringEscape
|
||||
} else {
|
||||
StripAnsiState::String
|
||||
}
|
||||
},
|
||||
StripAnsiState::StringEscape => match byte {
|
||||
b'\\' => StripAnsiState::Text,
|
||||
0x1B => StripAnsiState::StringEscape,
|
||||
_ => StripAnsiState::String,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/writer.rs"]
|
||||
mod tests;
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-logging-lib/tests/public_api.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
//! Integration tests for the public crate-root surface of `ksp-logging-lib`.
|
||||
|
||||
@@ -59,4 +59,9 @@ fn public_runtime_surface_is_addressable_without_installing_it() {
|
||||
let _reinitialize = ksp_logging_lib::reinitialize;
|
||||
let _already_initialized = ksp_logging_lib::ERROR_CODE_ALREADY_INITIALIZED;
|
||||
let _reload_failed = ksp_logging_lib::ERROR_CODE_RELOAD_FAILED;
|
||||
let _file_initialization_failed = ksp_logging_lib::ERROR_CODE_FILE_OUTPUT_INITIALIZATION_FAILED;
|
||||
let dropped = ksp_logging_lib::DroppedLines::zero();
|
||||
assert_eq!(dropped.console(), 0);
|
||||
assert_eq!(dropped.file(), 0);
|
||||
assert_eq!(dropped.total(), 0);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// file: crates/ksp-logging-lib/tests/runtime.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Integration tests for global initialization, takeover filtering and hot reload.
|
||||
//! Integration tests for global initialization, takeover filtering, non-blocking outputs and hot reload.
|
||||
|
||||
const LOGGING_TARGET: &str = "ksp-logging-lib";
|
||||
const OTHER_KSP_TARGET: &str = "ksp-store-lib";
|
||||
@@ -23,8 +23,49 @@ fn external_error_enabled() -> bool {
|
||||
return tracing::enabled!(target: EXTERNAL_TARGET, tracing::Level::ERROR);
|
||||
}
|
||||
|
||||
fn test_root_directory() -> std::path::PathBuf {
|
||||
return std::env::temp_dir().join(format!("ksp-logging-lib-runtime-{}", std::process::id()));
|
||||
}
|
||||
|
||||
fn reset_directory(path: &std::path::Path) {
|
||||
if path.exists() {
|
||||
let remove_result = std::fs::remove_dir_all(path);
|
||||
assert!(remove_result.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
fn read_directory_text(path: &std::path::Path) -> std::string::String {
|
||||
let read_result = std::fs::read_dir(path);
|
||||
let entries = match read_result {
|
||||
std::result::Result::Ok(entries) => entries,
|
||||
std::result::Result::Err(_) => return std::string::String::new(),
|
||||
};
|
||||
let mut output = std::string::String::new();
|
||||
for entry_result in entries {
|
||||
let entry = match entry_result {
|
||||
std::result::Result::Ok(entry) => entry,
|
||||
std::result::Result::Err(_) => continue,
|
||||
};
|
||||
let file_type = match entry.file_type() {
|
||||
std::result::Result::Ok(file_type) => file_type,
|
||||
std::result::Result::Err(_) => continue,
|
||||
};
|
||||
if !file_type.is_file() {
|
||||
continue;
|
||||
}
|
||||
let content = match std::fs::read_to_string(entry.path()) {
|
||||
std::result::Result::Ok(content) => content,
|
||||
std::result::Result::Err(_) => continue,
|
||||
};
|
||||
output.push_str(content.as_str());
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn global_runtime_supports_takeover_hot_reload_and_single_initialization() {
|
||||
fn global_runtime_supports_takeover_non_blocking_outputs_hot_reload_and_single_initialization() {
|
||||
let root = test_root_directory();
|
||||
reset_directory(root.as_path());
|
||||
let disabled = ksp_logging_lib::LoggingSettings::new(
|
||||
ksp_logging_lib::LogFilterLevel::Info,
|
||||
ksp_logging_lib::SpanEvents::Off,
|
||||
@@ -37,34 +78,66 @@ fn global_runtime_supports_takeover_hot_reload_and_single_initialization() {
|
||||
std::result::Result::Ok(guard) => guard,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(guard.dropped_lines(), ksp_logging_lib::DroppedLines::zero());
|
||||
assert!(!logging_trace_enabled());
|
||||
assert!(!other_ksp_info_enabled());
|
||||
assert!(!external_error_enabled());
|
||||
let enabled = ksp_logging_lib::LoggingSettings::new(
|
||||
let console_enabled = ksp_logging_lib::LoggingSettings::new(
|
||||
ksp_logging_lib::LogFilterLevel::Info,
|
||||
ksp_logging_lib::SpanEvents::NewAndClose,
|
||||
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
|
||||
std::option::Option::None,
|
||||
)
|
||||
.with_target_filter(ksp_logging_lib::TargetFilter::new(LOGGING_TARGET, ksp_logging_lib::LogFilterLevel::Trace));
|
||||
let reload_result = ksp_logging_lib::reinitialize(&mut guard, &enabled);
|
||||
let reload_result = ksp_logging_lib::reinitialize(&mut guard, &console_enabled);
|
||||
assert!(reload_result.is_ok());
|
||||
assert_eq!(guard.settings(), &enabled);
|
||||
assert_eq!(guard.settings(), &console_enabled);
|
||||
assert!(logging_trace_enabled());
|
||||
assert!(other_ksp_info_enabled());
|
||||
assert!(!other_ksp_debug_enabled());
|
||||
assert!(!external_error_enabled());
|
||||
let unsupported_file = ksp_logging_lib::LoggingSettings::new(
|
||||
let blocked_directory = root.join("not-a-directory");
|
||||
let create_root = std::fs::create_dir_all(root.as_path());
|
||||
assert!(create_root.is_ok());
|
||||
let create_blocker = std::fs::write(blocked_directory.as_path(), b"file blocks directory creation");
|
||||
assert!(create_blocker.is_ok());
|
||||
let invalid_file = ksp_logging_lib::LoggingSettings::new(
|
||||
ksp_logging_lib::LogFilterLevel::Error,
|
||||
ksp_logging_lib::SpanEvents::Full,
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(ksp_logging_lib::FileSettings::new("logs", "ksp", ksp_logging_lib::FileRotation::Daily)),
|
||||
std::option::Option::Some(ksp_logging_lib::FileSettings::new(blocked_directory.as_path(), "invalid", ksp_logging_lib::FileRotation::Daily)),
|
||||
);
|
||||
let failed_reload = ksp_logging_lib::reinitialize(&mut guard, &unsupported_file);
|
||||
let failed_reload = ksp_logging_lib::reinitialize(&mut guard, &invalid_file);
|
||||
assert!(failed_reload.is_err());
|
||||
assert_eq!(guard.settings(), &enabled);
|
||||
let file_error = match failed_reload {
|
||||
std::result::Result::Ok(()) => return,
|
||||
std::result::Result::Err(error) => error,
|
||||
};
|
||||
assert_eq!(file_error.code(), ksp_logging_lib::ERROR_CODE_FILE_OUTPUT_INITIALIZATION_FAILED);
|
||||
assert_eq!(guard.settings(), &console_enabled);
|
||||
assert!(logging_trace_enabled());
|
||||
assert!(!external_error_enabled());
|
||||
let log_directory = root.join("logs");
|
||||
let file_enabled = ksp_logging_lib::LoggingSettings::new(
|
||||
ksp_logging_lib::LogFilterLevel::Info,
|
||||
ksp_logging_lib::SpanEvents::Off,
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(ksp_logging_lib::FileSettings::new(log_directory.as_path(), "runtime-test.log", ksp_logging_lib::FileRotation::Never)),
|
||||
);
|
||||
let file_reload = ksp_logging_lib::reinitialize(&mut guard, &file_enabled);
|
||||
assert!(file_reload.is_ok());
|
||||
ksp_logging_lib::info!(target: LOGGING_TARGET, "file \x1b[31moutput\x1b[0m marker");
|
||||
tracing::error!(target: EXTERNAL_TARGET, "external marker must remain silent");
|
||||
let disable_after_file = ksp_logging_lib::reinitialize(&mut guard, &disabled);
|
||||
assert!(disable_after_file.is_ok());
|
||||
let file_text = read_directory_text(log_directory.as_path());
|
||||
assert!(file_text.contains("file output marker"));
|
||||
assert!(file_text.contains(LOGGING_TARGET));
|
||||
assert!(file_text.contains("runtime.rs"));
|
||||
assert!(!file_text.contains("\x1b["));
|
||||
assert!(!file_text.contains("external marker must remain silent"));
|
||||
let dropped = guard.dropped_lines();
|
||||
assert_eq!(dropped.total(), dropped.console().saturating_add(dropped.file()));
|
||||
let second_initialize = ksp_logging_lib::initialize(&disabled);
|
||||
assert!(second_initialize.is_err());
|
||||
let error = match second_initialize {
|
||||
@@ -72,4 +145,5 @@ fn global_runtime_supports_takeover_hot_reload_and_single_initialization() {
|
||||
std::result::Result::Err(error) => error,
|
||||
};
|
||||
assert_eq!(error.code(), ksp_logging_lib::ERROR_CODE_ALREADY_INITIALIZED);
|
||||
reset_directory(root.as_path());
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-logging-lib/unit_tests/runtime.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
#[test]
|
||||
fn level_mapping_covers_all_ksp_levels() {
|
||||
@@ -39,35 +39,52 @@ fn span_event_mapping_supports_disabled_timing_and_full_lifecycle() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_output_is_rejected_until_file_runtime_is_introduced() {
|
||||
let settings = crate::LoggingSettings::new(
|
||||
crate::LogFilterLevel::Info,
|
||||
crate::SpanEvents::Off,
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(crate::FileSettings::new("logs", "ksp", crate::FileRotation::Daily)),
|
||||
);
|
||||
let result = super::prepare_runtime_layers(&settings);
|
||||
assert!(result.is_err());
|
||||
let error = match result {
|
||||
std::result::Result::Ok(_) => return,
|
||||
std::result::Result::Err(error) => error,
|
||||
};
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_SETTINGS);
|
||||
fn file_rotation_mapping_covers_supported_cadences() {
|
||||
assert_eq!(super::map_file_rotation(crate::FileRotation::Never), tracing_appender::rolling::Rotation::NEVER);
|
||||
assert_eq!(super::map_file_rotation(crate::FileRotation::Hourly), tracing_appender::rolling::Rotation::HOURLY);
|
||||
assert_eq!(super::map_file_rotation(crate::FileRotation::Daily), tracing_appender::rolling::Rotation::DAILY);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn console_runtime_keeps_takeover_filter_separate_from_formatter() {
|
||||
fn disabled_runtime_has_no_layers_or_outputs() {
|
||||
let settings = crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::option::Option::None);
|
||||
let result = super::prepare_runtime(&settings);
|
||||
assert!(result.is_ok());
|
||||
let prepared = match result {
|
||||
std::result::Result::Ok(prepared) => prepared,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert!(prepared.layers.is_empty());
|
||||
assert!(prepared.outputs.console.is_none());
|
||||
assert!(prepared.outputs.file.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn console_runtime_keeps_takeover_filter_separate_from_formatter_and_owns_guard() {
|
||||
let settings = crate::LoggingSettings::new(
|
||||
crate::LogFilterLevel::Info,
|
||||
crate::SpanEvents::Off,
|
||||
std::option::Option::Some(crate::ConsoleSettings::stdout()),
|
||||
std::option::Option::None,
|
||||
);
|
||||
let result = super::prepare_runtime_layers(&settings);
|
||||
let result = super::prepare_runtime(&settings);
|
||||
assert!(result.is_ok());
|
||||
let layers = match result {
|
||||
std::result::Result::Ok(layers) => layers,
|
||||
let prepared = match result {
|
||||
std::result::Result::Ok(prepared) => prepared,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(layers.len(), 2);
|
||||
assert_eq!(prepared.layers.len(), 2);
|
||||
assert!(prepared.outputs.console.is_some());
|
||||
assert!(prepared.outputs.file.is_none());
|
||||
assert_eq!(prepared.outputs.dropped_lines(), crate::DroppedLines::zero());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dropped_line_snapshots_add_saturating_by_sink() {
|
||||
let first = crate::DroppedLines { console: usize::MAX, file: 4 };
|
||||
let second = crate::DroppedLines { console: 1, file: 7 };
|
||||
let combined = first.saturating_add(second);
|
||||
assert_eq!(combined.console(), usize::MAX);
|
||||
assert_eq!(combined.file(), 11);
|
||||
assert_eq!(combined.total(), usize::MAX);
|
||||
}
|
||||
|
||||
30
crates/ksp-logging-lib/unit_tests/writer.rs
Normal file
30
crates/ksp-logging-lib/unit_tests/writer.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
// file: crates/ksp-logging-lib/unit_tests/writer.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn ansi_writer_strips_csi_sequences() {
|
||||
let mut writer = super::StripAnsiWriter::new(std::vec::Vec::<u8>::new());
|
||||
let write_result = std::io::Write::write_all(&mut writer, b"before\x1b[31mred\x1b[0mafter");
|
||||
assert!(write_result.is_ok());
|
||||
assert_eq!(writer.into_inner(), b"beforeredafter");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ansi_writer_preserves_state_across_split_writes() {
|
||||
let mut writer = super::StripAnsiWriter::new(std::vec::Vec::<u8>::new());
|
||||
let first = std::io::Write::write_all(&mut writer, b"a\x1b[");
|
||||
let second = std::io::Write::write_all(&mut writer, b"32mb");
|
||||
assert!(first.is_ok());
|
||||
assert!(second.is_ok());
|
||||
assert_eq!(writer.into_inner(), b"ab");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ansi_writer_strips_osc_sequences_terminated_by_bell_or_st() {
|
||||
let mut writer = super::StripAnsiWriter::new(std::vec::Vec::<u8>::new());
|
||||
let first = std::io::Write::write_all(&mut writer, b"a\x1b]0;title\x07b");
|
||||
let second = std::io::Write::write_all(&mut writer, b"c\x1b]8;;https://example.invalid\x1b\\d");
|
||||
assert!(first.is_ok());
|
||||
assert!(second.is_ok());
|
||||
assert_eq!(writer.into_inner(), b"abcd");
|
||||
}
|
||||
Reference in New Issue
Block a user