0.5.1-pre.007

This commit is contained in:
2026-08-10 09:28:57 +02:00
parent 34a670eaec
commit a2820062eb
81 changed files with 3357 additions and 2381 deletions

View File

@@ -1,8 +1,15 @@
<!-- file: ks-logging/CHANGELOG.md -->
<!-- version: 15 -->
<!-- version: 18 -->
# CHANGELOG — ks-logging
## `0.5.1-pre.007`
- place `logs_directory` hors profils dans le document logging et le rend configurable via `${KS_LOGS_DIRECTORY:-logs}` ;
- remplace le sélecteur autonome `active_profile` par `default_profile` et résout les chemins relatifs des targets fichier sous la racine globale ;
- corrige le test de résolution des profils logging afin quil itère par référence et ne déplace pas `document.profiles` avant de réutiliser le document complet.
- corrige le test de routage canonique afin quil fixe explicitement la valeur de repli `logs` de `KS_LOGS_DIRECTORY` avant de vérifier les chemins résolus, sans modifier le runtime.
## `0.5.1-pre.006`
- ajoute la sélection explicite d'un profil logging nommé afin qu'une composition binaire puisse choisir un profil indépendamment du `active_profile` propre au document logging ;

View File

@@ -1,5 +1,5 @@
<!-- file: ks-logging/README.md -->
<!-- version: 7 -->
<!-- version: 8 -->
# ks-logging
@@ -7,39 +7,21 @@
## Responsabilités
- charger `logging.config.json` et résoudre ses placeholders denvironnement ;
- charger `logging.config.json` et résoudre ses placeholders d'environnement ;
- valider `config/schemas/logging.config.schema.json` ;
- sélectionner un profil logging indépendamment du profil applicatif ;
- posséder `LoggingConfig`, `LogTargetConfig` et `LogTargetFilterConfig` ;
- sélectionner les routes activées ;
- appliquer niveaux, targets exactes et préfixes wildcard ;
- créer les writers non bloquants et gérer les formats/rotations ;
- conserver les `WorkerGuard` pendant toute la durée du processus.
- posséder `logs_directory` comme valeur globale indépendante des profils ;
- sélectionner un `default_profile` ou un profil explicitement choisi par une composition ;
- résoudre les chemins relatifs des targets fichier sous `logs_directory` ;
- initialiser les routes `tracing` sans exposer de secret.
## Relations
`logs_directory` utilise actuellement `${KS_LOGS_DIRECTORY:-logs}`. Une installation peut donc déplacer la racine des logs par environnement ou `.env` sans recopier le chemin dans chaque profil logging.
`ks-logging` dépend de `ks-config` uniquement pour le chargement du fichier `.env` et la résolution générique `${KS_*}` / `${KB_*}`. `ks-config` ne dépend pas de `ks-logging` et ne duplique aucun type logging.
## Frontière
Le desktop lit le chemin `logging` et le `logging_profile` depuis `config/kb-app-demo-desktop.default.config.json`, charge le document logging puis transmet le profil explicitement sélectionné à `init_logging`.
## API publique
- `read_logging_json_file_with_environment` charge le document logging ;
- `parse_logging_json` et `validate_logging_json_schema` valident le format ;
- `active_logging_profile` sélectionne le profil autonome actif et `logging_profile` résout un profil explicitement nommé par une composition ;
- `LoggingConfigDocument` et `LoggingProfileConfig` décrivent le document ;
- `LoggingConfig`, `LogTargetConfig` et `LogTargetFilterConfig` décrivent le runtime ;
- `init_logging` initialise le subscriber global ;
- `LoggingGuard` maintient les writers ;
- `tracing_target` retourne le target canonique de la crate.
## Statut
Le split config/logging est fonctionnel. Les profils logging sont indépendants des profils applicatifs et les routes existantes restent conservées dans `config/logging.config.json`.
Les types logging restent possédés par `ks-logging`, pas par `ks-config`. Une composition de binaire ne recopie aucune route : elle référence le document logging partagé et peut remplacer seulement le profil sélectionné.
## Documents
- [Utilisation](USAGE.md)
- [Travaux restants](TODO.md)
- [Historique](CHANGELOG.md)
- [Configuration locale](../config/README.md)
- [Guide logging](../docs/guides/LOGGING.md)

View File

