0.5.1-pre.005
This commit is contained in:
@@ -1,8 +1,16 @@
|
||||
<!-- file: ks-logging/CHANGELOG.md -->
|
||||
<!-- version: 13 -->
|
||||
<!-- version: 14 -->
|
||||
|
||||
# CHANGELOG — ks-logging
|
||||
|
||||
## `0.5.1-pre.005`
|
||||
|
||||
- devient propriétaire unique des types `LoggingConfig`, `LogTargetConfig` et `LogTargetFilterConfig` ;
|
||||
- ajoute `LoggingConfigDocument`, les profils logging indépendants et leur chargement avec résolution d’environnement ;
|
||||
- embarque et valide `config/schemas/logging.config.schema.json` ;
|
||||
- charge `config/logging.config.json` par défaut via le desktop et supprime la conversion champ par champ depuis `ks-config` ;
|
||||
- ajoute les tests de schéma, profil actif et routes canoniques du document logging.
|
||||
|
||||
## `0.5.1-pre.003`
|
||||
|
||||
- aligne les routes de référence et la documentation de logging sur les identités `ks-lib-decoder.*`, `ks-lib-executor.*` et `ks-lib-materializer.*` ;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# file: ks-logging/Cargo.toml
|
||||
# version: 3
|
||||
# version: 4
|
||||
|
||||
[package]
|
||||
name = "ks-logging"
|
||||
@@ -9,7 +9,11 @@ license.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
jsonschema.workspace = true
|
||||
ks-config = { path = "../ks-config" }
|
||||
ks-core = { path = "../ks-core" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-appender.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
||||
@@ -1,38 +1,45 @@
|
||||
<!-- file: ks-logging/README.md -->
|
||||
<!-- version: 5 -->
|
||||
<!-- version: 6 -->
|
||||
|
||||
# ks-logging
|
||||
|
||||
`ks-logging` initialise le subscriber global `tracing` à partir d’une configuration de routes explicites vers la console ou des fichiers.
|
||||
`ks-logging` possède le contrat logging indépendant du workspace et initialise le subscriber global `tracing` à partir du profil logging sélectionné.
|
||||
|
||||
## Responsabilités
|
||||
|
||||
- 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 ;
|
||||
- gérer les formats human, compact, pretty et JSON ;
|
||||
- gérer les rotations de fichiers supportées ;
|
||||
- créer les writers non bloquants et gérer les formats/rotations ;
|
||||
- conserver les `WorkerGuard` pendant toute la durée du processus.
|
||||
|
||||
## Hors périmètre
|
||||
## Relations
|
||||
|
||||
La crate ne charge pas actuellement le fichier de configuration et ne définit pas les targets des autres crates. Le cadrage `0.5.0-pre.002` confirme qu’un document logging séparé sera introduit en `0.5.1`; la propriété exacte du DTO source sera fixée sans faire dépendre la configuration générale du runtime tracing.
|
||||
`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.
|
||||
|
||||
Le desktop charge `config/app.config.json` et `config/logging.config.json` séparément puis transmet directement le profil logging actif à `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 actif ;
|
||||
- `LoggingConfigDocument` et `LoggingProfileConfig` décrivent le document ;
|
||||
- `LoggingConfig`, `LogTargetConfig` et `LogTargetFilterConfig` décrivent le runtime ;
|
||||
- `init_logging` initialise le subscriber global ;
|
||||
- `LoggingConfig`, `LogTargetConfig` et `LogTargetFilterConfig` décrivent les routes ;
|
||||
- `LoggingGuard` maintient les writers et expose les routes installées ;
|
||||
- `tracing_target` retourne le target canonique de la crate ;
|
||||
- `LogFileRoute` reste disponible pour les anciens appelants file-only.
|
||||
- `LoggingGuard` maintient les writers ;
|
||||
- `tracing_target` retourne le target canonique de la crate.
|
||||
|
||||
## Statut
|
||||
|
||||
L’initialisation multi-routes est fonctionnelle. Elle échoue explicitement si aucune route n’est activée ou si le subscriber global a déjà été initialisé.
|
||||
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`.
|
||||
|
||||
## Documents
|
||||
|
||||
- [Utilisation](USAGE.md)
|
||||
- [Travaux restants](TODO.md)
|
||||
- [Historique](CHANGELOG.md)
|
||||
- [Guide de configuration](../ks-config/USAGE.md)
|
||||
- [Configuration locale](../config/README.md)
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
<!-- file: ks-logging/TODO.md -->
|
||||
<!-- version: 5 -->
|
||||
<!-- version: 6 -->
|
||||
|
||||
# TODO — ks-logging
|
||||
|
||||
## Série `0.5.x`
|
||||
|
||||
- [ ] `0.5.1` - consommer un document logging et un schéma distincts de la configuration généraliste.
|
||||
- [ ] `0.5.1` - définir avec `ks-config` un propriétaire unique du DTO source logging sans coupler `ks-config` au runtime tracing.
|
||||
- [ ] `0.5.1` - supprimer la conversion champ par champ actuellement portée par le desktop.
|
||||
- [ ] Compatibilité - préserver targets, niveaux, filtres, formats, routes et sémantique wildcard pendant la migration.
|
||||
- [ ] `0.5.1` - intégrer la propagation de sensibilité et le camouflage des valeurs provenant de `KS_SECRET_*` / `KB_SECRET_*` avant tout diagnostic ou log.
|
||||
- [ ] Compatibilité - préserver targets, niveaux, filtres, formats, routes et sémantique wildcard pendant les prochaines migrations.
|
||||
- [ ] Contrat - réévaluer `LogFileRoute`, actuellement sans consommateur actif hors de la crate.
|
||||
- [ ] Tests - ajouter les tests d’API externe et de migration du futur format logging.
|
||||
- [ ] Documentation - mettre à jour les exemples après implémentation du nouveau chargement.
|
||||
- [ ] Tests - ajouter les canaris garantissant qu’un secret résolu n’atteint jamais une sortie logging.
|
||||
- [ ] Documentation - maintenir le schéma et les exemples logging alignés avec les types possédés par la crate.
|
||||
|
||||
@@ -1,9 +1,57 @@
|
||||
<!-- file: ks-logging/USAGE.md -->
|
||||
<!-- version: 4 -->
|
||||
<!-- version: 5 -->
|
||||
|
||||
# Utilisation de ks-logging
|
||||
|
||||
## Initialisation
|
||||
## Chargement recommandé
|
||||
|
||||
```rust
|
||||
let document = match ks_logging::read_logging_json_file_with_environment(
|
||||
std::path::Path::new("config/logging.config.json"),
|
||||
std::path::Path::new("."),
|
||||
) {
|
||||
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) {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
```
|
||||
|
||||
Le desktop utilise `config/logging.config.json` par défaut. `KS_LOGGING_CONFIG_PATH` peut sélectionner un autre document sans modifier `KS_CONFIG_PATH` ni le profil applicatif.
|
||||
|
||||
## Format du document
|
||||
|
||||
```json
|
||||
{
|
||||
"active_profile": "local_devnet",
|
||||
"profiles": [
|
||||
{
|
||||
"name": "local_devnet",
|
||||
"default_level": "info",
|
||||
"targets": [],
|
||||
"target_filters": []
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
`targets` doit contenir au moins une route et au moins une route doit être activée.
|
||||
|
||||
## Sélection indépendante
|
||||
|
||||
Le `active_profile` logging n’est jamais déduit de `app.config.json`. Un opérateur peut donc utiliser, par exemple, un profil applicatif `mainnet_research` avec un profil logging `local_devnet` ou tout autre profil logging explicitement défini.
|
||||
|
||||
## Construction directe
|
||||
|
||||
Les tests ou consommateurs spécialisés peuvent toujours construire directement `LoggingConfig` puis appeler `init_logging` :
|
||||
|
||||
```rust
|
||||
let config = ks_logging::LoggingConfig {
|
||||
@@ -17,7 +65,7 @@ let config = ks_logging::LoggingConfig {
|
||||
rotation: "none".to_string(),
|
||||
format: "compact".to_string(),
|
||||
ansi: true,
|
||||
targets: vec!["kb-*".to_string()],
|
||||
targets: vec!["ks-*".to_string()],
|
||||
}],
|
||||
target_filters: Vec::new(),
|
||||
};
|
||||
@@ -27,16 +75,6 @@ let guard = match ks_logging::init_logging(&config) {
|
||||
};
|
||||
```
|
||||
|
||||
`init_logging` doit être appelée une seule fois par processus. Le `LoggingGuard` retourné doit rester vivant jusqu’à l’arrêt afin de ne pas interrompre les writers non bloquants.
|
||||
|
||||
## Inspection des routes
|
||||
|
||||
```rust
|
||||
assert_eq!(guard.route_count(), 1);
|
||||
assert_eq!(guard.guard_count(), 1);
|
||||
assert_eq!(guard.route_names(), &["console".to_string()]);
|
||||
```
|
||||
|
||||
## Route fichier JSON
|
||||
|
||||
```rust
|
||||
@@ -49,76 +87,13 @@ let file_route = ks_logging::LogTargetConfig {
|
||||
rotation: "daily".to_string(),
|
||||
format: "json".to_string(),
|
||||
ansi: false,
|
||||
targets: vec![
|
||||
"ks-pipeline*".to_string(),
|
||||
"ks-onchain-transport*".to_string(),
|
||||
],
|
||||
};
|
||||
|
||||
let config = ks_logging::LoggingConfig {
|
||||
default_level: "info".to_string(),
|
||||
targets: vec![file_route],
|
||||
target_filters: Vec::new(),
|
||||
};
|
||||
|
||||
let guard = match ks_logging::init_logging(&config) {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
targets: vec!["ks-pipeline*".to_string()],
|
||||
};
|
||||
```
|
||||
|
||||
Pour une route `file`, `path` doit désigner le fichier cible et `rotation` doit être une valeur supportée (`none`, `never`, `daily` ou `hourly`). Les formats supportés sont `human`, `compact`, `pretty` et `json`.
|
||||
Les rotations supportées sont `none`, `never`, `daily` et `hourly`. Les formats supportés sont `human`, `compact`, `pretty` et `json`.
|
||||
|
||||
## Filtres de targets
|
||||
|
||||
```rust
|
||||
let config = ks_logging::LoggingConfig {
|
||||
default_level: "warn".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!["kb-*".to_string()],
|
||||
}],
|
||||
target_filters: vec![
|
||||
ks_logging::LogTargetFilterConfig {
|
||||
target: "ks-pipeline".to_string(),
|
||||
level: "debug".to_string(),
|
||||
},
|
||||
ks_logging::LogTargetFilterConfig {
|
||||
target: "ks-store".to_string(),
|
||||
level: "info".to_string(),
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
Les routes acceptent des targets exactes et des préfixes wildcard. Les `target_filters` ajoutent des niveaux spécifiques aux routes wildcard. Une route d’erreur conserve la sémantique stricte prévue par l’implémentation.
|
||||
|
||||
## Émission d’événements après initialisation
|
||||
|
||||
```rust
|
||||
tracing::info!(
|
||||
target: ks_logging::tracing_target(),
|
||||
action = "startup",
|
||||
"logging runtime initialized"
|
||||
);
|
||||
|
||||
tracing::debug!(
|
||||
target: "ks-pipeline.demo",
|
||||
scenario = "token_2022",
|
||||
"scenario preparation started"
|
||||
);
|
||||
```
|
||||
|
||||
Les targets doivent suivre la nomenclature canonique du workspace pour que les routes et filtres restent prévisibles.
|
||||
|
||||
## Inspection des routes actives
|
||||
## Inspection des routes
|
||||
|
||||
```rust
|
||||
for route_name in guard.route_names() {
|
||||
@@ -132,6 +107,8 @@ assert_eq!(guard.route_count(), guard.route_names().len());
|
||||
|
||||
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 ;
|
||||
@@ -139,12 +116,11 @@ Les erreurs utilisent `ks_core::Error`. Les cas principaux sont :
|
||||
|
||||
## Tests instructifs
|
||||
|
||||
- `enabled_routes_keep_all_configured_outputs` vérifie l’installation multi-routes ;
|
||||
- `route_writer_filter_preserves_target_and_level_semantics` vérifie l’admission finale ;
|
||||
- `more_than_64_routes_compose_without_filtered_layer_ids` couvre un volume élevé de routes ;
|
||||
- `every_tracing_crate_exposes_canonical_targets` vérifie la cohérence des targets du workspace.
|
||||
- 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 ;
|
||||
- configuration fournie par l’appelant ;
|
||||
- la politique de redaction des valeurs secrètes avant logging est renforcée dans `0.5.1-pre.006`.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: ks-logging/src/config.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
//! Logging configuration data structures consumed by the tracing runtime.
|
||||
|
||||
@@ -15,7 +15,7 @@ pub struct LogFileRoute {
|
||||
}
|
||||
|
||||
/// One target-level filter shared by routes that include the wildcard target.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
|
||||
pub struct LogTargetFilterConfig {
|
||||
/// Tracing target or crate prefix.
|
||||
pub target: std::string::String,
|
||||
@@ -24,7 +24,7 @@ pub struct LogTargetFilterConfig {
|
||||
}
|
||||
|
||||
/// One logging output route.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
|
||||
pub struct LogTargetConfig {
|
||||
/// Route name used for diagnostics.
|
||||
pub name: std::string::String,
|
||||
@@ -46,8 +46,8 @@ pub struct LogTargetConfig {
|
||||
pub targets: std::vec::Vec<std::string::String>,
|
||||
}
|
||||
|
||||
/// Logging configuration consumed by `ks-logging` before profile binding exists.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
/// Logging configuration consumed by `ks-logging` for one named profile.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
|
||||
pub struct LoggingConfig {
|
||||
/// Default log level used when a route does not provide target directives.
|
||||
pub default_level: std::string::String,
|
||||
|
||||
519
ks-logging/src/document.rs
Normal file
519
ks-logging/src/document.rs
Normal file
@@ -0,0 +1,519 @@
|
||||
// file: ks-logging/src/document.rs
|
||||
// version: 1
|
||||
|
||||
//! Independent logging configuration document loading and validation.
|
||||
|
||||
const LOGGING_JSON_SCHEMA: &str = include_str!("../../config/schemas/logging.config.schema.json");
|
||||
|
||||
/// 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,
|
||||
/// Named logging profiles available in this document.
|
||||
pub profiles: std::vec::Vec<LoggingProfileConfig>,
|
||||
}
|
||||
|
||||
/// Named logging profile wrapping the runtime logging configuration.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
|
||||
pub struct LoggingProfileConfig {
|
||||
/// Profile code selected independently from the application profile.
|
||||
pub name: std::string::String,
|
||||
/// Runtime logging configuration owned by `ks-logging`.
|
||||
#[serde(flatten)]
|
||||
pub config: crate::LoggingConfig,
|
||||
}
|
||||
|
||||
/// Returns the embedded JSON Schema text used for logging configuration validation.
|
||||
pub fn logging_json_schema_text() -> &'static str {
|
||||
return LOGGING_JSON_SCHEMA;
|
||||
}
|
||||
|
||||
/// Parses the embedded logging JSON Schema into a JSON value.
|
||||
pub fn logging_json_schema_value() -> ks_core::Result<serde_json::Value> {
|
||||
let schema_result = serde_json::from_str::<serde_json::Value>(LOGGING_JSON_SCHEMA);
|
||||
return match schema_result {
|
||||
std::result::Result::Ok(schema) => std::result::Result::Ok(schema),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new(
|
||||
"logging_config_schema_parse_failed",
|
||||
error.to_string(),
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
/// Validates raw logging JSON against the embedded logging schema.
|
||||
pub fn validate_logging_json_schema(raw_json: &str) -> ks_core::Result<()> {
|
||||
let schema = match logging_json_schema_value() {
|
||||
std::result::Result::Ok(schema) => schema,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let instance = match serde_json::from_str::<serde_json::Value>(raw_json) {
|
||||
std::result::Result::Ok(instance) => instance,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"logging_config_json_parse_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
let validator = match jsonschema::validator_for(&schema) {
|
||||
std::result::Result::Ok(validator) => validator,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"logging_config_schema_compile_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
let validation_result = validator.validate(&instance);
|
||||
return match validation_result {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new(
|
||||
"logging_config_schema_validation_failed",
|
||||
error.to_string(),
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
/// Parses and validates one independent logging configuration document.
|
||||
pub fn parse_logging_json(raw_json: &str) -> ks_core::Result<LoggingConfigDocument> {
|
||||
match validate_logging_json_schema(raw_json) {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
let document = match serde_json::from_str::<LoggingConfigDocument>(raw_json) {
|
||||
std::result::Result::Ok(document) => document,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"logging_config_json_decode_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
return match validate_logging_document(&document) {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(document),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
/// Reads and parses one logging configuration document from a filesystem path.
|
||||
pub fn read_logging_json_file(path: &std::path::Path) -> ks_core::Result<LoggingConfigDocument> {
|
||||
let raw_json = match std::fs::read_to_string(path) {
|
||||
std::result::Result::Ok(content) => content,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"logging_config_file_read_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
return parse_logging_json(&raw_json);
|
||||
}
|
||||
|
||||
/// Loads workspace environment values, resolves placeholders and parses logging configuration.
|
||||
pub fn read_logging_json_file_with_environment(
|
||||
path: &std::path::Path,
|
||||
workspace_root: &std::path::Path,
|
||||
) -> ks_core::Result<LoggingConfigDocument> {
|
||||
match ks_config::load_workspace_environment(workspace_root) {
|
||||
std::result::Result::Ok(_) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
let raw_json = match std::fs::read_to_string(path) {
|
||||
std::result::Result::Ok(content) => content,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"logging_config_file_read_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
let resolved = ks_config::resolve_environment_placeholders(&raw_json);
|
||||
return parse_logging_json(&resolved);
|
||||
}
|
||||
|
||||
/// Returns the active runtime logging profile selected by the logging document.
|
||||
pub fn active_logging_profile(
|
||||
document: &LoggingConfigDocument,
|
||||
) -> ks_core::Result<&crate::LoggingConfig> {
|
||||
for profile in &document.profiles {
|
||||
if profile.name == document.active_profile {
|
||||
return std::result::Result::Ok(&profile.config);
|
||||
}
|
||||
}
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"active_logging_profile_not_found",
|
||||
document.active_profile.clone(),
|
||||
));
|
||||
}
|
||||
|
||||
/// 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") {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
if document.profiles.is_empty() {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"logging_config_profiles_empty",
|
||||
"at least one logging profile is required",
|
||||
));
|
||||
}
|
||||
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(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
if !names.insert(profile.name.clone()) {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"logging_profile_name_duplicate",
|
||||
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(());
|
||||
}
|
||||
|
||||
fn validate_logging_config(config: &crate::LoggingConfig) -> ks_core::Result<()> {
|
||||
match validate_log_level(&config.default_level, "logging.default_level") {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
if config.targets.is_empty() {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"logging_targets_empty",
|
||||
"at least one logging target is required",
|
||||
));
|
||||
}
|
||||
let mut enabled_count = 0_u32;
|
||||
let mut names = std::collections::BTreeSet::<std::string::String>::new();
|
||||
for target in &config.targets {
|
||||
if target.enabled {
|
||||
enabled_count += 1;
|
||||
}
|
||||
if !names.insert(target.name.clone()) {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"logging_target_duplicate",
|
||||
target.name.clone(),
|
||||
));
|
||||
}
|
||||
match validate_log_target(target) {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
}
|
||||
for filter in &config.target_filters {
|
||||
match require_non_empty(&filter.target, "logging.target_filters.target") {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
match validate_log_level(&filter.level, "logging.target_filters.level") {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
}
|
||||
if enabled_count == 0 {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"logging_no_enabled_targets",
|
||||
"at least one logging target must be enabled",
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn validate_log_target(config: &crate::LogTargetConfig) -> ks_core::Result<()> {
|
||||
match require_non_empty(&config.name, "logging.targets.name") {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
match validate_log_level(&config.level, "logging.targets.level") {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
if config.sink != "console" && config.sink != "file" {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"logging_sink_invalid",
|
||||
config.sink.clone(),
|
||||
));
|
||||
}
|
||||
if config.rotation != "none"
|
||||
&& config.rotation != "never"
|
||||
&& config.rotation != "daily"
|
||||
&& config.rotation != "hourly"
|
||||
{
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"logging_rotation_invalid",
|
||||
config.rotation.clone(),
|
||||
));
|
||||
}
|
||||
if config.format != "human"
|
||||
&& config.format != "compact"
|
||||
&& config.format != "pretty"
|
||||
&& config.format != "json"
|
||||
{
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"logging_format_invalid",
|
||||
config.format.clone(),
|
||||
));
|
||||
}
|
||||
if config.sink == "file" && config.path.trim().is_empty() {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"logging_file_path_empty",
|
||||
config.name.clone(),
|
||||
));
|
||||
}
|
||||
if config.sink == "console" && !config.path.trim().is_empty() {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"logging_console_path_not_empty",
|
||||
config.name.clone(),
|
||||
));
|
||||
}
|
||||
for target in &config.targets {
|
||||
match require_non_empty(target, "logging.targets.targets") {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn validate_log_level(value: &str, field_name: &str) -> ks_core::Result<()> {
|
||||
if value == "trace"
|
||||
|| value == "debug"
|
||||
|| value == "info"
|
||||
|| value == "warn"
|
||||
|| value == "error"
|
||||
|| value == "off"
|
||||
{
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"log_level_invalid",
|
||||
format!("{field_name}: {value}"),
|
||||
));
|
||||
}
|
||||
|
||||
fn require_non_empty(value: &str, field_name: &str) -> ks_core::Result<()> {
|
||||
if value.trim().is_empty() {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"logging_config_field_empty",
|
||||
field_name.to_string(),
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
const DEFAULT_LOGGING_CONFIG: &str = include_str!("../../config/logging.config.json");
|
||||
const EXAMPLE_LOGGING_CONFIG: &str = include_str!("../../config/example.logging.config.json");
|
||||
|
||||
fn parse_default_value() -> serde_json::Value {
|
||||
let result = serde_json::from_str::<serde_json::Value>(DEFAULT_LOGGING_CONFIG);
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("default logging config must be JSON: {error}"),
|
||||
};
|
||||
}
|
||||
|
||||
fn value_to_json(value: &serde_json::Value) -> std::string::String {
|
||||
let result = serde_json::to_string(value);
|
||||
return match result {
|
||||
std::result::Result::Ok(raw_json) => raw_json,
|
||||
std::result::Result::Err(error) => panic!("test logging value must serialize: {error}"),
|
||||
};
|
||||
}
|
||||
|
||||
fn tracing_crate_names() -> std::vec::Vec<std::string::String> {
|
||||
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let workspace_root = match manifest_dir.parent() {
|
||||
std::option::Option::Some(path) => path,
|
||||
std::option::Option::None => panic!("workspace root must exist"),
|
||||
};
|
||||
let read_result = std::fs::read_dir(workspace_root);
|
||||
let entries = match read_result {
|
||||
std::result::Result::Ok(entries) => entries,
|
||||
std::result::Result::Err(error) => panic!("workspace must be readable: {error}"),
|
||||
};
|
||||
let mut names = std::vec::Vec::<std::string::String>::new();
|
||||
for entry_result in entries {
|
||||
let entry = match entry_result {
|
||||
std::result::Result::Ok(entry) => entry,
|
||||
std::result::Result::Err(error) => panic!("workspace entry must be readable: {error}"),
|
||||
};
|
||||
let cargo_path = entry.path().join("Cargo.toml");
|
||||
if !cargo_path.is_file() {
|
||||
continue;
|
||||
}
|
||||
let cargo_result = std::fs::read_to_string(&cargo_path);
|
||||
let cargo_toml = match cargo_result {
|
||||
std::result::Result::Ok(content) => content,
|
||||
std::result::Result::Err(error) => panic!("Cargo.toml must be readable: {error}"),
|
||||
};
|
||||
if !cargo_toml.lines().any(|line| return line.trim() == "tracing.workspace = true") {
|
||||
continue;
|
||||
}
|
||||
let mut in_package = false;
|
||||
for line in cargo_toml.lines() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed == "[package]" {
|
||||
in_package = true;
|
||||
continue;
|
||||
}
|
||||
if in_package && trimmed.starts_with('[') {
|
||||
break;
|
||||
}
|
||||
if in_package && trimmed.starts_with("name = ") {
|
||||
names.push(trimmed.trim_start_matches("name = ").trim_matches('"').to_string());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
names.sort();
|
||||
return names;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logging_schema_text_is_valid_json() {
|
||||
let result = super::logging_json_schema_value();
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_and_example_logging_configs_validate_against_schema() {
|
||||
assert!(super::validate_logging_json_schema(DEFAULT_LOGGING_CONFIG).is_ok());
|
||||
assert!(super::validate_logging_json_schema(EXAMPLE_LOGGING_CONFIG).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logging_profiles_are_independent_and_active_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.profiles.len(), 3);
|
||||
assert!(super::active_logging_profile(&document).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_logging_config_routes_global_and_operational_crate_files() {
|
||||
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}"),
|
||||
};
|
||||
let operational_crates = tracing_crate_names();
|
||||
assert!(!operational_crates.is_empty());
|
||||
for profile in document.profiles {
|
||||
let directory = match profile.name.as_str() {
|
||||
"local_devnet" => "devnet",
|
||||
"mainnet_research" => "mainnet_research",
|
||||
"mainnet" => "mainnet",
|
||||
value => panic!("unexpected logging profile: {value}"),
|
||||
};
|
||||
for (suffix, level, format) in [
|
||||
("debug.log", "debug", "human"),
|
||||
("info.log", "info", "human"),
|
||||
("error.jsonl", "error", "json"),
|
||||
] {
|
||||
let expected_path = format!("logs/{directory}/{suffix}");
|
||||
assert!(profile.config.targets.iter().any(|target| {
|
||||
return target.enabled
|
||||
&& target.path == expected_path
|
||||
&& target.level == level
|
||||
&& target.format == format
|
||||
&& target.targets == std::vec!["*".to_string()];
|
||||
}));
|
||||
}
|
||||
assert!(profile.config.targets.iter().any(|target| {
|
||||
return target.enabled
|
||||
&& target.path == format!("logs/{directory}/app.log")
|
||||
&& target.level == "debug"
|
||||
&& target.format == "human"
|
||||
&& target.targets == std::vec!["kb-app-demo-desktop".to_string()];
|
||||
}));
|
||||
for crate_name in &operational_crates {
|
||||
for (suffix, level, format) in [
|
||||
("debug.log", "debug", "human"),
|
||||
("info.log", "info", "human"),
|
||||
("error.jsonl", "error", "json"),
|
||||
] {
|
||||
let route_directory = if crate_name == "ks-pipeline-demo-scenarios" {
|
||||
"ks-pipeline"
|
||||
} else {
|
||||
crate_name.as_str()
|
||||
};
|
||||
let expected_path = format!("logs/{directory}/{route_directory}/{suffix}");
|
||||
let route_exists = profile.config.targets.iter().any(|target| {
|
||||
return target.enabled
|
||||
&& target.path == expected_path
|
||||
&& target.level == level
|
||||
&& target.format == format
|
||||
&& target.targets.iter().any(|value| return value == crate_name);
|
||||
});
|
||||
assert!(
|
||||
route_exists,
|
||||
"logging profile {} is missing the canonical {} route for {} at {}",
|
||||
profile.name, level, crate_name, expected_path
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_logging_config_uses_canonical_wallet_and_pipeline_routes() {
|
||||
assert!(!DEFAULT_LOGGING_CONFIG.contains("api-key=95e73621"));
|
||||
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("ks_pipeline"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logging_parser_rejects_missing_active_profile() {
|
||||
let mut value = parse_default_value();
|
||||
value["active_profile"] = serde_json::Value::String("missing_profile".to_string());
|
||||
let result = super::parse_logging_json(&value_to_json(&value));
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logging_parser_rejects_duplicate_profile_names() {
|
||||
let mut value = parse_default_value();
|
||||
value["profiles"][1]["name"] = serde_json::Value::String("local_devnet".to_string());
|
||||
let result = super::parse_logging_json(&value_to_json(&value));
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logging_parser_rejects_file_target_without_path() {
|
||||
let mut value = parse_default_value();
|
||||
value["profiles"][0]["targets"][1]["path"] = serde_json::Value::String("".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: 7
|
||||
// version: 8
|
||||
|
||||
//! Logging and tracing initialization for applications and workers.
|
||||
#![warn(missing_docs)]
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
mod config;
|
||||
mod constants;
|
||||
mod document;
|
||||
mod tracing_runtime;
|
||||
|
||||
/// Canonical tracing target for this crate.
|
||||
@@ -21,6 +22,26 @@ pub use self::config::LogTargetConfig;
|
||||
pub use self::config::LogTargetFilterConfig;
|
||||
/// Exposes the logging configuration consumed by this crate.
|
||||
pub use self::config::LoggingConfig;
|
||||
/// Exposes the root independent logging configuration document.
|
||||
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 the embedded logging JSON Schema text.
|
||||
pub use self::document::logging_json_schema_text;
|
||||
/// Exposes the embedded logging JSON Schema parser.
|
||||
pub use self::document::logging_json_schema_value;
|
||||
/// Exposes logging configuration parsing from JSON.
|
||||
pub use self::document::parse_logging_json;
|
||||
/// Exposes logging configuration loading from a filesystem path.
|
||||
pub use self::document::read_logging_json_file;
|
||||
/// Exposes logging loading with workspace environment resolution.
|
||||
pub use self::document::read_logging_json_file_with_environment;
|
||||
/// Exposes typed logging document validation.
|
||||
pub use self::document::validate_logging_document;
|
||||
/// Exposes logging JSON Schema validation.
|
||||
pub use self::document::validate_logging_json_schema;
|
||||
/// Exposes the guard that keeps non-blocking logging workers alive.
|
||||
pub use self::tracing_runtime::LoggingGuard;
|
||||
/// Exposes initialization from a raw logging configuration section.
|
||||
|
||||
Reference in New Issue
Block a user