496 lines
20 KiB
Rust
496 lines
20 KiB
Rust
// file: crates/ksp-logging-lib/src/runtime.rs
|
|
// version: 13
|
|
|
|
use tracing_subscriber::Layer; // rust-rules: trait-import
|
|
use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import
|
|
|
|
/// 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) };
|
|
}
|
|
}
|
|
|
|
/// Metadata for one active persistent Logging file output.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct RuntimeFileMetadata {
|
|
output_id: std::string::String,
|
|
directory: std::path::PathBuf,
|
|
file_name_prefix: std::string::String,
|
|
rotation: crate::FileRotation,
|
|
}
|
|
|
|
impl RuntimeFileMetadata {
|
|
/// Returns the stable output identifier.
|
|
#[must_use]
|
|
pub fn output_id(&self) -> &str {
|
|
return self.output_id.as_str();
|
|
}
|
|
|
|
/// Returns the resolved directory containing this launch's files.
|
|
#[must_use]
|
|
pub fn directory(&self) -> &std::path::Path {
|
|
return self.directory.as_path();
|
|
}
|
|
|
|
/// Returns the effective launch-specific filename prefix passed to the rolling appender.
|
|
#[must_use]
|
|
pub fn file_name_prefix(&self) -> &str {
|
|
return self.file_name_prefix.as_str();
|
|
}
|
|
|
|
/// Returns the configured rotation cadence.
|
|
#[must_use]
|
|
pub const fn rotation(&self) -> crate::FileRotation {
|
|
return self.rotation;
|
|
}
|
|
}
|
|
|
|
/// 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,
|
|
runtime_identity: std::option::Option<crate::LoggingRuntimeIdentity>,
|
|
outputs: RuntimeOutputs,
|
|
retired_dropped_lines: crate::DroppedLines,
|
|
retired_file_dropped_lines: std::collections::HashMap<std::string::String, usize>,
|
|
}
|
|
|
|
impl LoggingGuard {
|
|
/// Returns the settings currently active in the KSP logging runtime.
|
|
#[must_use]
|
|
pub fn settings(&self) -> &crate::LoggingSettings {
|
|
return &self.settings;
|
|
}
|
|
|
|
/// Returns the stable application/launch identity attached to this runtime, when one was supplied at initialization.
|
|
#[must_use]
|
|
pub const fn runtime_identity(&self) -> std::option::Option<&crate::LoggingRuntimeIdentity> {
|
|
return self.runtime_identity.as_ref();
|
|
}
|
|
|
|
/// Returns metadata for the currently active persistent file outputs.
|
|
#[must_use]
|
|
pub fn active_file_outputs(&self) -> std::vec::Vec<crate::RuntimeFileMetadata> {
|
|
return self
|
|
.outputs
|
|
.files
|
|
.iter()
|
|
.map(|output| -> crate::RuntimeFileMetadata {
|
|
return output.metadata.clone();
|
|
})
|
|
.collect();
|
|
}
|
|
|
|
/// 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());
|
|
}
|
|
|
|
/// Returns cumulative dropped-line counters for one file `output_id` when that output has existed in the runtime.
|
|
#[must_use]
|
|
pub fn dropped_file_lines(&self, output_id: &str) -> std::option::Option<usize> {
|
|
let retired = self.retired_file_dropped_lines.get(output_id).copied();
|
|
let active = self.outputs.file_dropped_lines(output_id);
|
|
return match (retired, active) {
|
|
(std::option::Option::Some(retired), std::option::Option::Some(active)) => std::option::Option::Some(retired.saturating_add(active)),
|
|
(std::option::Option::Some(retired), std::option::Option::None) => std::option::Option::Some(retired),
|
|
(std::option::Option::None, std::option::Option::Some(active)) => std::option::Option::Some(active),
|
|
(std::option::Option::None, std::option::Option::None) => std::option::Option::None,
|
|
};
|
|
}
|
|
}
|
|
|
|
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>,
|
|
files: std::vec::Vec<RuntimeFileOutput>,
|
|
}
|
|
|
|
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 mut file = 0_usize;
|
|
for output in &self.files {
|
|
file = file.saturating_add(output.output.dropped_lines());
|
|
}
|
|
return crate::DroppedLines { console, file };
|
|
}
|
|
|
|
fn file_dropped_lines(&self, output_id: &str) -> std::option::Option<usize> {
|
|
return self
|
|
.files
|
|
.iter()
|
|
.find(|output| -> bool {
|
|
return output.metadata.output_id == output_id;
|
|
})
|
|
.map(|output| -> usize {
|
|
return output.output.dropped_lines();
|
|
});
|
|
}
|
|
|
|
fn accumulate_file_dropped_lines(&self, destination: &mut std::collections::HashMap<std::string::String, usize>) {
|
|
for output in &self.files {
|
|
let dropped = output.output.dropped_lines();
|
|
match destination.entry(output.metadata.output_id.clone()) {
|
|
std::collections::hash_map::Entry::Occupied(mut entry) => {
|
|
let cumulative = entry.get().saturating_add(dropped);
|
|
*entry.get_mut() = cumulative;
|
|
},
|
|
std::collections::hash_map::Entry::Vacant(entry) => {
|
|
entry.insert(dropped);
|
|
},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct RuntimeFileOutput {
|
|
metadata: crate::RuntimeFileMetadata,
|
|
output: RuntimeOutput,
|
|
}
|
|
|
|
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,
|
|
}
|
|
|
|
struct PreparedFileOutput {
|
|
metadata: crate::RuntimeFileMetadata,
|
|
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 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 initialize_runtime(settings, std::option::Option::None);
|
|
}
|
|
|
|
/// Installs the global KSP tracing subscriber and isolates persistent file outputs with one stable application launch identity.
|
|
pub fn initialize_with_identity(settings: &crate::LoggingSettings, identity: &crate::LoggingRuntimeIdentity) -> ksp_core_lib::Result<crate::LoggingGuard> {
|
|
return initialize_runtime(settings, std::option::Option::Some(identity.clone()));
|
|
}
|
|
|
|
fn initialize_runtime(
|
|
settings: &crate::LoggingSettings,
|
|
runtime_identity: std::option::Option<crate::LoggingRuntimeIdentity>,
|
|
) -> ksp_core_lib::Result<crate::LoggingGuard> {
|
|
return prepare_runtime_with_identity(settings, runtime_identity.as_ref()).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(),
|
|
runtime_identity,
|
|
outputs,
|
|
retired_dropped_lines: crate::DroppedLines::zero(),
|
|
retired_file_dropped_lines: std::collections::HashMap::new(),
|
|
}),
|
|
std::result::Result::Err(error) => std::result::Result::Err(
|
|
ksp_core_lib::Error::new(crate::ERROR_CODE_ALREADY_INITIALIZED, "the global KSP tracing subscriber is already installed").with_source(error),
|
|
),
|
|
};
|
|
});
|
|
}
|
|
|
|
/// Replaces the active KSP logging settings and non-blocking outputs without reinstalling the global subscriber.
|
|
///
|
|
/// 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. Retired
|
|
/// layers are then dropped before their worker guards so all retired `NonBlocking` senders are released before shutdown asks the workers to drain/flush.
|
|
pub fn reinitialize(guard: &mut crate::LoggingGuard, settings: &crate::LoggingSettings) -> ksp_core_lib::Result<()> {
|
|
return prepare_runtime_with_identity(settings, guard.runtime_identity.as_ref()).and_then(|prepared| -> ksp_core_lib::Result<()> {
|
|
let PreparedRuntime { layers, outputs } = prepared;
|
|
let mut retired_layers = RuntimeLayers::new();
|
|
let reload_result = guard.reload_handle.modify(|active_layers| {
|
|
retired_layers = std::mem::replace(active_layers, layers);
|
|
});
|
|
return match reload_result {
|
|
std::result::Result::Ok(()) => {
|
|
guard.retired_dropped_lines = guard.retired_dropped_lines.saturating_add(guard.outputs.dropped_lines());
|
|
guard.outputs.accumulate_file_dropped_lines(&mut guard.retired_file_dropped_lines);
|
|
let retired_outputs = std::mem::replace(&mut guard.outputs, outputs);
|
|
guard.settings = settings.clone();
|
|
drop(retired_layers);
|
|
drop(retired_outputs);
|
|
std::result::Result::Ok(())
|
|
},
|
|
std::result::Result::Err(error) => std::result::Result::Err(
|
|
ksp_core_lib::Error::new(crate::ERROR_CODE_RELOAD_FAILED, "unable to reload the KSP logging runtime").with_source(error),
|
|
),
|
|
};
|
|
});
|
|
}
|
|
|
|
fn prepare_runtime_with_identity(
|
|
settings: &crate::LoggingSettings,
|
|
runtime_identity: std::option::Option<&crate::LoggingRuntimeIdentity>,
|
|
) -> 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);
|
|
}
|
|
let enabled_console = settings.console().filter(|console| -> bool {
|
|
return console.enabled();
|
|
});
|
|
let enabled_files = settings.files().iter().filter(|file| -> bool {
|
|
return file.enabled();
|
|
});
|
|
let mut output_layers = RuntimeLayers::new();
|
|
let mut outputs = RuntimeOutputs::default();
|
|
if let std::option::Option::Some(console) = enabled_console {
|
|
let prepared_console = build_console_output(console, settings);
|
|
output_layers.push(prepared_console.layer);
|
|
outputs.console = std::option::Option::Some(prepared_console.output);
|
|
}
|
|
for file in enabled_files {
|
|
let prepared_file_result = build_file_output(file, settings, runtime_identity);
|
|
let prepared_file = match prepared_file_result {
|
|
std::result::Result::Ok(output) => output,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
output_layers.push(prepared_file.layer);
|
|
outputs.files.push(RuntimeFileOutput { metadata: prepared_file.metadata, output: prepared_file.output });
|
|
}
|
|
if output_layers.is_empty() {
|
|
return std::result::Result::Ok(PreparedRuntime { layers: RuntimeLayers::new(), outputs });
|
|
}
|
|
output_layers.insert(0, crate::domain::DomainContextLayer::new().boxed());
|
|
let takeover_layer = build_target_filter(settings).and_then(output_layers).boxed();
|
|
return std::result::Result::Ok(PreparedRuntime { layers: vec![takeover_layer], outputs });
|
|
}
|
|
|
|
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.span_events(),
|
|
true,
|
|
console.ansi(),
|
|
console.format(),
|
|
console.filter(),
|
|
),
|
|
crate::ConsoleOutput::Stderr => build_non_blocking_output(
|
|
std::io::stderr(),
|
|
"ksp-logging-console",
|
|
settings.span_events(),
|
|
true,
|
|
console.ansi(),
|
|
console.format(),
|
|
console.filter(),
|
|
),
|
|
};
|
|
}
|
|
|
|
fn build_file_output(
|
|
file: &crate::FileSettings,
|
|
settings: &crate::LoggingSettings,
|
|
runtime_identity: std::option::Option<&crate::LoggingRuntimeIdentity>,
|
|
) -> ksp_core_lib::Result<PreparedFileOutput> {
|
|
let file_name_prefix = match runtime_identity {
|
|
std::option::Option::Some(identity) => identity.file_name_prefix(file.file_name_prefix()),
|
|
std::option::Option::None => file.file_name_prefix().to_owned(),
|
|
};
|
|
let appender_result = tracing_appender::rolling::RollingFileAppender::builder()
|
|
.rotation(map_file_rotation(file.rotation()))
|
|
.filename_prefix(file_name_prefix.as_str())
|
|
.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("output_id", file.output_id())
|
|
.with_context("directory", file.directory().display().to_string())
|
|
.with_context("file_name_prefix", file_name_prefix.as_str())
|
|
.with_source(error),
|
|
);
|
|
},
|
|
};
|
|
let stripped_writer = crate::writer::StripAnsiWriter::new(appender);
|
|
let thread_name = format!("ksp-logging-{}", file.output_id());
|
|
let prepared = build_non_blocking_output(stripped_writer, thread_name.as_str(), settings.span_events(), false, false, file.format(), file.filter());
|
|
let metadata = crate::RuntimeFileMetadata {
|
|
output_id: file.output_id().to_owned(),
|
|
directory: file.directory().to_path_buf(),
|
|
file_name_prefix,
|
|
rotation: file.rotation(),
|
|
};
|
|
return std::result::Result::Ok(PreparedFileOutput { metadata, layer: prepared.layer, output: prepared.output });
|
|
}
|
|
|
|
fn build_non_blocking_output<W>(
|
|
writer: W,
|
|
thread_name: &str,
|
|
span_events: crate::SpanEvents,
|
|
ansi_sanitization: bool,
|
|
ansi: bool,
|
|
format: crate::LogFormat,
|
|
filter: &crate::OutputFilter,
|
|
) -> PreparedOutput
|
|
where
|
|
W: std::io::Write + std::marker::Send + 'static,
|
|
{
|
|
let (non_blocking, worker_guard) = non_blocking_builder(thread_name).finish(writer);
|
|
let error_counter = non_blocking.error_counter();
|
|
let layer = build_format_layer(non_blocking, span_events, ansi_sanitization, ansi, format, filter);
|
|
return PreparedOutput { layer, output: RuntimeOutput { _worker_guard: worker_guard, error_counter } };
|
|
}
|
|
|
|
fn non_blocking_builder(thread_name: &str) -> tracing_appender::non_blocking::NonBlockingBuilder {
|
|
return tracing_appender::non_blocking::NonBlockingBuilder::default().lossy(true).thread_name(thread_name);
|
|
}
|
|
|
|
fn build_format_layer(
|
|
writer: tracing_appender::non_blocking::NonBlocking,
|
|
span_events: crate::SpanEvents,
|
|
ansi_sanitization: bool,
|
|
ansi: bool,
|
|
format: crate::LogFormat,
|
|
filter: &crate::OutputFilter,
|
|
) -> BoxedRuntimeLayer {
|
|
let span_events = map_span_events(span_events);
|
|
return match format {
|
|
crate::LogFormat::Human => tracing_subscriber::fmt::layer()
|
|
.with_writer(crate::writer::RouteMakeWriter::new(writer, filter.clone()))
|
|
.with_ansi(ansi)
|
|
.with_ansi_sanitization(ansi_sanitization)
|
|
.with_target(true)
|
|
.with_file(true)
|
|
.with_line_number(true)
|
|
.with_span_events(span_events)
|
|
.boxed(),
|
|
crate::LogFormat::Compact => tracing_subscriber::fmt::layer()
|
|
.compact()
|
|
.with_writer(crate::writer::RouteMakeWriter::new(writer, filter.clone()))
|
|
.with_ansi(ansi)
|
|
.with_ansi_sanitization(ansi_sanitization)
|
|
.with_target(true)
|
|
.with_file(true)
|
|
.with_line_number(true)
|
|
.with_span_events(span_events)
|
|
.boxed(),
|
|
crate::LogFormat::Pretty => tracing_subscriber::fmt::layer()
|
|
.pretty()
|
|
.with_writer(crate::writer::RouteMakeWriter::new(writer, filter.clone()))
|
|
.with_ansi(ansi)
|
|
.with_ansi_sanitization(ansi_sanitization)
|
|
.with_target(true)
|
|
.with_file(true)
|
|
.with_line_number(true)
|
|
.with_span_events(span_events)
|
|
.boxed(),
|
|
crate::LogFormat::Json => tracing_subscriber::fmt::layer()
|
|
.json()
|
|
.with_writer(crate::writer::RouteMakeWriter::new(writer, filter.clone()))
|
|
.with_ansi(false)
|
|
.with_target(true)
|
|
.with_file(true)
|
|
.with_line_number(true)
|
|
.with_span_events(span_events)
|
|
.boxed(),
|
|
};
|
|
}
|
|
|
|
fn build_target_filter(settings: &crate::LoggingSettings) -> tracing_subscriber::filter::Targets {
|
|
let mut filter = tracing_subscriber::filter::Targets::new()
|
|
.with_default(tracing_subscriber::filter::LevelFilter::OFF)
|
|
.with_target("ksp-", map_filter_level(settings.default_filter()));
|
|
for target_filter in settings.target_filters() {
|
|
filter = filter.with_target(target_filter.target_prefix(), map_filter_level(target_filter.level()));
|
|
}
|
|
return filter;
|
|
}
|
|
|
|
const fn map_filter_level(level: crate::LogFilterLevel) -> tracing_subscriber::filter::LevelFilter {
|
|
return match level {
|
|
crate::LogFilterLevel::Off => tracing_subscriber::filter::LevelFilter::OFF,
|
|
crate::LogFilterLevel::Error => tracing_subscriber::filter::LevelFilter::ERROR,
|
|
crate::LogFilterLevel::Warn => tracing_subscriber::filter::LevelFilter::WARN,
|
|
crate::LogFilterLevel::Info => tracing_subscriber::filter::LevelFilter::INFO,
|
|
crate::LogFilterLevel::Debug => tracing_subscriber::filter::LevelFilter::DEBUG,
|
|
crate::LogFilterLevel::Trace => tracing_subscriber::filter::LevelFilter::TRACE,
|
|
};
|
|
}
|
|
|
|
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,
|
|
crate::SpanEvents::NewAndClose => tracing_subscriber::fmt::format::FmtSpan::NEW | tracing_subscriber::fmt::format::FmtSpan::CLOSE,
|
|
crate::SpanEvents::Full => tracing_subscriber::fmt::format::FmtSpan::FULL,
|
|
};
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "../unit_tests/runtime.rs"]
|
|
mod tests;
|