0.5.1-pre.005

This commit is contained in:
2026-08-10 01:36:42 +02:00
parent ec07ddbd80
commit b6a286a4df
54 changed files with 6236 additions and 5569 deletions

View File

@@ -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 denvironnement ;
- 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.*` ;

View File

@@ -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

View File

@@ -1,38 +1,45 @@
<!-- file: ks-logging/README.md -->
<!-- version: 5 -->
<!-- version: 6 -->
# ks-logging
`ks-logging` initialise le subscriber global `tracing` à partir dune 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 denvironnement ;
- 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 quun 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
Linitialisation multi-routes est fonctionnelle. Elle échoue explicitement si aucune route nest 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)

View File

@@ -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 dAPI 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 quun secret résolu natteint jamais une sortie logging.
- [ ] Documentation - maintenir le schéma et les exemples logging alignés avec les types possédés par la crate.

View File

@@ -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 nest 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à larrê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 derreur conserve la sémantique stricte prévue par limplé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 linstallation multi-routes ;
- `route_writer_filter_preserves_target_and_level_semantics` vérifie ladmission 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 linstallation, les filtres et le volume de routes.
## Limites
- initialisation globale unique par processus ;
- configuration fournie par lappelant ;
- la politique de redaction des valeurs secrètes avant logging est renforcée dans `0.5.1-pre.006`.

View File

@@ -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
View 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());
}
}

View File

@@ -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.