// file: crates/ksp-config-lib/src/logging.rs // version: 2 /// Effective standard Logging configuration resolved from Config and mapped to the Logging runtime contract. #[derive(Clone, Eq, PartialEq)] pub struct ResolvedLoggingConfig { file_id: crate::ConfigFileId, source_path: std::path::PathBuf, profile_id: String, selection_source: crate::ConfigProfileSelectionSource, effective: crate::ResolvedConfigJson, logs_directory: std::path::PathBuf, settings: ksp_logging_lib::LoggingSettings, } impl ResolvedLoggingConfig { /// Returns the logical Config document identifier used by this runtime configuration. #[must_use] pub const fn file_id(&self) -> &crate::ConfigFileId { return &self.file_id; } /// Returns the physical source Config document path. #[must_use] pub fn source_path(&self) -> &std::path::Path { return self.source_path.as_path(); } /// Returns the selected standard Logging profile identifier. #[must_use] pub fn profile_id(&self) -> &str { return self.profile_id.as_str(); } /// Returns the source that selected the standard Logging profile. #[must_use] pub const fn selection_source(&self) -> crate::ConfigProfileSelectionSource { return self.selection_source; } /// Returns the detailed environment-resolved effective Config view. /// /// The real tree is available to legitimate runtime consumers and the safe tree is suitable for ordinary diagnostics. #[must_use] pub const fn effective(&self) -> &crate::ResolvedConfigJson { return &self.effective; } /// Returns the validated real Logging root directory. /// /// Relative Config values are anchored to the process current working directory when this adapter runs. Absolute Config values are preserved. #[must_use] pub fn logs_directory(&self) -> &std::path::Path { return self.logs_directory.as_path(); } /// Returns the mapped runtime Logging settings. #[must_use] pub const fn settings(&self) -> &ksp_logging_lib::LoggingSettings { return &self.settings; } /// Consumes this resolved Config and returns the mapped runtime Logging settings. #[must_use] pub fn into_settings(self) -> ksp_logging_lib::LoggingSettings { return self.settings; } } impl std::fmt::Debug for ResolvedLoggingConfig { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { return formatter .debug_struct("ResolvedLoggingConfig") .field("file_id", &self.file_id) .field("source_path", &self.source_path) .field("profile_id", &self.profile_id) .field("selection_source", &self.selection_source) .field("effective", &self.effective) .finish_non_exhaustive(); } } impl crate::ConfigDocumentEngine { /// Loads the standard Logging document, selects a profile, resolves environment placeholders and maps the effective result to `LoggingSettings`. /// /// `requested_profile = None` uses the document `default_profile`; `Some(profile_id)` requests an explicit profile. Source JSON validation remains distinct /// from effective runtime validation: an environment value that resolves to an invalid Logging setting returns /// [`crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID`] and does not silently fall back to the placeholder fallback. pub fn load_resolved_logging_config( &self, requested_profile: std::option::Option<&str>, environment: &crate::ConfigEnvironment, ) -> ksp_core_lib::Result { let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING); let file_id = match file_id { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let profile = self.load_resolved_profile(&file_id, requested_profile); let profile = match profile { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return resolve_logging_profile(&profile, environment); } /// Maps an already resolved standard Logging profile to the runtime Logging adapter while preserving its selection provenance. /// /// This entry point is intended for profiles selected by a composite. The profile must reference `cfg.std.logging`. pub fn resolve_logging_config_profile( &self, profile: &crate::ResolvedConfigProfile, environment: &crate::ConfigEnvironment, ) -> ksp_core_lib::Result { if profile.file_id().as_str() != crate::FILE_ID_STD_LOGGING { return std::result::Result::Err(effective_error(profile, "resolved Config profile does not reference the standard Logging document")); } let descriptor = self.registry().descriptor(profile.file_id()); if let std::result::Result::Err(error) = descriptor { return std::result::Result::Err(error); } return resolve_logging_profile(profile, environment); } } #[derive(serde::Deserialize)] #[serde(deny_unknown_fields)] struct EffectiveLoggingSource { format_version: u32, logs_directory: String, profile_id: String, default_filter: String, span_events: String, console: EffectiveConsoleSource, files: std::vec::Vec, target_filters: std::vec::Vec, } #[derive(serde::Deserialize)] #[serde(deny_unknown_fields)] struct EffectiveConsoleSource { enabled: bool, output: String, ansi: bool, format: String, filter: EffectiveOutputFilterSource, } #[derive(serde::Deserialize)] #[serde(deny_unknown_fields)] struct EffectiveFileSource { output_id: String, enabled: bool, path: String, rotation: String, format: String, ansi: bool, filter: EffectiveOutputFilterSource, } #[derive(serde::Deserialize)] #[serde(deny_unknown_fields)] struct EffectiveOutputFilterSource { level: String, targets: std::vec::Vec, domains: std::vec::Vec, } #[derive(serde::Deserialize)] #[serde(deny_unknown_fields)] struct EffectiveTargetFilterSource { target_prefix: String, level: String, } fn resolve_logging_profile(profile: &crate::ResolvedConfigProfile, environment: &crate::ConfigEnvironment) -> ksp_core_lib::Result { let effective = profile.resolve_effective_environment_detailed(environment); let effective = match effective { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let sensitivity_validation = validate_logging_sensitivity(profile, &effective); if let std::result::Result::Err(error) = sensitivity_validation { return std::result::Result::Err(error); } let source = serde_json::from_value::(effective.value().clone()); let source = match source { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err( effective_error(profile, "effective Logging Config cannot be decoded into the runtime adapter contract").with_source(error), ); }, }; if source.format_version != 1 { return std::result::Result::Err(effective_error(profile, "effective Logging format_version is unsupported")); } if source.profile_id != profile.profile_id() { return std::result::Result::Err(effective_error(profile, "effective Logging profile_id differs from the selected source profile")); } let safe_logs_directory = safe_string_at(effective.safe_value(), "/logs_directory"); let logs_directory = resolve_logs_directory(source.logs_directory.as_str(), safe_logs_directory.as_str(), profile); let logs_directory = match logs_directory { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let default_filter = map_level(source.default_filter.as_str(), "default_filter", profile); let default_filter = match default_filter { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let span_events = map_span_events(source.span_events.as_str(), profile); let span_events = match span_events { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let console = map_console(source.console, profile); let console = match console { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let files = map_files(source.files, logs_directory.as_path(), profile); let files = match files { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let mut settings = ksp_logging_lib::LoggingSettings::new(default_filter, span_events, std::option::Option::Some(console), files); for target_filter in source.target_filters { let level = map_level(target_filter.level.as_str(), "target_filters.level", profile); let level = match level { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; settings = settings.with_target_filter(ksp_logging_lib::TargetFilter::new(target_filter.target_prefix, level)); } let validation = settings.validate(); if let std::result::Result::Err(error) = validation { return std::result::Result::Err( effective_error(profile, "effective Logging settings fail the Logging runtime contract") .with_context("logging_error_domain", error.code().domain()) .with_context("logging_error_code", error.code().code()), ); } return std::result::Result::Ok(ResolvedLoggingConfig { file_id: profile.file_id().clone(), source_path: profile.path().to_path_buf(), profile_id: profile.profile_id().to_owned(), selection_source: profile.selection_source(), effective, logs_directory, settings, }); } fn validate_logging_sensitivity(profile: &crate::ResolvedConfigProfile, effective: &crate::ResolvedConfigJson) -> ksp_core_lib::Result<()> { if effective.sensitivity().is_secret() { return std::result::Result::Err(effective_error(profile, "standard Logging configuration must not consume Secret environment values")); } return std::result::Result::Ok(()); } fn map_console(source: EffectiveConsoleSource, profile: &crate::ResolvedConfigProfile) -> ksp_core_lib::Result { let output = match source.output.as_str() { "stdout" => ksp_logging_lib::ConsoleOutput::Stdout, "stderr" => ksp_logging_lib::ConsoleOutput::Stderr, _ => return std::result::Result::Err(effective_field_error(profile, "console.output", "effective Logging console output is unsupported")), }; let format = map_format(source.format.as_str(), "console.format", profile); let format = match format { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let filter = map_output_filter(source.filter, "console.filter", profile); let filter = match filter { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(ksp_logging_lib::ConsoleSettings::new(source.enabled, output, source.ansi, format, filter)); } fn map_files( sources: std::vec::Vec, logs_directory: &std::path::Path, profile: &crate::ResolvedConfigProfile, ) -> ksp_core_lib::Result> { let mut files = std::vec::Vec::::with_capacity(sources.len()); for source in sources { if !relative_file_path_is_valid(source.path.as_str()) { return std::result::Result::Err( effective_field_error(profile, "files.path", "effective Logging file path must stay relative to logs_directory without traversal") .with_context("output_id", source.output_id.as_str()), ); } let file_path = std::path::Path::new(source.path.as_str()); let file_name = match file_path.file_name().and_then(std::ffi::OsStr::to_str) { std::option::Option::Some(value) if !value.is_empty() => value.to_owned(), _ => return std::result::Result::Err(effective_field_error(profile, "files.path", "effective Logging file path has no UTF-8 file name")), }; let relative_directory = match file_path.parent() { std::option::Option::Some(value) => value, std::option::Option::None => std::path::Path::new(""), }; let directory = logs_directory.join(relative_directory); let rotation = map_rotation(source.rotation.as_str(), profile); let rotation = match rotation { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let format = map_format(source.format.as_str(), "files.format", profile); let format = match format { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let filter = map_output_filter(source.filter, "files.filter", profile); let filter = match filter { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let file = ksp_logging_lib::FileSettings::new(source.output_id, source.enabled, directory, file_name, rotation, format, filter).with_ansi(source.ansi); files.push(file); } return std::result::Result::Ok(files); } fn map_output_filter( source: EffectiveOutputFilterSource, field: &'static str, profile: &crate::ResolvedConfigProfile, ) -> ksp_core_lib::Result { let level = map_level(source.level.as_str(), field, profile); let level = match level { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(ksp_logging_lib::OutputFilter::new(level, source.targets, source.domains)); } fn map_level(value: &str, field: &'static str, profile: &crate::ResolvedConfigProfile) -> ksp_core_lib::Result { return match value { "off" => std::result::Result::Ok(ksp_logging_lib::LogFilterLevel::Off), "error" => std::result::Result::Ok(ksp_logging_lib::LogFilterLevel::Error), "warn" => std::result::Result::Ok(ksp_logging_lib::LogFilterLevel::Warn), "info" => std::result::Result::Ok(ksp_logging_lib::LogFilterLevel::Info), "debug" => std::result::Result::Ok(ksp_logging_lib::LogFilterLevel::Debug), "trace" => std::result::Result::Ok(ksp_logging_lib::LogFilterLevel::Trace), _ => std::result::Result::Err(effective_field_error(profile, field, "effective Logging level is unsupported")), }; } fn map_span_events(value: &str, profile: &crate::ResolvedConfigProfile) -> ksp_core_lib::Result { return match value { "off" => std::result::Result::Ok(ksp_logging_lib::SpanEvents::Off), "new_and_close" => std::result::Result::Ok(ksp_logging_lib::SpanEvents::NewAndClose), "full" => std::result::Result::Ok(ksp_logging_lib::SpanEvents::Full), _ => std::result::Result::Err(effective_field_error(profile, "span_events", "effective Logging span_events value is unsupported")), }; } fn map_format(value: &str, field: &'static str, profile: &crate::ResolvedConfigProfile) -> ksp_core_lib::Result { return match value { "human" => std::result::Result::Ok(ksp_logging_lib::LogFormat::Human), "compact" => std::result::Result::Ok(ksp_logging_lib::LogFormat::Compact), "pretty" => std::result::Result::Ok(ksp_logging_lib::LogFormat::Pretty), "json" => std::result::Result::Ok(ksp_logging_lib::LogFormat::Json), _ => std::result::Result::Err(effective_field_error(profile, field, "effective Logging format is unsupported")), }; } fn map_rotation(value: &str, profile: &crate::ResolvedConfigProfile) -> ksp_core_lib::Result { return match value { "never" => std::result::Result::Ok(ksp_logging_lib::FileRotation::Never), "hourly" => std::result::Result::Ok(ksp_logging_lib::FileRotation::Hourly), "daily" => std::result::Result::Ok(ksp_logging_lib::FileRotation::Daily), _ => std::result::Result::Err(effective_field_error(profile, "files.rotation", "effective Logging rotation is unsupported")), }; } fn resolve_logs_directory(value: &str, safe_value: &str, profile: &crate::ResolvedConfigProfile) -> ksp_core_lib::Result { if value.trim().is_empty() { return std::result::Result::Err( effective_field_error(profile, "logs_directory", "effective Logging logs_directory must not be empty").with_context("safe_value", safe_value), ); } let configured = std::path::PathBuf::from(value); let resolved = if configured.is_absolute() { configured } else { let current_directory = std::env::current_dir(); let current_directory = match current_directory { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err( effective_field_error(profile, "logs_directory", "process current working directory cannot be resolved").with_source(error), ); }, }; current_directory.join(configured) }; let metadata = std::fs::metadata(resolved.as_path()); match metadata { std::result::Result::Ok(value) if !value.is_dir() => { return std::result::Result::Err( effective_field_error(profile, "logs_directory", "effective Logging logs_directory resolves to an existing non-directory path") .with_context("safe_value", safe_value), ); }, std::result::Result::Ok(_) => {}, std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}, std::result::Result::Err(error) => { return std::result::Result::Err( effective_field_error(profile, "logs_directory", "effective Logging logs_directory cannot be inspected") .with_context("safe_value", safe_value) .with_source(error), ); }, } return std::result::Result::Ok(resolved); } fn relative_file_path_is_valid(value: &str) -> bool { let path = std::path::Path::new(value); if path.is_absolute() { return false; } let mut has_normal_component = false; for component in path.components() { match component { std::path::Component::Normal(_) => has_normal_component = true, std::path::Component::CurDir | std::path::Component::ParentDir | std::path::Component::RootDir | std::path::Component::Prefix(_) => return false, } } return has_normal_component; } fn safe_string_at(value: &serde_json::Value, pointer: &str) -> String { return match value.pointer(pointer).and_then(serde_json::Value::as_str) { std::option::Option::Some(value) => value.to_owned(), std::option::Option::None => "".to_owned(), }; } fn effective_field_error(profile: &crate::ResolvedConfigProfile, field: &'static str, reason: &'static str) -> ksp_core_lib::Error { return effective_error(profile, reason).with_context("field", field); } fn effective_error(profile: &crate::ResolvedConfigProfile, reason: &'static str) -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID, "effective Config cannot be mapped to the requested runtime contract") .with_context("file_id", profile.file_id().as_str()) .with_context("profile_id", profile.profile_id()) .with_context("reason", reason); } #[cfg(test)] #[path = "../unit_tests/logging.rs"] mod tests;