370 lines
16 KiB
Rust
370 lines
16 KiB
Rust
// file: crates/ksp-app-config-desk/src/logging_editor.rs
|
|
// version: 6
|
|
|
|
//! Typed Logging document projection and validated persistence for the Config Desk Logging editor.
|
|
|
|
use ts_rs::TS; // rust-rules: trait-import
|
|
|
|
/// One Logging sink selector/filter exposed to the editor.
|
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/logging/LoggingOutputFilterDto.ts")]
|
|
pub(crate) struct LoggingOutputFilterDto {
|
|
/// Filter level source text.
|
|
pub(crate) level: String,
|
|
/// Target selectors in source order.
|
|
pub(crate) targets: std::vec::Vec<String>,
|
|
/// Structured domain selectors in source order.
|
|
pub(crate) domains: std::vec::Vec<String>,
|
|
}
|
|
|
|
/// Console sink configuration exposed to the editor.
|
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/logging/LoggingConsoleDto.ts")]
|
|
pub(crate) struct LoggingConsoleDto {
|
|
/// Whether the console sink is enabled.
|
|
pub(crate) enabled: bool,
|
|
/// `stdout` or `stderr` source text.
|
|
pub(crate) output: String,
|
|
/// Whether ANSI output is requested.
|
|
pub(crate) ansi: bool,
|
|
/// Console format source text.
|
|
pub(crate) format: String,
|
|
/// Sink-local selectors.
|
|
pub(crate) filter: crate::LoggingOutputFilterDto,
|
|
}
|
|
|
|
/// One persistent file sink exposed to the editor.
|
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/logging/LoggingFileDto.ts")]
|
|
pub(crate) struct LoggingFileDto {
|
|
/// Stable sink identifier.
|
|
pub(crate) output_id: String,
|
|
/// Whether the file sink is enabled.
|
|
pub(crate) enabled: bool,
|
|
/// Source path relative to the Logging root.
|
|
pub(crate) path: String,
|
|
/// Rotation source text.
|
|
pub(crate) rotation: String,
|
|
/// File format source text.
|
|
pub(crate) format: String,
|
|
/// Whether ANSI output is requested.
|
|
pub(crate) ansi: bool,
|
|
/// Sink-local selectors.
|
|
pub(crate) filter: crate::LoggingOutputFilterDto,
|
|
}
|
|
|
|
/// One global Logging target override exposed to the editor.
|
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/logging/LoggingTargetFilterDto.ts")]
|
|
pub(crate) struct LoggingTargetFilterDto {
|
|
/// KSP target prefix.
|
|
pub(crate) target_prefix: String,
|
|
/// Override level source text.
|
|
pub(crate) level: String,
|
|
}
|
|
|
|
/// One typed Logging profile exposed to the editor.
|
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/logging/LoggingProfileDto.ts")]
|
|
pub(crate) struct LoggingProfileDto {
|
|
/// Stable profile identifier.
|
|
pub(crate) profile_id: String,
|
|
/// Profile-level default filter source text.
|
|
pub(crate) default_filter: String,
|
|
/// Span lifecycle source text.
|
|
pub(crate) span_events: String,
|
|
/// Console sink configuration.
|
|
pub(crate) console: crate::LoggingConsoleDto,
|
|
/// Persistent file sinks in source order.
|
|
pub(crate) files: std::vec::Vec<crate::LoggingFileDto>,
|
|
/// Global target overrides in source order.
|
|
pub(crate) target_filters: std::vec::Vec<crate::LoggingTargetFilterDto>,
|
|
}
|
|
|
|
/// Complete typed Logging document exposed to the editor.
|
|
#[derive(Clone, Debug, serde::Serialize, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/logging/LoggingDocumentDto.ts")]
|
|
pub(crate) struct LoggingDocumentDto {
|
|
/// Stable Config file identifier.
|
|
pub(crate) file_id: String,
|
|
/// Physical path resolved by Config.
|
|
pub(crate) path: String,
|
|
/// Source format version.
|
|
pub(crate) format_version: u32,
|
|
/// Source Logging root expression/path.
|
|
pub(crate) logs_directory: String,
|
|
/// Autonomous default profile identifier.
|
|
pub(crate) default_profile: String,
|
|
/// Typed profiles in source order.
|
|
pub(crate) profiles: std::vec::Vec<crate::LoggingProfileDto>,
|
|
}
|
|
|
|
/// Complete editable Logging candidate accepted from the frontend.
|
|
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/logging/LoggingDocumentCandidateDto.ts")]
|
|
pub(crate) struct LoggingDocumentCandidateDto {
|
|
/// Source Logging root expression/path.
|
|
pub(crate) logs_directory: String,
|
|
/// Autonomous default profile identifier.
|
|
pub(crate) default_profile: String,
|
|
/// Typed profiles in source order.
|
|
pub(crate) profiles: std::vec::Vec<crate::LoggingProfileDto>,
|
|
}
|
|
|
|
/// Result of one validated typed Logging persistence operation.
|
|
#[derive(Clone, Debug, serde::Serialize, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/logging/LoggingDocumentSaveResultDto.ts")]
|
|
pub(crate) struct LoggingDocumentSaveResultDto {
|
|
/// Whether `std.logging.json` bytes changed.
|
|
pub(crate) source_changed: bool,
|
|
/// Whether Config consumers must reload the document.
|
|
pub(crate) reload_required: bool,
|
|
/// Whether the active KSP Logging runtime was hot-reloaded from the persisted candidate.
|
|
pub(crate) runtime_applied: bool,
|
|
/// Runtime generation after the successful hot reload.
|
|
pub(crate) logging_generation: u32,
|
|
/// Profile selected and applied to the runtime.
|
|
pub(crate) active_profile: String,
|
|
/// Fresh typed document loaded after successful persistence and runtime application.
|
|
pub(crate) document: LoggingDocumentDto,
|
|
}
|
|
|
|
/// Loads the typed Logging source through ConfigManagement and projects it for the frontend.
|
|
pub(crate) fn logging_document(state: &crate::AppState) -> ksp_core_lib::Result<LoggingDocumentDto> {
|
|
return document_from_management(state.config_management());
|
|
}
|
|
|
|
/// Validates, atomically persists and hot-reloads one typed Logging candidate through ConfigManagement and `ksp-logging-lib`.
|
|
pub(crate) fn save_logging_document(state: &crate::AppState, candidate: LoggingDocumentCandidateDto) -> ksp_core_lib::Result<LoggingDocumentSaveResultDto> {
|
|
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::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 previous = state.config_management().read_source(&file_id);
|
|
let previous = match previous {
|
|
std::result::Result::Ok(value) => value.content().to_owned(),
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let source = config_candidate(candidate);
|
|
let report = state.config_management().save_logging_document(&source);
|
|
let report = match report {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let applied = apply_persisted_runtime(state);
|
|
let (active_profile, logging_generation) = match applied {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
return rollback_after_runtime_failure(state.config_management(), &file_id, previous.as_str(), report.source_changed(), error);
|
|
},
|
|
};
|
|
let document = document_from_management(state.config_management());
|
|
let document = match document {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
ksp_logging_lib::debug!(
|
|
target: crate::TRACING_TARGET,
|
|
domain = crate::TRACING_DOMAIN_LOGGING_EDITOR,
|
|
source_changed = report.source_changed(),
|
|
reload_required = report.reload_required(),
|
|
runtime_applied = true,
|
|
logging_generation,
|
|
active_profile = active_profile.as_str(),
|
|
profile_count = document.profiles.len(),
|
|
default_profile = document.default_profile.as_str(),
|
|
"typed Logging document persistence and runtime hot reload completed"
|
|
);
|
|
return std::result::Result::Ok(LoggingDocumentSaveResultDto {
|
|
source_changed: report.source_changed(),
|
|
reload_required: report.reload_required(),
|
|
runtime_applied: true,
|
|
logging_generation,
|
|
active_profile,
|
|
document,
|
|
});
|
|
}
|
|
|
|
fn apply_persisted_runtime(state: &crate::AppState) -> ksp_core_lib::Result<(String, u32)> {
|
|
let environment = ksp_config_lib::ConfigEnvironment::load();
|
|
let environment = match environment {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let resolved = state.config_management().engine().load_resolved_logging_config(std::option::Option::None, &environment);
|
|
let resolved = match resolved {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let profile_id = resolved.profile_id().to_owned();
|
|
let settings = resolved.into_settings();
|
|
let generation = state.reinitialize_logging_runtime(profile_id.as_str(), "default_profile", &settings);
|
|
return match generation {
|
|
std::result::Result::Ok(value) => std::result::Result::Ok((profile_id, value)),
|
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
|
};
|
|
}
|
|
|
|
fn rollback_after_runtime_failure(
|
|
management: &ksp_config_lib::ConfigManagement,
|
|
file_id: &ksp_config_lib::ConfigFileId,
|
|
previous_source: &str,
|
|
source_changed: bool,
|
|
runtime_error: ksp_core_lib::Error,
|
|
) -> ksp_core_lib::Result<LoggingDocumentSaveResultDto> {
|
|
if !source_changed {
|
|
return std::result::Result::Err(runtime_error);
|
|
}
|
|
let rollback = management.save_source_candidate(file_id, previous_source);
|
|
return match rollback {
|
|
std::result::Result::Ok(_) => std::result::Result::Err(runtime_error),
|
|
std::result::Result::Err(rollback_error) => std::result::Result::Err(
|
|
ksp_core_lib::Error::new(
|
|
crate::ERROR_CODE_LOGGING_SOURCE_ROLLBACK_FAILED,
|
|
"Logging runtime hot reload failed and the previous persisted source could not be restored",
|
|
)
|
|
.with_context("runtime_error_domain", runtime_error.code().domain())
|
|
.with_context("runtime_error_code", runtime_error.code().code())
|
|
.with_source(rollback_error),
|
|
),
|
|
};
|
|
}
|
|
|
|
fn document_from_management(management: &ksp_config_lib::ConfigManagement) -> ksp_core_lib::Result<LoggingDocumentDto> {
|
|
let source = management.load_logging_document();
|
|
let source = match source {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::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 path = management.engine().registry().resolve_path(management.engine().bootstrap(), &file_id);
|
|
let path = match path {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let mut profiles = std::vec::Vec::<crate::LoggingProfileDto>::with_capacity(source.profiles().len());
|
|
let mut file_count = 0usize;
|
|
let mut target_filter_count = 0usize;
|
|
for profile in source.profiles() {
|
|
file_count += profile.files().len();
|
|
target_filter_count += profile.target_filters().len();
|
|
profiles.push(project_profile(profile));
|
|
}
|
|
let result = LoggingDocumentDto {
|
|
file_id: file_id.as_str().to_owned(),
|
|
path: path.display().to_string(),
|
|
format_version: source.format_version(),
|
|
logs_directory: source.logs_directory().to_owned(),
|
|
default_profile: source.default_profile().to_owned(),
|
|
profiles,
|
|
};
|
|
ksp_logging_lib::debug!(
|
|
target: crate::TRACING_TARGET,
|
|
domain = crate::TRACING_DOMAIN_LOGGING_EDITOR,
|
|
file_id = result.file_id.as_str(),
|
|
profile_count = result.profiles.len(),
|
|
file_count,
|
|
target_filter_count,
|
|
"typed Logging document projected for editor"
|
|
);
|
|
return std::result::Result::Ok(result);
|
|
}
|
|
|
|
fn config_candidate(candidate: LoggingDocumentCandidateDto) -> ksp_config_lib::LoggingConfigDocument {
|
|
let mut profiles = std::vec::Vec::<ksp_config_lib::LoggingProfileConfig>::with_capacity(candidate.profiles.len());
|
|
for profile in candidate.profiles {
|
|
profiles.push(config_profile(profile));
|
|
}
|
|
return ksp_config_lib::LoggingConfigDocument::new(candidate.logs_directory, candidate.default_profile, profiles);
|
|
}
|
|
|
|
fn config_profile(profile: crate::LoggingProfileDto) -> ksp_config_lib::LoggingProfileConfig {
|
|
let mut files = std::vec::Vec::<ksp_config_lib::LoggingFileConfig>::with_capacity(profile.files.len());
|
|
for file in profile.files {
|
|
let mut config =
|
|
ksp_config_lib::LoggingFileConfig::new(file.output_id, file.enabled, file.path, file.rotation, file.format, config_output_filter(file.filter));
|
|
config.set_ansi(file.ansi);
|
|
files.push(config);
|
|
}
|
|
let mut target_filters = std::vec::Vec::<ksp_config_lib::LoggingTargetFilterConfig>::with_capacity(profile.target_filters.len());
|
|
for target_filter in profile.target_filters {
|
|
target_filters.push(ksp_config_lib::LoggingTargetFilterConfig::new(target_filter.target_prefix, target_filter.level));
|
|
}
|
|
return ksp_config_lib::LoggingProfileConfig::new(
|
|
profile.profile_id,
|
|
profile.default_filter,
|
|
profile.span_events,
|
|
ksp_config_lib::LoggingConsoleConfig::new(
|
|
profile.console.enabled,
|
|
profile.console.output,
|
|
profile.console.ansi,
|
|
profile.console.format,
|
|
config_output_filter(profile.console.filter),
|
|
),
|
|
files,
|
|
target_filters,
|
|
);
|
|
}
|
|
|
|
fn config_output_filter(filter: crate::LoggingOutputFilterDto) -> ksp_config_lib::LoggingOutputFilterConfig {
|
|
return ksp_config_lib::LoggingOutputFilterConfig::new(filter.level, filter.targets, filter.domains);
|
|
}
|
|
|
|
fn project_profile(profile: &ksp_config_lib::LoggingProfileConfig) -> crate::LoggingProfileDto {
|
|
let mut files = std::vec::Vec::<crate::LoggingFileDto>::with_capacity(profile.files().len());
|
|
for file in profile.files() {
|
|
files.push(crate::LoggingFileDto {
|
|
output_id: file.output_id().to_owned(),
|
|
enabled: file.enabled(),
|
|
path: file.path().to_owned(),
|
|
rotation: file.rotation().to_owned(),
|
|
format: file.format().to_owned(),
|
|
ansi: file.ansi(),
|
|
filter: project_output_filter(file.filter()),
|
|
});
|
|
}
|
|
let mut target_filters = std::vec::Vec::<crate::LoggingTargetFilterDto>::with_capacity(profile.target_filters().len());
|
|
for target_filter in profile.target_filters() {
|
|
target_filters.push(crate::LoggingTargetFilterDto { target_prefix: target_filter.target_prefix().to_owned(), level: target_filter.level().to_owned() });
|
|
}
|
|
return crate::LoggingProfileDto {
|
|
profile_id: profile.profile_id().to_owned(),
|
|
default_filter: profile.default_filter().to_owned(),
|
|
span_events: profile.span_events().to_owned(),
|
|
console: crate::LoggingConsoleDto {
|
|
enabled: profile.console().enabled(),
|
|
output: profile.console().output().to_owned(),
|
|
ansi: profile.console().ansi(),
|
|
format: profile.console().format().to_owned(),
|
|
filter: project_output_filter(profile.console().filter()),
|
|
},
|
|
files,
|
|
target_filters,
|
|
};
|
|
}
|
|
|
|
fn project_output_filter(filter: &ksp_config_lib::LoggingOutputFilterConfig) -> crate::LoggingOutputFilterDto {
|
|
return crate::LoggingOutputFilterDto {
|
|
level: filter.level().to_owned(),
|
|
targets: filter.targets().to_vec(),
|
|
domains: filter.domains().to_vec(),
|
|
};
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "../unit_tests/logging_editor.rs"]
|
|
mod tests;
|