// file: crates/ksp-logging-lib/src/runtime.rs // version: 16 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, outputs: RuntimeOutputs, retired_dropped_lines: crate::DroppedLines, retired_file_dropped_lines: std::collections::HashMap, } 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 { 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 { 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 + std::marker::Send + std::marker::Sync + 'static>; type RuntimeLayers = std::vec::Vec; type RuntimeReloadHandle = tracing_subscriber::reload::Handle; struct PreparedRuntime { layers: RuntimeLayers, outputs: RuntimeOutputs, } #[derive(Default)] struct RuntimeOutputs { console: std::option::Option, files: std::vec::Vec, } 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 { 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) { 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 { 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 { return initialize_runtime(settings, std::option::Option::Some(identity.clone())); } /// 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 initialize_runtime( settings: &crate::LoggingSettings, runtime_identity: std::option::Option, ) -> ksp_core_lib::Result { return prepare_runtime_with_identity(settings, runtime_identity.as_ref()).and_then(|prepared| -> ksp_core_lib::Result { 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), ), }; }); } fn prepare_runtime_with_identity( settings: &crate::LoggingSettings, runtime_identity: std::option::Option<&crate::LoggingRuntimeIdentity>, ) -> ksp_core_lib::Result { 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::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 { 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::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( 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::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::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::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::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;