v0.1.2-pre.004
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 30
|
# version: 31
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = ["crates/ksp-core-lib", "crates/ksp-logging-lib"]
|
members = ["crates/ksp-core-lib", "crates/ksp-logging-lib"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.2-pre.3.fix.1"
|
version = "0.1.2-pre.4"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||||
@@ -17,6 +17,7 @@ publish = false
|
|||||||
solana-pubkey = { version = "^4.3", default-features = false }
|
solana-pubkey = { version = "^4.3", default-features = false }
|
||||||
tracing = { version = "^0.1", default-features = false, features = ["std"] }
|
tracing = { version = "^0.1", default-features = false, features = ["std"] }
|
||||||
tracing-subscriber = { version = "^0.3", default-features = false, features = ["fmt"] }
|
tracing-subscriber = { version = "^0.3", default-features = false, features = ["fmt"] }
|
||||||
|
tracing-appender = { version = "^0.2", default-features = false }
|
||||||
|
|
||||||
[workspace.lints.rust]
|
[workspace.lints.rust]
|
||||||
missing_docs = "warn"
|
missing_docs = "warn"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# file: crates/ksp-logging-lib/Cargo.toml
|
# file: crates/ksp-logging-lib/Cargo.toml
|
||||||
# version: 2
|
# version: 3
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "ksp-logging-lib"
|
name = "ksp-logging-lib"
|
||||||
@@ -11,6 +11,7 @@ repository.workspace = true
|
|||||||
ksp-core-lib = { path = "../ksp-core-lib" }
|
ksp-core-lib = { path = "../ksp-core-lib" }
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
|
tracing-appender.workspace = true
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-logging-lib/src/error.rs
|
// file: crates/ksp-logging-lib/src/error.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
/// Error code used when runtime logging settings are invalid.
|
/// 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");
|
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");
|
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.
|
/// 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");
|
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
|
// file: crates/ksp-logging-lib/src/lib.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
@@ -7,22 +7,27 @@
|
|||||||
//! KSP-owned logging and tracing facade.
|
//! 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
|
//! 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
|
//! `tracing` stack. `0.1.2-pre.004` owns the single global subscriber, KSP takeover filtering, hot reload, non-blocking console/file outputs, rolling file
|
||||||
//! hot reload. Non-blocking writers, file output, ANSI stripping and writer guards are completed by the following prerelease.
|
//! appenders, ANSI stripping, dropped-line counters and the worker guards required to flush active queues.
|
||||||
|
|
||||||
mod error;
|
mod error;
|
||||||
mod macros;
|
mod macros;
|
||||||
mod runtime;
|
mod runtime;
|
||||||
mod settings;
|
mod settings;
|
||||||
mod span;
|
mod span;
|
||||||
|
mod writer;
|
||||||
|
|
||||||
/// Error code used when a global logging subscriber is already installed.
|
/// Error code used when a global logging subscriber is already installed.
|
||||||
pub use self::error::ERROR_CODE_ALREADY_INITIALIZED;
|
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.
|
/// Error code used when runtime logging settings are invalid.
|
||||||
pub use self::error::ERROR_CODE_INVALID_SETTINGS;
|
pub use self::error::ERROR_CODE_INVALID_SETTINGS;
|
||||||
/// Error code used when a hot reload cannot replace the active runtime layers.
|
/// Error code used when a hot reload cannot replace the active runtime layers.
|
||||||
pub use self::error::ERROR_CODE_RELOAD_FAILED;
|
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;
|
pub use self::runtime::LoggingGuard;
|
||||||
/// Installs the global KSP tracing subscriber.
|
/// Installs the global KSP tracing subscriber.
|
||||||
pub use self::runtime::initialize;
|
pub use self::runtime::initialize;
|
||||||
|
|||||||
@@ -1,13 +1,52 @@
|
|||||||
// file: crates/ksp-logging-lib/src/runtime.rs
|
// file: crates/ksp-logging-lib/src/runtime.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
use tracing_subscriber::Layer; // rust-rules: trait-import
|
use tracing_subscriber::Layer; // rust-rules: trait-import
|
||||||
use tracing_subscriber::layer::SubscriberExt; // 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 {
|
pub struct LoggingGuard {
|
||||||
reload_handle: RuntimeReloadHandle,
|
reload_handle: RuntimeReloadHandle,
|
||||||
settings: crate::LoggingSettings,
|
settings: crate::LoggingSettings,
|
||||||
|
outputs: RuntimeOutputs,
|
||||||
|
retired_dropped_lines: crate::DroppedLines,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LoggingGuard {
|
impl LoggingGuard {
|
||||||
@@ -16,23 +55,76 @@ impl LoggingGuard {
|
|||||||
pub fn settings(&self) -> &crate::LoggingSettings {
|
pub fn settings(&self) -> &crate::LoggingSettings {
|
||||||
return &self.settings;
|
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 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 RuntimeLayers = std::vec::Vec<BoxedRuntimeLayer>;
|
||||||
type RuntimeReloadHandle = tracing_subscriber::reload::Handle<RuntimeLayers, tracing_subscriber::Registry>;
|
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.
|
/// 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
|
/// 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
|
||||||
/// KSP logging configuration without installing a second global subscriber.
|
/// [`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> {
|
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> {
|
return prepare_runtime(settings).and_then(|prepared| -> ksp_core_lib::Result<crate::LoggingGuard> {
|
||||||
let (reload_layer, reload_handle) = tracing_subscriber::reload::Layer::new(runtime_layers);
|
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 subscriber = tracing_subscriber::registry().with(reload_layer);
|
||||||
let install_result = tracing::subscriber::set_global_default(subscriber);
|
let install_result = tracing::subscriber::set_global_default(subscriber);
|
||||||
return match install_result {
|
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(
|
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),
|
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
|
/// New runtime layers, writers and guards are fully prepared before the reload is attempted. If validation or preparation fails, the currently active
|
||||||
/// unchanged.
|
/// 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<()> {
|
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<()> {
|
return prepare_runtime(settings).and_then(|prepared| -> ksp_core_lib::Result<()> {
|
||||||
let reload_result = guard.reload_handle.reload(runtime_layers);
|
let PreparedRuntime { layers, outputs } = prepared;
|
||||||
|
let reload_result = guard.reload_handle.reload(layers);
|
||||||
return match reload_result {
|
return match reload_result {
|
||||||
std::result::Result::Ok(()) => {
|
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();
|
guard.settings = settings.clone();
|
||||||
|
drop(retired_outputs);
|
||||||
std::result::Result::Ok(())
|
std::result::Result::Ok(())
|
||||||
},
|
},
|
||||||
std::result::Result::Err(error) => std::result::Result::Err(
|
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();
|
let validation_error = settings.validate().err();
|
||||||
if let std::option::Option::Some(error) = validation_error {
|
if let std::option::Option::Some(error) = validation_error {
|
||||||
return std::result::Result::Err(error);
|
return std::result::Result::Err(error);
|
||||||
}
|
}
|
||||||
if settings.file().is_some() {
|
if settings.console().is_none() && settings.file().is_none() {
|
||||||
return std::result::Result::Err(
|
return std::result::Result::Ok(PreparedRuntime { layers: RuntimeLayers::new(), outputs: RuntimeOutputs::default() });
|
||||||
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 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();
|
let mut layers = RuntimeLayers::new();
|
||||||
if let std::option::Option::Some(console) = settings.console() {
|
layers.push(std::boxed::Box::new(build_target_filter(settings)));
|
||||||
let takeover_filter: BoxedRuntimeLayer = std::boxed::Box::new(build_target_filter(settings));
|
let mut outputs = RuntimeOutputs::default();
|
||||||
layers.push(takeover_filter);
|
if let std::option::Option::Some(console) = prepared_console {
|
||||||
layers.push(build_console_layer(console, settings));
|
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 {
|
fn build_console_output(console: &crate::ConsoleSettings, settings: &crate::LoggingSettings) -> PreparedOutput {
|
||||||
let writer = match console.output() {
|
return match console.output() {
|
||||||
crate::ConsoleOutput::Stdout => tracing_subscriber::fmt::writer::BoxMakeWriter::new(std::io::stdout),
|
crate::ConsoleOutput::Stdout => build_non_blocking_output(std::io::stdout(), "ksp-logging-console", settings),
|
||||||
crate::ConsoleOutput::Stderr => tracing_subscriber::fmt::writer::BoxMakeWriter::new(std::io::stderr),
|
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_writer(writer)
|
||||||
.with_ansi(false)
|
.with_ansi(false)
|
||||||
.with_target(true)
|
.with_target(true)
|
||||||
|
.with_file(true)
|
||||||
|
.with_line_number(true)
|
||||||
.with_span_events(map_span_events(settings.span_events()))
|
.with_span_events(map_span_events(settings.span_events()))
|
||||||
.boxed();
|
.boxed();
|
||||||
return layer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_target_filter(settings: &crate::LoggingSettings) -> tracing_subscriber::filter::Targets {
|
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 {
|
fn map_span_events(span_events: crate::SpanEvents) -> tracing_subscriber::fmt::format::FmtSpan {
|
||||||
return match span_events {
|
return match span_events {
|
||||||
crate::SpanEvents::Off => tracing_subscriber::fmt::format::FmtSpan::NONE,
|
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
|
// 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`.
|
//! 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 _reinitialize = ksp_logging_lib::reinitialize;
|
||||||
let _already_initialized = ksp_logging_lib::ERROR_CODE_ALREADY_INITIALIZED;
|
let _already_initialized = ksp_logging_lib::ERROR_CODE_ALREADY_INITIALIZED;
|
||||||
let _reload_failed = ksp_logging_lib::ERROR_CODE_RELOAD_FAILED;
|
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
|
// 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 LOGGING_TARGET: &str = "ksp-logging-lib";
|
||||||
const OTHER_KSP_TARGET: &str = "ksp-store-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);
|
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]
|
#[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(
|
let disabled = ksp_logging_lib::LoggingSettings::new(
|
||||||
ksp_logging_lib::LogFilterLevel::Info,
|
ksp_logging_lib::LogFilterLevel::Info,
|
||||||
ksp_logging_lib::SpanEvents::Off,
|
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::Ok(guard) => guard,
|
||||||
std::result::Result::Err(_) => return,
|
std::result::Result::Err(_) => return,
|
||||||
};
|
};
|
||||||
|
assert_eq!(guard.dropped_lines(), ksp_logging_lib::DroppedLines::zero());
|
||||||
assert!(!logging_trace_enabled());
|
assert!(!logging_trace_enabled());
|
||||||
assert!(!other_ksp_info_enabled());
|
assert!(!other_ksp_info_enabled());
|
||||||
assert!(!external_error_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::LogFilterLevel::Info,
|
||||||
ksp_logging_lib::SpanEvents::NewAndClose,
|
ksp_logging_lib::SpanEvents::NewAndClose,
|
||||||
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
|
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
|
||||||
std::option::Option::None,
|
std::option::Option::None,
|
||||||
)
|
)
|
||||||
.with_target_filter(ksp_logging_lib::TargetFilter::new(LOGGING_TARGET, ksp_logging_lib::LogFilterLevel::Trace));
|
.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!(reload_result.is_ok());
|
||||||
assert_eq!(guard.settings(), &enabled);
|
assert_eq!(guard.settings(), &console_enabled);
|
||||||
assert!(logging_trace_enabled());
|
assert!(logging_trace_enabled());
|
||||||
assert!(other_ksp_info_enabled());
|
assert!(other_ksp_info_enabled());
|
||||||
assert!(!other_ksp_debug_enabled());
|
assert!(!other_ksp_debug_enabled());
|
||||||
assert!(!external_error_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::LogFilterLevel::Error,
|
||||||
ksp_logging_lib::SpanEvents::Full,
|
ksp_logging_lib::SpanEvents::Full,
|
||||||
std::option::Option::None,
|
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!(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!(logging_trace_enabled());
|
||||||
assert!(!external_error_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);
|
let second_initialize = ksp_logging_lib::initialize(&disabled);
|
||||||
assert!(second_initialize.is_err());
|
assert!(second_initialize.is_err());
|
||||||
let error = match second_initialize {
|
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,
|
std::result::Result::Err(error) => error,
|
||||||
};
|
};
|
||||||
assert_eq!(error.code(), ksp_logging_lib::ERROR_CODE_ALREADY_INITIALIZED);
|
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
|
// file: crates/ksp-logging-lib/unit_tests/runtime.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn level_mapping_covers_all_ksp_levels() {
|
fn level_mapping_covers_all_ksp_levels() {
|
||||||
@@ -39,35 +39,52 @@ fn span_event_mapping_supports_disabled_timing_and_full_lifecycle() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn file_output_is_rejected_until_file_runtime_is_introduced() {
|
fn file_rotation_mapping_covers_supported_cadences() {
|
||||||
let settings = crate::LoggingSettings::new(
|
assert_eq!(super::map_file_rotation(crate::FileRotation::Never), tracing_appender::rolling::Rotation::NEVER);
|
||||||
crate::LogFilterLevel::Info,
|
assert_eq!(super::map_file_rotation(crate::FileRotation::Hourly), tracing_appender::rolling::Rotation::HOURLY);
|
||||||
crate::SpanEvents::Off,
|
assert_eq!(super::map_file_rotation(crate::FileRotation::Daily), tracing_appender::rolling::Rotation::DAILY);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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(
|
let settings = crate::LoggingSettings::new(
|
||||||
crate::LogFilterLevel::Info,
|
crate::LogFilterLevel::Info,
|
||||||
crate::SpanEvents::Off,
|
crate::SpanEvents::Off,
|
||||||
std::option::Option::Some(crate::ConsoleSettings::stdout()),
|
std::option::Option::Some(crate::ConsoleSettings::stdout()),
|
||||||
std::option::Option::None,
|
std::option::Option::None,
|
||||||
);
|
);
|
||||||
let result = super::prepare_runtime_layers(&settings);
|
let result = super::prepare_runtime(&settings);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
let layers = match result {
|
let prepared = match result {
|
||||||
std::result::Result::Ok(layers) => layers,
|
std::result::Result::Ok(prepared) => prepared,
|
||||||
std::result::Result::Err(_) => return,
|
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");
|
||||||
|
}
|
||||||
301
deltas/0.1.2/pre.004.md
Normal file
301
deltas/0.1.2/pre.004.md
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
<!-- file: deltas/0.1.2/pre.004.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta 0.1.2-pre.004
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
Livraison précédente validée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-pre.003-fix.001
|
||||||
|
```
|
||||||
|
|
||||||
|
La base de développement validée porte :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = "0.1.2-pre.3.fix.1"
|
||||||
|
Cargo.toml header version = 30
|
||||||
|
```
|
||||||
|
|
||||||
|
Les validations remontées avant l'ouverture de cette tranche sont propres :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Compléter le runtime Logging avec les sorties réellement retenues pour `0.1.2` :
|
||||||
|
|
||||||
|
- ajouter `tracing-appender` ;
|
||||||
|
- rendre console et fichier non bloquants pour le caller ;
|
||||||
|
- posséder les `WorkerGuard` jusqu'au reload/shutdown approprié ;
|
||||||
|
- exposer les dropped-line counters ;
|
||||||
|
- activer le fichier `Never/Hourly/Daily` avec construction fallible ;
|
||||||
|
- supprimer les séquences ANSI avant persistence ;
|
||||||
|
- conserver le takeover et le hot reload transactionnel établis par `pre.003-fix.001`.
|
||||||
|
|
||||||
|
## Version Cargo
|
||||||
|
|
||||||
|
`workspace.package.version` passe de :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-pre.3.fix.1
|
||||||
|
```
|
||||||
|
|
||||||
|
à :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-pre.4
|
||||||
|
```
|
||||||
|
|
||||||
|
L'identifiant de livraison est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-pre.004
|
||||||
|
```
|
||||||
|
|
||||||
|
Le header du `Cargo.toml` racine passe de version 30 à 31.
|
||||||
|
|
||||||
|
## Dépendance `tracing-appender`
|
||||||
|
|
||||||
|
L'audit du 2026-08-14 confirme `tracing-appender 0.2.5`, publié le 2026-04-17, dans la génération `^0.2`.
|
||||||
|
|
||||||
|
La dépendance est centralisée sous `[workspace.dependencies]` :
|
||||||
|
|
||||||
|
```toml
|
||||||
|
tracing-appender = { version = "^0.2", default-features = false }
|
||||||
|
```
|
||||||
|
|
||||||
|
`ksp-logging-lib` la consomme avec :
|
||||||
|
|
||||||
|
```toml
|
||||||
|
tracing-appender.workspace = true
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune feature optionnelle n'est activée. Le backend expose `NonBlockingBuilder`, `WorkerGuard`, `ErrorCounter` et `RollingFileAppender` sans feature supplémentaire.
|
||||||
|
|
||||||
|
## Console non bloquante
|
||||||
|
|
||||||
|
La console n'utilise plus directement `stdout`/`stderr` dans le formatter.
|
||||||
|
|
||||||
|
Chaque sink console construit :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Stdout | Stderr
|
||||||
|
-> NonBlockingBuilder(lossy = true)
|
||||||
|
-> fmt layer
|
||||||
|
+ WorkerGuard
|
||||||
|
+ ErrorCounter
|
||||||
|
```
|
||||||
|
|
||||||
|
Le mode lossy est explicite : lorsque la queue est saturée, un log peut être abandonné au lieu de bloquer le thread appelant.
|
||||||
|
|
||||||
|
Le thread worker console est nommé :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-logging-console
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichier et rotation
|
||||||
|
|
||||||
|
`FileSettings` est maintenant réellement consommé par le runtime.
|
||||||
|
|
||||||
|
Le mapping est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
FileRotation::Never -> Rotation::NEVER
|
||||||
|
FileRotation::Hourly -> Rotation::HOURLY
|
||||||
|
FileRotation::Daily -> Rotation::DAILY
|
||||||
|
```
|
||||||
|
|
||||||
|
Le runtime utilise uniquement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
RollingFileAppender::builder()
|
||||||
|
.rotation(...)
|
||||||
|
.filename_prefix(...)
|
||||||
|
.build(directory)
|
||||||
|
```
|
||||||
|
|
||||||
|
La forme builder retourne un `Result`; aucune API de construction qui panique n'est utilisée par KSP.
|
||||||
|
|
||||||
|
Un échec retourne :
|
||||||
|
|
||||||
|
```text
|
||||||
|
logging.file_output_initialization_failed
|
||||||
|
```
|
||||||
|
|
||||||
|
avec le directory, le file-name prefix et l'erreur `InitError` externe conservés dans le contrat Core.
|
||||||
|
|
||||||
|
## Stripping ANSI
|
||||||
|
|
||||||
|
Le fichier est composé comme suit :
|
||||||
|
|
||||||
|
```text
|
||||||
|
fmt layer
|
||||||
|
-> NonBlocking queue
|
||||||
|
-> StripAnsiWriter
|
||||||
|
-> RollingFileAppender
|
||||||
|
```
|
||||||
|
|
||||||
|
Le stripping est donc effectué par le thread logging et non par le caller.
|
||||||
|
|
||||||
|
`StripAnsiWriter` conserve un état entre les appels `Write` afin de retirer correctement une séquence terminal coupée entre plusieurs buffers. La première surface couvre :
|
||||||
|
|
||||||
|
- CSI (`ESC [` ... final byte) ;
|
||||||
|
- OSC terminé par BEL ou ST ;
|
||||||
|
- autres chaînes terminal ESC de type DCS/SOS/PM/APC terminées par ST.
|
||||||
|
|
||||||
|
Ce mécanisme est générique et n'introduit aucune dépendance Tauri.
|
||||||
|
|
||||||
|
## Formatter humain
|
||||||
|
|
||||||
|
Console et fichier partagent le formatter humain KSP avec :
|
||||||
|
|
||||||
|
- timestamp standard `tracing-subscriber` ;
|
||||||
|
- niveau ;
|
||||||
|
- target ;
|
||||||
|
- champs/message ;
|
||||||
|
- source file ;
|
||||||
|
- line number ;
|
||||||
|
- ANSI du formatter désactivé ;
|
||||||
|
- lifecycle de spans selon `SpanEvents`.
|
||||||
|
|
||||||
|
La ponctuation exacte du formatter reste hors contrat public.
|
||||||
|
|
||||||
|
## Ownership et reload
|
||||||
|
|
||||||
|
`LoggingGuard` possède désormais les outputs actifs :
|
||||||
|
|
||||||
|
```text
|
||||||
|
LoggingGuard
|
||||||
|
├── reload handle
|
||||||
|
├── current LoggingSettings
|
||||||
|
├── active console WorkerGuard/ErrorCounter
|
||||||
|
├── active file WorkerGuard/ErrorCounter
|
||||||
|
└── cumulative retired dropped-line counters
|
||||||
|
```
|
||||||
|
|
||||||
|
`reinitialize()` :
|
||||||
|
|
||||||
|
1. valide les nouveaux settings ;
|
||||||
|
2. construit entièrement le nouveau file appender et tous les nouveaux non-blocking writers/guards ;
|
||||||
|
3. construit les nouveaux layers ;
|
||||||
|
4. remplace le `Vec` reloadable ;
|
||||||
|
5. mémorise les dropped lines des anciens sinks ;
|
||||||
|
6. remplace les outputs actifs ;
|
||||||
|
7. détruit les anciens `WorkerGuard`, provoquant leur flush borné par le backend.
|
||||||
|
|
||||||
|
Une erreur avant le swap détruit uniquement les nouveaux outputs préparés et laisse l'ancienne configuration active.
|
||||||
|
|
||||||
|
## Dropped lines
|
||||||
|
|
||||||
|
Nouvelle surface publique :
|
||||||
|
|
||||||
|
```text
|
||||||
|
DroppedLines
|
||||||
|
LoggingGuard::dropped_lines() -> DroppedLines
|
||||||
|
```
|
||||||
|
|
||||||
|
`DroppedLines` expose :
|
||||||
|
|
||||||
|
```text
|
||||||
|
console()
|
||||||
|
file()
|
||||||
|
total()
|
||||||
|
```
|
||||||
|
|
||||||
|
Les valeurs sont cumulées pour toute la durée de vie du `LoggingGuard`, y compris après plusieurs hot reloads. Les `ErrorCounter` de `tracing-appender` ne sont pas exposés directement aux consumers.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
### Unitaires
|
||||||
|
|
||||||
|
- mapping `Never/Hourly/Daily` ;
|
||||||
|
- runtime sans sink ;
|
||||||
|
- console préparée avec filter séparé, non-blocking output et guard ;
|
||||||
|
- addition saturante des dropped-line counters ;
|
||||||
|
- stripping CSI ;
|
||||||
|
- stripping d'une CSI coupée entre deux writes ;
|
||||||
|
- stripping OSC terminé par BEL/ST.
|
||||||
|
|
||||||
|
### Intégration runtime global
|
||||||
|
|
||||||
|
Le test global vérifie désormais :
|
||||||
|
|
||||||
|
1. initialisation silencieuse sans sink ;
|
||||||
|
2. hot reload console non bloquante ;
|
||||||
|
3. takeover KSP et silence `sqlx` ;
|
||||||
|
4. erreur de création d'un file appender sur un chemin invalide ;
|
||||||
|
5. conservation des settings précédents après cet échec ;
|
||||||
|
6. hot reload vers un fichier `Never` ;
|
||||||
|
7. émission d'un message contenant des codes ANSI ;
|
||||||
|
8. retrait du sink fichier par reload, donc drop/flush de son guard ;
|
||||||
|
9. présence du message KSP dans le fichier ;
|
||||||
|
10. absence des codes ANSI persistés ;
|
||||||
|
11. absence du message externe `sqlx` ;
|
||||||
|
12. présence du target et de la source ;
|
||||||
|
13. lecture de la statistique cumulée ;
|
||||||
|
14. refus d'un second `initialize()`.
|
||||||
|
|
||||||
|
La saturation déterministe avec une queue artificiellement petite est reportée à `pre.005`, où un writer de test injecté pourra être utilisé sans rendre la capacité de queue publique dans `LoggingSettings`.
|
||||||
|
|
||||||
|
## Fichiers ajoutés
|
||||||
|
|
||||||
|
- `crates/ksp-logging-lib/src/writer.rs`
|
||||||
|
- `crates/ksp-logging-lib/unit_tests/writer.rs`
|
||||||
|
- `deltas/0.1.2/pre.004.md`
|
||||||
|
|
||||||
|
## Fichiers modifiés
|
||||||
|
|
||||||
|
- `Cargo.toml`
|
||||||
|
- `crates/ksp-logging-lib/Cargo.toml`
|
||||||
|
- `crates/ksp-logging-lib/src/error.rs`
|
||||||
|
- `crates/ksp-logging-lib/src/lib.rs`
|
||||||
|
- `crates/ksp-logging-lib/src/runtime.rs`
|
||||||
|
- `crates/ksp-logging-lib/unit_tests/runtime.rs`
|
||||||
|
- `crates/ksp-logging-lib/tests/runtime.rs`
|
||||||
|
- `crates/ksp-logging-lib/tests/public_api.rs`
|
||||||
|
- `docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md`
|
||||||
|
|
||||||
|
## Validations exécutées pendant la préparation
|
||||||
|
|
||||||
|
- revérification documentaire officielle de `tracing-appender 0.2.5` ;
|
||||||
|
- vérification de la sémantique lossy de `NonBlockingBuilder` ;
|
||||||
|
- vérification de `WorkerGuard` et `ErrorCounter::dropped_lines()` ;
|
||||||
|
- vérification du builder fallible de `RollingFileAppender` ;
|
||||||
|
- contrôle TOML des manifests ;
|
||||||
|
- contrôle des headers `file:` / `version:` ;
|
||||||
|
- contrôle de la centralisation de `tracing-appender` sous `[workspace.dependencies]` ;
|
||||||
|
- contrôle que le code production ajouté n'utilise ni `unwrap`, ni `expect`, ni `panic`, ni opérateur `?`, ni `unsafe` ;
|
||||||
|
- contrôle que les usages directs de `tracing-appender` restent dans `ksp-logging-lib`.
|
||||||
|
|
||||||
|
## Validations à exécuter dans le workspace
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test --workspace
|
||||||
|
cargo tree -p ksp-logging-lib
|
||||||
|
cargo tree -p ksp-logging-lib -d
|
||||||
|
cargo tree -p ksp-logging-lib -e features
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune validation Cargo non exécutable dans l'environnement de préparation n'est déclarée réussie.
|
||||||
|
|
||||||
|
## Suite
|
||||||
|
|
||||||
|
Après validation de `pre.004`, passer à `0.1.2-pre.005` :
|
||||||
|
|
||||||
|
- concurrence/reloads répétés ;
|
||||||
|
- saturation déterministe et dropped lines ;
|
||||||
|
- audits de façade et usages directs de la stack tracing ;
|
||||||
|
- audits Cargo/features/doublons ;
|
||||||
|
- mesure grossière de l'overhead du reload/runtime ;
|
||||||
|
- compléments de tests et documentation de crate avant la tranche finale.
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
<!-- file: docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md -->
|
<!-- file: docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md -->
|
||||||
<!-- version: 6 -->
|
<!-- version: 7 -->
|
||||||
|
|
||||||
# Plan KSP 0.1.2 — Logging foundation
|
# Plan KSP 0.1.2 — Logging foundation
|
||||||
|
|
||||||
## Statut
|
## Statut
|
||||||
|
|
||||||
Plan actif de `0.1.2`, établi par `0.1.2-pre.001`, corrigé par `0.1.2-pre.001-fix.001`, concrétisé par la façade de `0.1.2-pre.002` puis étendu au runtime subscriber par `0.1.2-pre.003` et son correctif `0.1.2-pre.003-fix.001`.
|
Plan actif de `0.1.2`, établi par `0.1.2-pre.001`, corrigé par `0.1.2-pre.001-fix.001`, concrétisé par la façade de `0.1.2-pre.002`, étendu au runtime subscriber par `0.1.2-pre.003`/`pre.003-fix.001`, puis complété par les sorties non bloquantes de `0.1.2-pre.004`.
|
||||||
|
|
||||||
`pre.002-fix.001` a été validé dans l'environnement de développement avec `cargo fmt`, `cargo check`, `cargo clippy --workspace --all-targets` et `cargo test --workspace` propres sur la version Cargo `0.1.2-pre.2.fix.1`. Pour `pre.003`, `cargo fmt`, `cargo check` et `cargo clippy --workspace --all-targets` sont propres, mais `cargo test --workspace` a révélé un panic de `tracing-subscriber` lors du premier hot reload avec console : un `Filtered` nouvellement injecté dans le `Vec` reloadable ne possédait pas de `FilterId` enregistré. `pre.003-fix.001` sépare donc le `Targets` global du formatter afin qu'aucun `Filtered` ne soit remplacé par `Handle::reload`. Les writers non bloquants, le fichier, les guards et le stripping ANSI restent réservés à `pre.004`.
|
`pre.003-fix.001` a été validé dans l'environnement de développement avec `cargo fmt --all`, `cargo check --workspace`, `cargo clippy --workspace --all-targets` et `cargo test --workspace` propres sur la version Cargo `0.1.2-pre.3.fix.1`. `pre.004` ajoute `tracing-appender`, remplace la console synchrone par un writer non bloquant, introduit le fichier `Never/Hourly/Daily`, possède les `WorkerGuard`, expose des compteurs cumulés de dropped lines, déporte le stripping ANSI côté worker fichier et conserve le hot reload transactionnel des sinks. Les validations Cargo de cette nouvelle tranche restent à exécuter dans le workspace utilisateur avant validation.
|
||||||
|
|
||||||
## Base auditée
|
## Base auditée
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ tracing-appender 0.2.5 MSRV annoncé : rustc 1.63+
|
|||||||
|
|
||||||
Ces exigences restent inférieures au MSRV déjà imposé indirectement par la génération Solana retenue dans `0.1.1` (`solana-pubkey 4.3.0` / workspace Solana SDK auditée à Rust 1.89.0). Logging ne relève donc pas le plancher observé du workspace.
|
Ces exigences restent inférieures au MSRV déjà imposé indirectement par la génération Solana retenue dans `0.1.1` (`solana-pubkey 4.3.0` / workspace Solana SDK auditée à Rust 1.89.0). Logging ne relève donc pas le plancher observé du workspace.
|
||||||
|
|
||||||
Les versions sont revérifiées au moment exact de leur ajout effectif au manifest. Pour `pre.002`, `tracing` reste à `0.1.44`, publié dans la génération `^0.1`; `tracing-subscriber` et `tracing-appender` ne sont pas encore ajoutés.
|
Les versions sont revérifiées au moment exact de leur ajout effectif au manifest. `pre.002` a ajouté `tracing` dans la génération `^0.1`, `pre.003` a ajouté `tracing-subscriber` dans la génération `^0.3`, et `pre.004` revérifie puis ajoute `tracing-appender 0.2.5` dans la génération `^0.2`.
|
||||||
|
|
||||||
### `tracing`
|
### `tracing`
|
||||||
|
|
||||||
@@ -155,7 +155,15 @@ tracing-appender = { version = "^0.2", default-features = false }
|
|||||||
tracing-subscriber = { version = "^0.3", default-features = false, features = ["fmt"] }
|
tracing-subscriber = { version = "^0.3", default-features = false, features = ["fmt"] }
|
||||||
```
|
```
|
||||||
|
|
||||||
La feature `fmt` apporte `registry` et `std`, nécessaires à la composition des layers, `Targets`, `reload` et au formatter console. `env-filter`, `ansi`, `tracing-log`, `json`, `time` et les autres features optionnelles ne sont pas activées. `tracing-appender` reste absent jusqu'à `pre.004`, où il sera réellement consommé.
|
La feature `fmt` apporte `registry` et `std`, nécessaires à la composition des layers, `Targets`, `reload` et aux formatters. `env-filter`, `ansi`, `tracing-log`, `json`, `time` et les autres features optionnelles ne sont pas activées.
|
||||||
|
|
||||||
|
`pre.004` revérifie `tracing-appender 0.2.5` et ajoute réellement :
|
||||||
|
|
||||||
|
```toml
|
||||||
|
tracing-appender = { version = "^0.2", default-features = false }
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune feature optionnelle n'est activée. Le `NonBlockingBuilder` reste explicitement en mode lossy afin que la saturation de sa queue n'applique pas de backpressure au hot path.
|
||||||
|
|
||||||
Après chaque ajout réel :
|
Après chaque ajout réel :
|
||||||
|
|
||||||
@@ -468,7 +476,7 @@ Objectifs :
|
|||||||
|
|
||||||
Le mode de queue doit privilégier le non-blocage du caller : lorsque la queue est saturée, les lignes peuvent être abandonnées plutôt que d'appliquer une backpressure au hot path.
|
Le mode de queue doit privilégier le non-blocage du caller : lorsque la queue est saturée, les lignes peuvent être abandonnées plutôt que d'appliquer une backpressure au hot path.
|
||||||
|
|
||||||
Le compteur `ErrorCounter` correspondant est conservé par le runtime afin que les pertes puissent être observées/inspectées ; l'API publique exacte de cette statistique sera fixée pendant l'implémentation.
|
Le compteur `ErrorCounter` correspondant est conservé par le runtime. `pre.004` stabilise l'observation publique via `LoggingGuard::dropped_lines() -> DroppedLines`, avec compteurs `console`, `file` et `total` cumulés pendant toute la durée de vie du guard, y compris à travers les hot reloads.
|
||||||
|
|
||||||
Le format initial est un format humain unique. Il doit inclure au minimum :
|
Le format initial est un format humain unique. Il doit inclure au minimum :
|
||||||
|
|
||||||
@@ -522,6 +530,8 @@ La taille de file reste celle du backend tant qu'un besoin réel ne justifie pas
|
|||||||
|
|
||||||
La sortie fichier passe par un writer KSP de stripping ANSI avant persistence afin de supprimer les séquences de terminal déjà présentes dans les données écrites.
|
La sortie fichier passe par un writer KSP de stripping ANSI avant persistence afin de supprimer les séquences de terminal déjà présentes dans les données écrites.
|
||||||
|
|
||||||
|
`pre.004` place ce writer **derrière** la queue `NonBlocking`, autour du `RollingFileAppender`. Le parsing/stripping et l'I/O disque s'exécutent donc sur le worker logging plutôt que dans le hot path du caller. Le stripper conserve son état entre plusieurs appels `Write` afin de gérer une séquence ANSI coupée entre deux buffers ; les séquences CSI ainsi que les séquences terminal de type OSC/string terminées par BEL/ST sont couvertes.
|
||||||
|
|
||||||
Cette responsabilité reste générique : `ksp-logging-lib` ne dépend pas de Tauri. Elle évite simplement que des séquences ANSI injectées par une couche d'application/framework se retrouvent persistées dans les fichiers.
|
Cette responsabilité reste générique : `ksp-logging-lib` ne dépend pas de Tauri. Elle évite simplement que des séquences ANSI injectées par une couche d'application/framework se retrouvent persistées dans les fichiers.
|
||||||
|
|
||||||
Le stripping n'est pas présenté comme un mécanisme de redaction de données.
|
Le stripping n'est pas présenté comme un mécanisme de redaction de données.
|
||||||
@@ -740,6 +750,7 @@ ksp_logging_lib::ConsoleSettings
|
|||||||
ksp_logging_lib::FileRotation
|
ksp_logging_lib::FileRotation
|
||||||
ksp_logging_lib::FileSettings
|
ksp_logging_lib::FileSettings
|
||||||
ksp_logging_lib::LoggingSettings
|
ksp_logging_lib::LoggingSettings
|
||||||
|
ksp_logging_lib::DroppedLines
|
||||||
ksp_logging_lib::LoggingGuard
|
ksp_logging_lib::LoggingGuard
|
||||||
ksp_logging_lib::initialize
|
ksp_logging_lib::initialize
|
||||||
ksp_logging_lib::reinitialize
|
ksp_logging_lib::reinitialize
|
||||||
@@ -765,14 +776,16 @@ crates/ksp-logging-lib/
|
|||||||
│ ├── macros.rs
|
│ ├── macros.rs
|
||||||
│ ├── settings.rs
|
│ ├── settings.rs
|
||||||
│ ├── span.rs
|
│ ├── span.rs
|
||||||
│ └── runtime.rs
|
│ ├── runtime.rs
|
||||||
|
│ └── writer.rs
|
||||||
├── unit_tests/
|
├── unit_tests/
|
||||||
│ ├── settings.rs
|
│ ├── settings.rs
|
||||||
│ ├── span.rs
|
│ ├── span.rs
|
||||||
│ └── runtime.rs
|
│ ├── runtime.rs
|
||||||
|
│ └── writer.rs
|
||||||
└── tests/
|
└── tests/
|
||||||
├── callsite.rs
|
├── callsite.rs
|
||||||
├── reload.rs
|
├── runtime.rs
|
||||||
└── public_api.rs
|
└── public_api.rs
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -947,16 +960,23 @@ Le runtime reloadable reste un `Vec<Box<dyn Layer<Registry>>>` placé derrière
|
|||||||
|
|
||||||
### `0.1.2-pre.004` — non-blocking console/fichier + guards + ANSI + reload sinks
|
### `0.1.2-pre.004` — non-blocking console/fichier + guards + ANSI + reload sinks
|
||||||
|
|
||||||
Objectifs :
|
Statut : implémenté, validations Cargo à exécuter.
|
||||||
|
|
||||||
- revérifier puis ajouter `tracing-appender` ;
|
Réalisé :
|
||||||
- passer console et fichier sur writers non bloquants ;
|
|
||||||
- implémenter `FileSettings` et Never/Hourly/Daily ;
|
- `tracing-appender 0.2.5` ajouté via `^0.2`, sans feature optionnelle ;
|
||||||
- utiliser le builder fallible du file appender ;
|
- console stdout/stderr remplacée par `NonBlockingBuilder` en mode lossy ;
|
||||||
- conserver les `WorkerGuard` et `ErrorCounter` ;
|
- fichier optionnel réellement activé avec `RollingFileAppender::builder()` fallible et rotations `Never/Hourly/Daily` ;
|
||||||
- implémenter le stripping ANSI fichier ;
|
- console et fichier possèdent chacun leur `WorkerGuard` et leur `ErrorCounter` ;
|
||||||
- permettre le remplacement à chaud des sinks/settings concernés ;
|
- `LoggingGuard::dropped_lines()` expose un `DroppedLines` cumulatif pour console/fichier/total, sans perdre les compteurs des sinks retirés lors d'un reload ;
|
||||||
- tester flush/lifetime, saturation et erreurs de fichier.
|
- formatter humain enrichi avec target, source file et line, ANSI du formatter désactivé ;
|
||||||
|
- stripping ANSI fichier effectué côté worker, avant persistence, avec état conservé entre buffers ;
|
||||||
|
- hot reload prépare les nouveaux writers/guards avant le swap, remplace les layers, mémorise les dropped lines des anciens sinks puis détruit leurs guards pour provoquer leur flush ;
|
||||||
|
- une erreur de construction du nouveau file appender retourne `logging.file_output_initialization_failed` et conserve la configuration précédente ;
|
||||||
|
- test d'intégration étendu pour couvrir fichier, erreur transactionnelle, flush lors du retrait du sink, takeover externe et stripping ANSI ;
|
||||||
|
- tests unitaires ajoutés pour rotations, runtime disabled/non bloquant, statistiques et stripper ANSI.
|
||||||
|
|
||||||
|
La saturation déterministe des queues, les reloads concurrents et l'audit complet des dépendances restent à renforcer dans `pre.005`.
|
||||||
|
|
||||||
### `0.1.2-pre.005` — intégration + concurrence + tests + audits
|
### `0.1.2-pre.005` — intégration + concurrence + tests + audits
|
||||||
|
|
||||||
@@ -1031,7 +1051,7 @@ La release peut être stabilisée lorsque :
|
|||||||
- les validations workspace et audits présents sont propres ;
|
- les validations workspace et audits présents sont propres ;
|
||||||
- la documentation finale et le prompt `0.1.3` sont prêts.
|
- la documentation finale et le prompt `0.1.3` sont prêts.
|
||||||
|
|
||||||
## Questions ouvertes après `pre.003`
|
## Questions ouvertes après `pre.004`
|
||||||
|
|
||||||
Les deux questions d'API propres à `pre.002` sont résolues :
|
Les deux questions d'API propres à `pre.002` sont résolues :
|
||||||
|
|
||||||
@@ -1042,8 +1062,8 @@ La composition de reload est désormais fixée pour cette release à un `Vec` de
|
|||||||
|
|
||||||
Restent à confirmer par les prereleases suivantes sans remettre en cause ce contrat :
|
Restent à confirmer par les prereleases suivantes sans remettre en cause ce contrat :
|
||||||
|
|
||||||
1. l'API publique exacte d'observation des dropped lines ;
|
1. le détail visuel exact du formatter humain, sans transformer sa ponctuation en contrat public ;
|
||||||
2. le détail visuel exact du formatter humain, sans transformer sa ponctuation en contrat public ;
|
2. la stratégie de test déterministe de saturation des queues avec writer injecté, sans exposer la taille de queue dans `LoggingSettings` ;
|
||||||
3. le comportement de flush/rotation et le swap transactionnel des `WorkerGuard` lorsque les sinks non bloquants seront introduits.
|
3. les mesures d'overhead et tests de concurrence/reload prévus pour `pre.005`.
|
||||||
|
|
||||||
La prochaine action après validation de `pre.003-fix.001` est `0.1.2-pre.004` : `tracing-appender`, console/fichier non bloquants, guards, rotation, stripping ANSI et reload des sinks.
|
La prochaine action après validation de `pre.004` est `0.1.2-pre.005` : intégration, concurrence, saturation déterministe, audits de façade/dépendances/features et mesure grossière de l'overhead de reload.
|
||||||
|
|||||||
Reference in New Issue
Block a user