456 lines
20 KiB
Rust
456 lines
20 KiB
Rust
// 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<ResolvedLoggingConfig> {
|
|
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<ResolvedLoggingConfig> {
|
|
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<EffectiveFileSource>,
|
|
target_filters: std::vec::Vec<EffectiveTargetFilterSource>,
|
|
}
|
|
|
|
#[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<String>,
|
|
domains: std::vec::Vec<String>,
|
|
}
|
|
|
|
#[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<ResolvedLoggingConfig> {
|
|
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::<EffectiveLoggingSource>(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<ksp_logging_lib::ConsoleSettings> {
|
|
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<EffectiveFileSource>,
|
|
logs_directory: &std::path::Path,
|
|
profile: &crate::ResolvedConfigProfile,
|
|
) -> ksp_core_lib::Result<std::vec::Vec<ksp_logging_lib::FileSettings>> {
|
|
let mut files = std::vec::Vec::<ksp_logging_lib::FileSettings>::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<ksp_logging_lib::OutputFilter> {
|
|
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<ksp_logging_lib::LogFilterLevel> {
|
|
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<ksp_logging_lib::SpanEvents> {
|
|
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<ksp_logging_lib::LogFormat> {
|
|
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<ksp_logging_lib::FileRotation> {
|
|
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<std::path::PathBuf> {
|
|
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 => "<unavailable>".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;
|