0.5.1-pre.007
This commit is contained in:
@@ -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 qu’il 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 qu’il 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 ;
|
||||
|
||||
@@ -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 d’environnement ;
|
||||
- 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)
|
||||
|
||||
@@ -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. Lorsqu’un 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 d’initialisation 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 l’installation, 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`.
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user