@@ -1,5 +1,5 @@
<!-- file: ks-logging/USAGE.md -->
<!-- version: 6 -->
<!-- version: 7 -->
# Utilisation de ks-logging
@@ -13,114 +13,48 @@ let document = match ks_logging::read_logging_json_file_with_environment(
Ok(value) => value,
Err(error) => return Err(error),
};
let config = match ks_logging::active_logging_profile(&document) {
Ok(value) => value,
Err(error) => return Err(error),
};
let guard = match ks_logging::init_logging(config) {
let config = match ks_logging::default_logging_profile(&document) {
Ok(value) => value,
Err(error) => return Err(error),
};
```
Le desktop utilise normalement le chemin déclaré dans sa composition `config/kb-app-demo-desktop.default.config.json`. `KS_LOGGING_CONFIG_PATH` peut encore remplacer explicitement ce chemin sans modifier le fichier de composition.
## Format du document
Le document possède :
```json
{
"active_profile": "local_devnet",
"profiles": [
{
"name": "local_devnet",
"default_level": "info",
"targets": [],
"target_filters": []
}
]
"logs_directory": "${KS_LOGS_DIRECTORY:-logs}",
"default_profile": "local_devnet",
"profiles": []
}
```
Le schéma formel est `config/schemas/logging.config.schema.json`. `config/example.logging.config.json` fournit un exemple minimal conforme avec des routes valides.
`logs_directory` n'appartient à aucun profil. Les paths relatifs des targets fichier sont résolus sous cette racine. Un path absolu reste inchangé.
`targets` doit contenir au moins une route et au moins une route doit être activée.
## Override par composition
## Sélection indépendante
Le `active_profile` logging reste le défaut autonome du document. Lorsquun binaire utilise une composition, il sélectionne explicitement son `logging_profile` avec `ks_logging::logging_profile`; cette sélection prime pour ce binaire sans modifier le document partagé.
## Construction directe
Les tests ou consommateurs spécialisés peuvent toujours construire directement `LoggingConfig` puis appeler `init_logging` :
Une application peut sélectionner un autre profil avec :
```rust
let config = ks_logging::LoggingConfig {
default_level: "info".to_string(),
targets: vec![ks_logging::LogTargetConfig {
name: "console".to_string(),
enabled: true,
sink: "console".to_string(),
level: "info".to_string(),
path: String::new(),
rotation: "none".to_string(),
format: "compact".to_string(),
ansi: true,
targets: vec!["ks-*".to_string()],
}],
target_filters: Vec::new(),
};
let guard = match ks_logging::init_logging(&config) {
let config = match ks_logging::logging_profile(&document, "mainnet_research") {
Ok(value) => value,
Err(error) => return Err(error),
};
```
## Route fichier JSON
Le `default_profile` reste le fallback autonome du document ; la sélection explicite d'une composition prime uniquement pour le consommateur concerné.
Le chemin du document peut être remplacé opérationnellement par `KS_LOGGING_CONFIG_PATH`. La racine des fichiers peut être remplacée indépendamment par `KS_LOGS_DIRECTORY`.
## Initialisation
Une fois le profil résolu :
```rust
let file_route = ks_logging::LogTargetConfig {
name: "operations-json".to_string(),
enabled: true,
sink: "file".to_string(),
level: "debug".to_string(),
path: "logs/operations.jsonl".to_string(),
rotation: "daily".to_string(),
format: "json".to_string(),
ansi: false,
targets: vec!["ks-pipeline*".to_string()],
};
```
Les rotations supportées sont `none`, `never`, `daily` et `hourly`. Les formats supportés sont `human`, `compact`, `pretty` et `json`.
## Inspection des routes
```rust
for route_name in guard.route_names() {
println!("active logging route: {route_name}");
match ks_logging::init_logging(&config) {
Ok(()) => (),
Err(error) => return Err(error),
}
assert_eq!(guard.route_count(), guard.route_names().len());
```
## Erreurs
Les erreurs utilisent `ks_core::Error`. Les cas principaux sont :
- document ou schéma invalide ;
- profil actif absent ou dupliqué ;
- aucune route activée ;
- sink, format, rotation ou niveau inconnu ;
- chemin fichier invalide ;
- échec dinitialisation du subscriber global.
## Tests instructifs
- les tests de `document` vérifient les deux fichiers logging versionnés et les profils indépendants ;
- `default_logging_config_routes_global_and_operational_crate_files` vérifie les routes canoniques ;
- les tests de `tracing_runtime` vérifient linstallation, les filtres et le volume de routes.
## Limites
- initialisation globale unique par processus ;
- la politique de redaction des valeurs secrètes avant logging est renforcée dans `0.5.1-pre.006`.
L'initialisation reste globale au processus. Les binaires doivent donc résoudre leur configuration avant le premier appel à `init_logging`.

View File

@@ -1,5 +1,5 @@
// file: ks-logging/src/document.rs
// version: 2
// version: 5
//! Independent logging configuration document loading and validation.
@@ -8,8 +8,10 @@ const LOGGING_JSON_SCHEMA: &str = include_str!("../../config/schemas/logging.con
/// Root logging document containing independently selectable named profiles.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
pub struct LoggingConfigDocument {
/// Active logging profile name.
pub active_profile: std::string::String,
/// Root directory for file logs, independent from profile selection.
pub logs_directory: std::string::String,
/// Default logging profile used when a composition does not override it.
pub default_profile: std::string::String,
/// Named logging profiles available in this document.
pub profiles: std::vec::Vec<LoggingProfileConfig>,
}
@@ -132,14 +134,44 @@ pub fn read_logging_json_file_with_environment(
return parse_logging_json(&resolved);
}
/// Returns one named runtime logging profile.
pub fn logging_profile<'a>(
/// Returns one named runtime logging profile with file paths resolved below the global log root.
pub fn logging_profile(
document: &LoggingConfigDocument,
profile_name: &str,
) -> ks_core::Result<crate::LoggingConfig> {
let source = match source_logging_profile(document, profile_name) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut config = source.config.clone();
let root = std::path::PathBuf::from(document.logs_directory.as_str());
for target in &mut config.targets {
if target.sink != "file" || target.path.trim().is_empty() {
continue;
}
let configured = std::path::PathBuf::from(target.path.as_str());
if configured.is_absolute() {
continue;
}
target.path = root.join(configured).display().to_string();
}
return std::result::Result::Ok(config);
}
/// Returns the default runtime logging profile selected by the logging document.
pub fn default_logging_profile(
document: &LoggingConfigDocument,
) -> ks_core::Result<crate::LoggingConfig> {
return logging_profile(document, document.default_profile.as_str());
}
fn source_logging_profile<'a>(
document: &'a LoggingConfigDocument,
profile_name: &str,
) -> ks_core::Result<&'a crate::LoggingConfig> {
) -> ks_core::Result<&'a LoggingProfileConfig> {
for profile in &document.profiles {
if profile.name == profile_name {
return std::result::Result::Ok(&profile.config);
return std::result::Result::Ok(profile);
}
}
return std::result::Result::Err(ks_core::Error::new(
@@ -148,16 +180,13 @@ pub fn logging_profile<'a>(
));
}
/// Returns the active runtime logging profile selected by the logging document.
pub fn active_logging_profile(
document: &LoggingConfigDocument,
) -> ks_core::Result<&crate::LoggingConfig> {
return logging_profile(document, document.active_profile.as_str());
}
/// Validates a typed logging configuration document.
pub fn validate_logging_document(document: &LoggingConfigDocument) -> ks_core::Result<()> {
match require_non_empty(&document.active_profile, "active_profile") {
match require_non_empty(&document.logs_directory, "logging.logs_directory") {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
match require_non_empty(&document.default_profile, "logging.default_profile") {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
@@ -168,7 +197,6 @@ pub fn validate_logging_document(document: &LoggingConfigDocument) -> ks_core::R
));
}
let mut names = std::collections::BTreeSet::<std::string::String>::new();
let mut active_count = 0_u32;
for profile in &document.profiles {
match require_non_empty(&profile.name, "logging.profile.name") {
std::result::Result::Ok(()) => (),
@@ -180,24 +208,15 @@ pub fn validate_logging_document(document: &LoggingConfigDocument) -> ks_core::R
profile.name.clone(),
));
}
if profile.name == document.active_profile {
active_count += 1;
}
match validate_logging_config(&profile.config) {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
}
if active_count != 1 {
return std::result::Result::Err(ks_core::Error::new(
"active_logging_profile_count_invalid",
format!(
"active logging profile '{}' must match exactly one profile",
document.active_profile
),
));
}
return std::result::Result::Ok(());
return match default_logging_profile(document) {
std::result::Result::Ok(_) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
fn validate_logging_config(config: &crate::LoggingConfig) -> ks_core::Result<()> {
@@ -416,28 +435,35 @@ mod tests {
}
#[test]
fn logging_profiles_are_independent_and_active_profile_resolves() {
fn logging_profiles_are_independent_and_default_profile_resolves() {
let document_result = super::parse_logging_json(DEFAULT_LOGGING_CONFIG);
let document = match document_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("default logging config must parse: {error}"),
};
assert_eq!(document.active_profile, "mainnet_research");
assert_eq!(document.default_profile, "local_devnet");
assert_eq!(document.profiles.len(), 3);
assert!(super::active_logging_profile(&document).is_ok());
assert!(super::default_logging_profile(&document).is_ok());
assert!(super::logging_profile(&document, "local_devnet").is_ok());
}
#[test]
fn default_logging_config_routes_global_and_operational_crate_files() {
let document_result = super::parse_logging_json(DEFAULT_LOGGING_CONFIG);
let mut value = parse_default_value();
value["logs_directory"] = serde_json::Value::String("logs".to_string());
let raw_json = value_to_json(&value);
let document_result = super::parse_logging_json(&raw_json);
let document = match document_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("default logging config must parse: {error}"),
};
let operational_crates = tracing_crate_names();
assert!(!operational_crates.is_empty());
for profile in document.profiles {
for profile in &document.profiles {
let resolved = match super::logging_profile(&document, profile.name.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("logging profile must resolve: {error}"),
};
let directory = match profile.name.as_str() {
"local_devnet" => "devnet",
"mainnet_research" => "mainnet_research",
@@ -450,7 +476,7 @@ mod tests {
("error.jsonl", "error", "json"),
] {
let expected_path = format!("logs/{directory}/{suffix}");
assert!(profile.config.targets.iter().any(|target| {
assert!(resolved.targets.iter().any(|target| {
return target.enabled
&& target.path == expected_path
&& target.level == level
@@ -458,7 +484,7 @@ mod tests {
&& target.targets == std::vec!["*".to_string()];
}));
}
assert!(profile.config.targets.iter().any(|target| {
assert!(resolved.targets.iter().any(|target| {
return target.enabled
&& target.path == format!("logs/{directory}/app.log")
&& target.level == "debug"
@@ -477,7 +503,7 @@ mod tests {
crate_name.as_str()
};
let expected_path = format!("logs/{directory}/{route_directory}/{suffix}");
let route_exists = profile.config.targets.iter().any(|target| {
let route_exists = resolved.targets.iter().any(|target| {
return target.enabled
&& target.path == expected_path
&& target.level == level
@@ -500,16 +526,19 @@ mod tests {
assert!(!DEFAULT_LOGGING_CONFIG.contains("\"ks_wallet\""));
assert!(DEFAULT_LOGGING_CONFIG.contains("\"ks-wallet\""));
assert!(DEFAULT_LOGGING_CONFIG.contains("\"ks-pipeline\""));
assert!(DEFAULT_LOGGING_CONFIG.contains("logs/devnet/ks-pipeline/debug.log"));
assert!(DEFAULT_LOGGING_CONFIG.contains("logs/mainnet_research/ks-pipeline/info.log"));
assert!(DEFAULT_LOGGING_CONFIG.contains("logs/mainnet/ks-pipeline/error.jsonl"));
assert!(
DEFAULT_LOGGING_CONFIG.contains("\"logs_directory\": \"${KS_LOGS_DIRECTORY:-logs}\"")
);
assert!(DEFAULT_LOGGING_CONFIG.contains("devnet/ks-pipeline/debug.log"));
assert!(DEFAULT_LOGGING_CONFIG.contains("mainnet_research/ks-pipeline/info.log"));
assert!(DEFAULT_LOGGING_CONFIG.contains("mainnet/ks-pipeline/error.jsonl"));
assert!(!DEFAULT_LOGGING_CONFIG.contains("ks_pipeline"));
}
#[test]
fn logging_parser_rejects_missing_active_profile() {
fn logging_parser_rejects_missing_default_profile() {
let mut value = parse_default_value();
value["active_profile"] = serde_json::Value::String("missing_profile".to_string());
value["default_profile"] = serde_json::Value::String("missing_profile".to_string());
let result = super::parse_logging_json(&value_to_json(&value));
assert!(result.is_err());
}

View File

@@ -1,5 +1,5 @@
// file: ks-logging/src/lib.rs
// version: 9
// version: 10
//! Logging and tracing initialization for applications and workers.
#![warn(missing_docs)]
@@ -26,8 +26,8 @@ pub use self::config::LoggingConfig;
pub use self::document::LoggingConfigDocument;
/// Exposes one named logging profile.
pub use self::document::LoggingProfileConfig;
/// Exposes active logging profile resolution.
pub use self::document::active_logging_profile;
/// Exposes default logging profile resolution.
pub use self::document::default_logging_profile;
/// Exposes the embedded logging JSON Schema text.
pub use self::document::logging_json_schema_text;
/// Exposes the embedded logging JSON Schema parser.