diff --git a/Cargo.toml b/Cargo.toml index 4c49b8f..9d15cdb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 46 +# version: 47 [workspace] resolver = "3" members = ["crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib"] [workspace.package] -version = "0.1.3-pre.5.fix.1" +version = "0.1.3-pre.6" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-logging-lib/README.md b/crates/ksp-logging-lib/README.md index d50ad9e..37a5ea7 100644 --- a/crates/ksp-logging-lib/README.md +++ b/crates/ksp-logging-lib/README.md @@ -1,5 +1,5 @@ - + # ksp-logging-lib @@ -14,8 +14,8 @@ La crate possède : - `LoggingSettings`, la console explicite et les settings fichier indépendants de Config ; - les formats runtime `Human/Compact/Pretty/Json` ; - zéro, un ou plusieurs outputs fichier actifs simultanément, identifiés par `output_id` unique ; -- le routing par output sur niveau et target KSP ; -- le contrat de filtre `domain`, dont l'activation runtime structurée est volontairement séparée dans la tranche suivante ; +- le routing par output sur niveau, target KSP et champ structuré `domain` ; +- l'héritage du `domain` effectif à travers les spans, avec possibilité pour un event ou un span enfant de le remplacer explicitement ; - l'installation unique du subscriber global ; - le hot reload via `reinitialize` sans second subscriber global ; - le takeover des logs : les targets externes sont silencieux par défaut ; @@ -28,7 +28,7 @@ L'API async de production reste indépendante de tout executor. Tokio est utilis ## Routing `domain` -`OutputFilter` représente déjà : +`OutputFilter` applique désormais les trois dimensions : ```text level @@ -36,9 +36,17 @@ targets[] domains[] ``` -`0.1.3-pre.005` active réellement `level` et `targets[]`. Un output actif dont `domains[]` est différent de `[*]` est refusé explicitement par `initialize/reinitialize` jusqu'à la tranche dédiée au routing structuré par champ `domain`. +Le `domain` reste un champ structuré distinct du target. Sa résolution runtime suit ces règles : -Ce refus évite d'assimiler artificiellement `domain` au target ou d'ignorer silencieusement la dimension demandée. +- le `domain` porté directement par un event est prioritaire ; +- sinon l'event hérite du `domain` effectif de son span ; +- un span qui porte son propre `domain` remplace celui de son parent ; +- un span sans `domain` hérite de celui de son parent au moment de sa création ; +- les événements de lifecycle de span utilisent le `domain` effectif du span concerné ; +- un selector `domains = ["*"]` accepte aussi les événements sans `domain` ; +- un selector nommé correspond par préfixe et ne sélectionne pas un événement sans `domain`. + +Le routing `domain` est appliqué en plus du niveau et du target de l'output. Il ne modifie ni le target propriétaire KSP ni les champs formatés. ## Frontières diff --git a/crates/ksp-logging-lib/TODO.md b/crates/ksp-logging-lib/TODO.md index 988c175..1f311f6 100644 --- a/crates/ksp-logging-lib/TODO.md +++ b/crates/ksp-logging-lib/TODO.md @@ -1,20 +1,17 @@ - + # TODO ksp-logging-lib ## À fermer pendant `0.1.3` -`0.1.3-pre.005` active le runtime multi-sink pour les dimensions disponibles directement dans les métadonnées `tracing` : niveau et target. +Les trois tranches Logging nécessaires à la fondation Config sont maintenant couvertes fonctionnellement : -La tranche Logging suivante doit encore : +- `pre.004` : contrats/settings multi-output ; +- `pre.005` : runtime multi-sink, formats et routing level/target ; +- `pre.006` : routing structuré `domain`, héritage de spans et lifecycle. -- appliquer réellement `OutputFilter.domains[]` à partir du champ structuré `domain` ; -- définir l'héritage du domain pour les events émis dans un span lorsqu'ils ne portent pas eux-mêmes le champ ; -- conserver une sémantique cohérente pour les événements de lifecycle de span ; -- couvrir ce routing par des tests dédiés avant de figer `std.logging.schema.json`. - -Tant que cette tranche n'est pas livrée, un output actif avec `domains[] != ["*"]` est refusé explicitement par `initialize/reinitialize`. +Avant le gel de `std.logging.schema.json`, il reste uniquement à faire valider `pre.006` par les commandes workspace usuelles. Les travaux suivants de `0.1.3` reviennent ensuite à `ksp-config-lib`. ## Capacités désormais actives @@ -22,12 +19,13 @@ Le runtime supporte maintenant : - zéro, un ou plusieurs outputs fichier simultanés ; - une console indépendante ; -- routing par niveau et target pour chaque output ; +- routing par niveau, target et `domain` structuré pour chaque output ; - `Human`, `Compact`, `Pretty` et `Json` ; - ANSI console configurable et fichiers persistants sans ANSI ; - guards non bloquants indépendants ; - compteurs agrégés et compteurs cumulatifs par `output_id` fichier ; -- hot reload transactionnel du groupe de sinks. +- hot reload transactionnel du groupe de sinks ; +- héritage du `domain` effectif pour les events/spans et lifecycle de spans. ## Capacités différées diff --git a/crates/ksp-logging-lib/USAGE.md b/crates/ksp-logging-lib/USAGE.md index 68da752..5eede8f 100644 --- a/crates/ksp-logging-lib/USAGE.md +++ b/crates/ksp-logging-lib/USAGE.md @@ -1,5 +1,5 @@ - + # Utilisation de ksp-logging-lib @@ -46,18 +46,20 @@ Une sortie fichier possède un `output_id` stable et unique dans `LoggingSetting Les fichiers persistants interdisent `ansi = true`. -## Runtime multi-output de `pre.005` +## Runtime multi-output et routing `domain` -`0.1.3-pre.005` active réellement : +`0.1.3-pre.005` active le multi-sink, les formats et le routing niveau/target ; `0.1.3-pre.006` complète le routing structuré `domain`. Le runtime supporte donc réellement : - plusieurs fichiers simultanés ; - les formats `Human`, `Compact`, `Pretty` et `Json` ; - l'ANSI console ; -- le routing par niveau et target pour chaque output ; +- le routing par niveau, target et `domain` pour chaque output ; - le comptage cumulatif des lignes abandonnées par `output_id` fichier ; - le hot reload de ces sorties sans réinstaller le subscriber global. -Le routing `domain` est volontairement séparé. `OutputFilter` continue de représenter `domains[]`, mais un output actif dont cette dimension est différente de `["*"]` est refusé par `initialize/reinitialize` jusqu'à la tranche suivante. La configuration n'est donc jamais acceptée puis partiellement ignorée. +Le `domain` n'est jamais transformé en target. Un event portant directement `domain` utilise cette valeur ; sinon il hérite du `domain` effectif de son span. Un span explicite remplace le `domain` de son parent, tandis qu'un span sans `domain` l'hérite. Les lifecycle events d'un span utilisent le même `domain` effectif. + +Pour `domains[]`, `["*"]` accepte tous les événements, y compris ceux sans `domain`. Un selector nommé correspond par préfixe et ne sélectionne pas une entrée sans `domain`. Les helpers `ConsoleSettings::stdout()` et `ConsoleSettings::stderr()` restent des raccourcis `Human`, sans ANSI et sans restriction supplémentaire par output. @@ -92,7 +94,7 @@ Une configuration sans output actif est valide et installe une infrastructure in ## Construction d'un runtime multi-output -Le runtime peut maintenant activer plusieurs sorties ayant des formats et filtres metadata distincts : +Le runtime peut activer plusieurs sorties ayant des formats et filtres niveau/target/domain distincts : ```rust let console = ksp_logging_lib::ConsoleSettings::new( @@ -131,7 +133,19 @@ let settings = ksp_logging_lib::LoggingSettings::new( let validation = settings.validate(); ``` -`validate()` vérifie le contrat structurel. `initialize/reinitialize` appliquent le niveau et les targets par output ; les domains spécifiques restent refusés jusqu'à la tranche de routing structuré. +`validate()` vérifie le contrat structurel. `initialize/reinitialize` appliquent ensuite conjointement niveau, target et `domain` par output. + +Par exemple, un fichier réservé au domaine Store peut utiliser : + +```rust +ksp_logging_lib::OutputFilter::new( + ksp_logging_lib::LogFilterLevel::Debug, + std::vec!["ksp-store-lib".to_string()], + std::vec!["store".to_string()], +) +``` + +Un event `domain = "store.postgres"` correspond au selector `store`; un event sans `domain` n'y correspond pas. ## Hot reload diff --git a/crates/ksp-logging-lib/src/domain.rs b/crates/ksp-logging-lib/src/domain.rs new file mode 100644 index 0000000..640a15e --- /dev/null +++ b/crates/ksp-logging-lib/src/domain.rs @@ -0,0 +1,181 @@ +// file: crates/ksp-logging-lib/src/domain.rs +// version: 1 + +std::thread_local! { + static CURRENT_DOMAIN: std::cell::RefCell> = const { std::cell::RefCell::new(std::option::Option::None) }; +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +struct SpanDomain { + value: std::option::Option, +} + +#[derive(Default)] +struct DomainVisitor { + value: std::option::Option, +} + +impl tracing::field::Visit for DomainVisitor { + fn record_str(&mut self, field: &tracing::field::Field, value: &str) { + if field.name() == "domain" { + self.value = std::option::Option::Some(value.to_string()); + } + } + + fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) { + if field.name() == "domain" { + self.value = std::option::Option::Some(format!("{value:?}")); + } + } +} + +pub(crate) struct DomainContextLayer; + +impl DomainContextLayer { + pub(crate) const fn new() -> Self { + return Self; + } +} + +impl tracing_subscriber::Layer for DomainContextLayer +where + S: tracing::Subscriber + for<'lookup> tracing_subscriber::registry::LookupSpan<'lookup>, +{ + fn on_new_span(&self, attrs: &tracing::span::Attributes<'_>, id: &tracing::span::Id, ctx: tracing_subscriber::layer::Context<'_, S>) { + let mut visitor = DomainVisitor::default(); + attrs.record(&mut visitor); + let effective_domain = match visitor.value { + std::option::Option::Some(domain) => std::option::Option::Some(domain), + std::option::Option::None => span_parent_domain(id, &ctx), + }; + if let std::option::Option::Some(span) = ctx.span(id) { + span.extensions_mut().insert(SpanDomain { value: effective_domain.clone() }); + } + set_current_domain(effective_domain.as_deref()); + } + + fn on_record(&self, id: &tracing::span::Id, values: &tracing::span::Record<'_>, ctx: tracing_subscriber::layer::Context<'_, S>) { + let mut visitor = DomainVisitor::default(); + values.record(&mut visitor); + if let std::option::Option::Some(domain) = visitor.value { + if let std::option::Option::Some(span) = ctx.span(id) { + let mut extensions = span.extensions_mut(); + if let std::option::Option::Some(stored) = extensions.get_mut::() { + stored.value = std::option::Option::Some(domain.clone()); + } else { + extensions.insert(SpanDomain { value: std::option::Option::Some(domain.clone()) }); + } + } + set_current_domain(std::option::Option::Some(domain.as_str())); + return; + } + let domain = span_domain(id, &ctx); + set_current_domain(domain.as_deref()); + } + + fn on_event(&self, event: &tracing::Event<'_>, ctx: tracing_subscriber::layer::Context<'_, S>) { + let mut visitor = DomainVisitor::default(); + event.record(&mut visitor); + let effective_domain = match visitor.value { + std::option::Option::Some(domain) => std::option::Option::Some(domain), + std::option::Option::None => event_parent_domain(event, &ctx), + }; + set_current_domain(effective_domain.as_deref()); + } + + fn on_enter(&self, id: &tracing::span::Id, ctx: tracing_subscriber::layer::Context<'_, S>) { + let domain = span_domain(id, &ctx); + set_current_domain(domain.as_deref()); + } + + fn on_exit(&self, id: &tracing::span::Id, ctx: tracing_subscriber::layer::Context<'_, S>) { + let domain = span_domain(id, &ctx); + set_current_domain(domain.as_deref()); + } + + fn on_close(&self, id: tracing::span::Id, ctx: tracing_subscriber::layer::Context<'_, S>) { + let domain = span_domain(&id, &ctx); + set_current_domain(domain.as_deref()); + } +} + +pub(crate) fn current_domain_matches(selectors: &[std::string::String]) -> bool { + if let [selector] = selectors + && selector == "*" + { + return true; + } + return CURRENT_DOMAIN.with(|current| -> bool { + let borrow_result = current.try_borrow(); + let current = match borrow_result { + std::result::Result::Ok(current) => current, + std::result::Result::Err(_) => return false, + }; + let domain = match current.as_ref() { + std::option::Option::Some(domain) => domain, + std::option::Option::None => return false, + }; + return selectors.iter().any(|selector| -> bool { + return domain.starts_with(selector.as_str()); + }); + }); +} + +fn set_current_domain(domain: std::option::Option<&str>) { + CURRENT_DOMAIN.with(|current| { + let borrow_result = current.try_borrow_mut(); + if let std::result::Result::Ok(mut current) = borrow_result { + *current = domain.map(std::borrow::ToOwned::to_owned); + } + }); +} + +fn span_parent_domain(id: &tracing::span::Id, ctx: &tracing_subscriber::layer::Context<'_, S>) -> std::option::Option +where + S: tracing::Subscriber + for<'lookup> tracing_subscriber::registry::LookupSpan<'lookup>, +{ + let span = match ctx.span(id) { + std::option::Option::Some(span) => span, + std::option::Option::None => return std::option::Option::None, + }; + let parent = match span.parent() { + std::option::Option::Some(parent) => parent, + std::option::Option::None => return std::option::Option::None, + }; + let extensions = parent.extensions(); + return extensions.get::().and_then(|domain| -> std::option::Option { + return domain.value.clone(); + }); +} + +fn span_domain(id: &tracing::span::Id, ctx: &tracing_subscriber::layer::Context<'_, S>) -> std::option::Option +where + S: tracing::Subscriber + for<'lookup> tracing_subscriber::registry::LookupSpan<'lookup>, +{ + let span = match ctx.span(id) { + std::option::Option::Some(span) => span, + std::option::Option::None => return std::option::Option::None, + }; + let extensions = span.extensions(); + return extensions.get::().and_then(|domain| -> std::option::Option { + return domain.value.clone(); + }); +} + +fn event_parent_domain(event: &tracing::Event<'_>, ctx: &tracing_subscriber::layer::Context<'_, S>) -> std::option::Option +where + S: tracing::Subscriber + for<'lookup> tracing_subscriber::registry::LookupSpan<'lookup>, +{ + let parent = match ctx.event_span(event) { + std::option::Option::Some(parent) => parent, + std::option::Option::None => return std::option::Option::None, + }; + let extensions = parent.extensions(); + return extensions.get::().and_then(|domain| -> std::option::Option { + return domain.value.clone(); + }); +} + +#[cfg(test)] +#[path = "../unit_tests/domain.rs"] +mod tests; diff --git a/crates/ksp-logging-lib/src/lib.rs b/crates/ksp-logging-lib/src/lib.rs index ca5fbb6..4a323c2 100644 --- a/crates/ksp-logging-lib/src/lib.rs +++ b/crates/ksp-logging-lib/src/lib.rs @@ -1,5 +1,5 @@ // file: crates/ksp-logging-lib/src/lib.rs -// version: 6 +// version: 7 #![warn(missing_docs)] #![deny(unreachable_pub)] #![forbid(unsafe_code)] @@ -7,10 +7,11 @@ //! KSP-owned logging and tracing facade. //! //! This crate owns the KSP runtime logging contract. Behavioral KSP crates emit events and spans through this facade rather than depending directly on the -//! `tracing` stack. The crate owns the single global subscriber, KSP takeover filtering, hot reload and non-blocking outputs. `0.1.3-pre.005` activates -//! multiple simultaneous file outputs, per-output level/target routing, selectable formats, console ANSI and per-file dropped-line accounting. Structured -//! `domain` routing remains explicitly rejected until its dedicated runtime tranche so field-based routing is never approximated or ignored silently. +//! `tracing` stack. The crate owns the single global subscriber, KSP takeover filtering, hot reload and non-blocking outputs. `0.1.3-pre.006` supports +//! multiple simultaneous outputs with per-output level/target/domain routing, selectable formats, console ANSI and per-file dropped-line accounting. Structured +//! `domain` routing remains distinct from targets and follows explicit event domains or inherited span domains. +mod domain; mod error; mod macros; mod runtime; diff --git a/crates/ksp-logging-lib/src/runtime.rs b/crates/ksp-logging-lib/src/runtime.rs index f369150..e9af54a 100644 --- a/crates/ksp-logging-lib/src/runtime.rs +++ b/crates/ksp-logging-lib/src/runtime.rs @@ -1,5 +1,5 @@ // file: crates/ksp-logging-lib/src/runtime.rs -// version: 10 +// version: 11 use tracing_subscriber::Layer; // rust-rules: trait-import use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import @@ -219,10 +219,6 @@ fn prepare_runtime(settings: &crate::LoggingSettings) -> ksp_core_lib::Result bool { return console.enabled(); }); @@ -248,38 +244,11 @@ fn prepare_runtime(settings: &crate::LoggingSettings) -> ksp_core_lib::Result ksp_core_lib::Result<()> { - if let std::option::Option::Some(console) = settings.console() - && console.enabled() - && !domains_are_unrestricted(console.filter()) - { - return runtime_capability_error("domain routing requires the dedicated structured-domain runtime tranche"); - } - for file in settings.files() { - if file.enabled() && !domains_are_unrestricted(file.filter()) { - return runtime_capability_error("domain routing requires the dedicated structured-domain runtime tranche"); - } - } - return std::result::Result::Ok(()); -} - -fn domains_are_unrestricted(filter: &crate::OutputFilter) -> bool { - return match filter.domains() { - [selector] => selector == "*", - _ => false, - }; -} - -fn runtime_capability_error(message: &str) -> ksp_core_lib::Result<()> { - return std::result::Result::Err( - ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, message).with_context("runtime_contract", "metadata-routing-before-domain-routing"), - ); -} - fn build_console_output(console: &crate::ConsoleSettings, settings: &crate::LoggingSettings) -> PreparedOutput { return match console.output() { crate::ConsoleOutput::Stdout => build_non_blocking_output( diff --git a/crates/ksp-logging-lib/src/writer.rs b/crates/ksp-logging-lib/src/writer.rs index 7452841..a984a61 100644 --- a/crates/ksp-logging-lib/src/writer.rs +++ b/crates/ksp-logging-lib/src/writer.rs @@ -1,5 +1,5 @@ // file: crates/ksp-logging-lib/src/writer.rs -// version: 2 +// version: 3 #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum StripAnsiState { @@ -158,9 +158,12 @@ fn metadata_matches_filter(metadata: &tracing::Metadata<'_>, filter: &crate::Out if !level_is_enabled(metadata.level(), filter.level()) { return false; } - return filter.targets().iter().any(|selector| -> bool { + if !filter.targets().iter().any(|selector| -> bool { return selector == "*" || metadata.target().starts_with(selector.as_str()); - }); + }) { + return false; + } + return crate::domain::current_domain_matches(filter.domains()); } fn level_is_enabled(level: &tracing::Level, filter: crate::LogFilterLevel) -> bool { diff --git a/crates/ksp-logging-lib/tests/runtime.rs b/crates/ksp-logging-lib/tests/runtime.rs index cdc2a73..a0bec33 100644 --- a/crates/ksp-logging-lib/tests/runtime.rs +++ b/crates/ksp-logging-lib/tests/runtime.rs @@ -1,5 +1,5 @@ // file: crates/ksp-logging-lib/tests/runtime.rs -// version: 7 +// version: 8 //! Integration tests for global initialization, takeover filtering, non-blocking outputs and hot reload. @@ -173,21 +173,6 @@ fn global_runtime_supports_takeover_non_blocking_outputs_hot_reload_and_single_i assert!(logging_trace_enabled()); assert!(!external_error_enabled()); exercise_concurrent_reload(&mut guard, &disabled); - let domain_routed = ksp_logging_lib::LoggingSettings::new( - ksp_logging_lib::LogFilterLevel::Info, - ksp_logging_lib::SpanEvents::Off, - std::option::Option::Some(ksp_logging_lib::ConsoleSettings::new( - true, - ksp_logging_lib::ConsoleOutput::Stderr, - false, - ksp_logging_lib::LogFormat::Human, - ksp_logging_lib::OutputFilter::new(ksp_logging_lib::LogFilterLevel::Info, std::vec!["*".to_string()], std::vec!["logging".to_string()]), - )), - std::vec::Vec::new(), - ); - let domain_reload = ksp_logging_lib::reinitialize(&mut guard, &domain_routed); - assert!(domain_reload.is_err()); - assert_ne!(guard.settings(), &domain_routed); let human_directory = root.join("human"); let compact_directory = root.join("compact"); let pretty_directory = root.join("pretty"); @@ -272,6 +257,119 @@ fn global_runtime_supports_takeover_non_blocking_outputs_hot_reload_and_single_i assert_eq!(guard.dropped_file_lines("file.pretty.error"), std::option::Option::Some(0)); assert_eq!(guard.dropped_file_lines("file.json.logging"), std::option::Option::Some(0)); assert_eq!(guard.dropped_file_lines("file.unknown"), std::option::Option::None); + let domain_logging_directory = root.join("domain-logging"); + let domain_store_directory = root.join("domain-store"); + let domain_any_directory = root.join("domain-any"); + let domain_settings = ksp_logging_lib::LoggingSettings::new( + ksp_logging_lib::LogFilterLevel::Trace, + ksp_logging_lib::SpanEvents::Full, + std::option::Option::None, + std::vec![ + ksp_logging_lib::FileSettings::new( + "file.domain.logging", + true, + domain_logging_directory.as_path(), + "logging.log", + ksp_logging_lib::FileRotation::Never, + ksp_logging_lib::LogFormat::Human, + ksp_logging_lib::OutputFilter::new( + ksp_logging_lib::LogFilterLevel::Trace, + std::vec![LOGGING_TARGET.to_string()], + std::vec!["logging".to_string()], + ), + ), + ksp_logging_lib::FileSettings::new( + "file.domain.store", + true, + domain_store_directory.as_path(), + "store.log", + ksp_logging_lib::FileRotation::Never, + ksp_logging_lib::LogFormat::Human, + ksp_logging_lib::OutputFilter::new( + ksp_logging_lib::LogFilterLevel::Trace, + std::vec![LOGGING_TARGET.to_string()], + std::vec!["store".to_string()], + ), + ), + ksp_logging_lib::FileSettings::new( + "file.domain.any", + true, + domain_any_directory.as_path(), + "any.log", + ksp_logging_lib::FileRotation::Never, + ksp_logging_lib::LogFormat::Human, + ksp_logging_lib::OutputFilter::new(ksp_logging_lib::LogFilterLevel::Trace, std::vec![LOGGING_TARGET.to_string()], std::vec!["*".to_string()],), + ), + ], + ); + let domain_reload = ksp_logging_lib::reinitialize(&mut guard, &domain_settings); + assert!(domain_reload.is_ok()); + ksp_logging_lib::info!(target: LOGGING_TARGET, domain = "logging.runtime", "direct logging domain marker"); + ksp_logging_lib::info!(target: LOGGING_TARGET, domain = "store", "direct store domain marker"); + ksp_logging_lib::info!(target: LOGGING_TARGET, "undomained marker"); + let logging_lifecycle = ksp_logging_lib::info_span!(target: LOGGING_TARGET, "logging_lifecycle_span", domain = "logging"); + logging_lifecycle.in_scope(|| { + return; + }); + let store_lifecycle = ksp_logging_lib::info_span!(target: LOGGING_TARGET, "store_lifecycle_span", domain = "store"); + store_lifecycle.in_scope(|| { + return; + }); + let logging_parent = ksp_logging_lib::info_span!(target: LOGGING_TARGET, "logging_parent_span", domain = "logging"); + logging_parent.in_scope(|| { + ksp_logging_lib::info!(target: LOGGING_TARGET, "inherited logging marker"); + ksp_logging_lib::info!(target: LOGGING_TARGET, domain = "store", "event override store marker"); + let inherited_child = ksp_logging_lib::info_span!(target: LOGGING_TARGET, "inherited_child_span"); + inherited_child.in_scope(|| { + ksp_logging_lib::info!(target: LOGGING_TARGET, "child inherited logging marker"); + }); + let store_child = ksp_logging_lib::info_span!(target: LOGGING_TARGET, "store_child_span", domain = "store"); + store_child.in_scope(|| { + ksp_logging_lib::info!(target: LOGGING_TARGET, "child explicit store marker"); + }); + }); + let store_parent = ksp_logging_lib::info_span!(target: LOGGING_TARGET, "store_parent_span", domain = "store"); + store_parent.in_scope(|| { + ksp_logging_lib::info!(target: LOGGING_TARGET, "inherited store marker"); + }); + let disable_after_domain = ksp_logging_lib::reinitialize(&mut guard, &disabled); + assert!(disable_after_domain.is_ok()); + let domain_logging_text = read_directory_text(domain_logging_directory.as_path()); + let domain_store_text = read_directory_text(domain_store_directory.as_path()); + let domain_any_text = read_directory_text(domain_any_directory.as_path()); + assert!(domain_logging_text.contains("direct logging domain marker")); + assert!(domain_logging_text.contains("inherited logging marker")); + assert!(domain_logging_text.contains("child inherited logging marker")); + assert!(domain_logging_text.contains("logging_lifecycle_span")); + assert!(!domain_logging_text.contains("direct store domain marker")); + assert!(!domain_logging_text.contains("event override store marker")); + assert!(!domain_logging_text.contains("child explicit store marker")); + assert!(!domain_logging_text.contains("inherited store marker")); + assert!(!domain_logging_text.contains("store_lifecycle_span")); + assert!(!domain_logging_text.contains("undomained marker")); + assert!(domain_store_text.contains("direct store domain marker")); + assert!(domain_store_text.contains("event override store marker")); + assert!(domain_store_text.contains("child explicit store marker")); + assert!(domain_store_text.contains("inherited store marker")); + assert!(domain_store_text.contains("store_lifecycle_span")); + assert!(!domain_store_text.contains("direct logging domain marker")); + assert!(!domain_store_text.contains("inherited logging marker")); + assert!(!domain_store_text.contains("child inherited logging marker")); + assert!(!domain_store_text.contains("logging_lifecycle_span")); + assert!(!domain_store_text.contains("undomained marker")); + assert!(domain_any_text.contains("direct logging domain marker")); + assert!(domain_any_text.contains("direct store domain marker")); + assert!(domain_any_text.contains("undomained marker")); + assert!(domain_any_text.contains("inherited logging marker")); + assert!(domain_any_text.contains("event override store marker")); + assert!(domain_any_text.contains("child inherited logging marker")); + assert!(domain_any_text.contains("child explicit store marker")); + assert!(domain_any_text.contains("inherited store marker")); + assert!(domain_any_text.contains("logging_lifecycle_span")); + assert!(domain_any_text.contains("store_lifecycle_span")); + assert_eq!(guard.dropped_file_lines("file.domain.logging"), std::option::Option::Some(0)); + assert_eq!(guard.dropped_file_lines("file.domain.store"), std::option::Option::Some(0)); + assert_eq!(guard.dropped_file_lines("file.domain.any"), std::option::Option::Some(0)); let dropped = guard.dropped_lines(); assert_eq!(dropped.total(), dropped.console().saturating_add(dropped.file())); let second_initialize = ksp_logging_lib::initialize(&disabled); diff --git a/crates/ksp-logging-lib/unit_tests/domain.rs b/crates/ksp-logging-lib/unit_tests/domain.rs new file mode 100644 index 0000000..0a7b578 --- /dev/null +++ b/crates/ksp-logging-lib/unit_tests/domain.rs @@ -0,0 +1,20 @@ +// file: crates/ksp-logging-lib/unit_tests/domain.rs +// version: 1 + +#[test] +fn wildcard_domain_matches_with_or_without_current_domain() { + super::set_current_domain(std::option::Option::None); + assert!(super::current_domain_matches(&["*".to_string()])); + super::set_current_domain(std::option::Option::Some("logging")); + assert!(super::current_domain_matches(&["*".to_string()])); +} + +#[test] +fn named_domain_requires_current_matching_prefix() { + super::set_current_domain(std::option::Option::None); + assert!(!super::current_domain_matches(&["logging".to_string()])); + super::set_current_domain(std::option::Option::Some("logging.runtime")); + assert!(super::current_domain_matches(&["logging".to_string()])); + assert!(super::current_domain_matches(&["store".to_string(), "logging.runtime".to_string()])); + assert!(!super::current_domain_matches(&["store".to_string()])); +} diff --git a/crates/ksp-logging-lib/unit_tests/runtime.rs b/crates/ksp-logging-lib/unit_tests/runtime.rs index 9fa748f..cf63d8a 100644 --- a/crates/ksp-logging-lib/unit_tests/runtime.rs +++ b/crates/ksp-logging-lib/unit_tests/runtime.rs @@ -1,5 +1,5 @@ // file: crates/ksp-logging-lib/unit_tests/runtime.rs -// version: 8 +// version: 9 #[test] fn level_mapping_covers_all_ksp_levels() { @@ -154,7 +154,7 @@ fn multi_sink_runtime_accepts_metadata_routing_formats_and_console_ansi() { } #[test] -fn domain_routing_is_explicitly_deferred_instead_of_ignored() { +fn domain_routing_is_accepted_by_runtime_preparation() { let console = crate::ConsoleSettings::new( true, crate::ConsoleOutput::Stdout, @@ -165,12 +165,7 @@ fn domain_routing_is_explicitly_deferred_instead_of_ignored() { let settings = crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::Some(console), std::vec::Vec::new()); assert!(settings.validate().is_ok()); let result = super::prepare_runtime(&settings); - assert!(result.is_err()); - let error = match result { - std::result::Result::Ok(_) => return, - std::result::Result::Err(error) => error, - }; - assert_eq!(error.code(), crate::ERROR_CODE_INVALID_SETTINGS); + assert!(result.is_ok()); } struct BlockingWriter { diff --git a/deltas/0.1.3/pre.006.md b/deltas/0.1.3/pre.006.md new file mode 100644 index 0000000..3fddcd1 --- /dev/null +++ b/deltas/0.1.3/pre.006.md @@ -0,0 +1,226 @@ + + + +# Delta 0.1.3-pre.006 + +## Base requise + +Livraison précédente validée : + +```text +0.1.3-pre.005-fix.001 +``` + +Version technique de cette base : + +```text +workspace.package.version = "0.1.3-pre.5.fix.1" +Cargo.toml header version = 46 +``` + +Validations utilisateur exécutées le 2026-08-15 : + +```text +cargo fmt --all OK +cargo check --workspace OK +cargo clippy --workspace --all-targets OK +cargo test --workspace OK +cargo tree -p ksp-logging-lib OK +cargo tree -p ksp-logging-lib -d OK — aucune duplication +cargo tree -p ksp-logging-lib -e features OK +``` + +Le fix JSON de `pre.005` est donc clos avant ouverture de cette tranche. + +## Objet de pre.006 + +Fermer la troisième dimension de routing déjà représentée par `OutputFilter` : + +```text +level +target +domain +``` + +`domain` reste un champ structuré indépendant du target. Cette tranche ne change pas les contrats Config et ne crée encore aucun document JSON Config. + +## Domain effectif + +Le runtime applique la sémantique suivante : + +1. un event portant directement `domain` utilise cette valeur ; +2. sinon l'event hérite du domain effectif de son span ; +3. un span portant directement `domain` définit son domain effectif ; +4. un span sans `domain` hérite du domain effectif de son parent au moment de sa création ; +5. un span enfant explicite peut donc remplacer le domain hérité ; +6. les lifecycle events `NEW`, `ENTER`, `EXIT` et `CLOSE` utilisent le domain effectif du span concerné. + +Un domain direct d'event ne modifie pas le domain mémorisé du span. + +## Routing par output + +Le writer routé applique maintenant conjointement : + +```text +OutputFilter.level +OutputFilter.targets[] +OutputFilter.domains[] +``` + +Pour `domains[]` : + +- `["*"]` signifie aucune restriction et accepte aussi un event/span sans domain ; +- un selector nommé correspond par préfixe ; +- un selector nommé ne correspond jamais à une entrée sans domain. + +La politique globale existante reste distincte : + +```text +default_filter + TargetFilter[] + ↓ +DomainContextLayer + groupe de sinks + ↓ +OutputFilter level/target/domain de chaque sink +``` + +Aucune convention de pseudo-target domain n'est introduite. + +## Couche interne de contexte + +`DomainContextLayer` est une couche privée de `ksp-logging-lib` installée avant les formatters lorsque le runtime possède au moins un output actif. + +Elle : + +- capture le champ `domain` des events et spans ; +- conserve le domain effectif d'un span dans ses extensions runtime ; +- expose uniquement au routing interne le domain effectif correspondant au callback en cours ; +- conserve les champs originaux pour les formatters Human/Compact/Pretty/Json ; +- n'ajoute aucun type public et ne change pas la façade consommateur. + +Le contexte courant utilisé par le routing reste interne au thread de dispatch Logging et n'est pas une variable globale Config/applicative. + +## Runtime préservé + +Cette tranche conserve sans changement de contrat : + +- subscriber global unique ; +- takeover des targets externes ; +- multi-sink ; +- formats Human/Compact/Pretty/Json ; +- ANSI console et stripping fichier ; +- writers non bloquants ; +- guards et compteurs par `output_id` ; +- hot reload transactionnel ; +- comportement de saturation. + +Le refus transitoire `domains != ["*"]` introduit en `pre.005` est supprimé puisque la dimension domain est maintenant exécutée réellement. + +## Tests + +Les tests unitaires couvrent : + +- wildcard domain avec ou sans domain courant ; +- selector nommé absent sans domain ; +- correspondance par préfixe ; +- acceptation runtime d'un filtre domain spécifique. + +Le test d'intégration global ajoute trois sinks simultanés : + +```text +file.domain.logging -> domains = ["logging"] +file.domain.store -> domains = ["store"] +file.domain.any -> domains = ["*"] +``` + +Il couvre : + +- domain direct d'event ; +- event sans domain ; +- héritage depuis un span parent ; +- override direct d'event ; +- enfant sans domain ; +- enfant avec domain explicite ; +- lifecycle de spans Logging/Store ; +- maintien des compteurs à zéro dans le scénario nominal. + +## Dépendances + +Aucune nouvelle dépendance et aucune nouvelle feature Cargo ne sont ajoutées. + +`ksp-logging-lib` reste le seul propriétaire direct de la stack `tracing*` et ne dépend toujours pas de `ksp-config-lib`. + +## Plan + +Les trois tranches Logging nécessaires avant le premier schema Config sont désormais fonctionnellement couvertes : + +```text +pre.004 contrats/settings multi-output +pre.005 runtime multi-sink + level/target/formats +pre.006 routing structuré domain +``` + +Après validation utilisateur de `pre.006`, la prochaine tranche est : + +```text +pre.007 JSON/JSON Schema + std.logging.json +``` + +## Version technique + +La prerelease devient : + +```text +workspace.package.version = "0.1.3-pre.6" +Cargo.toml header version = 47 +``` + +## Fichiers ajoutés + +```text +crates/ksp-logging-lib/src/domain.rs +crates/ksp-logging-lib/unit_tests/domain.rs +deltas/0.1.3/pre.006.md +``` + +## Fichiers modifiés + +```text +Cargo.toml +crates/ksp-logging-lib/README.md +crates/ksp-logging-lib/TODO.md +crates/ksp-logging-lib/USAGE.md +crates/ksp-logging-lib/src/lib.rs +crates/ksp-logging-lib/src/runtime.rs +crates/ksp-logging-lib/src/writer.rs +crates/ksp-logging-lib/tests/runtime.rs +crates/ksp-logging-lib/unit_tests/runtime.rs +docs/plans/000-README.md +docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md +docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md +``` + +## Hors scope + +Cette tranche ne modifie pas : + +- `ksp-config-lib` ; +- le registre `file_id` ; +- JSON/JSON Schema ; +- `std.logging.json` ; +- profils/composites ; +- `.env` ou les variables d'environnement ; +- persistence Config. + +## Validations à exécuter + +```bash +cargo fmt --all +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test --workspace +cargo tree -p ksp-logging-lib +cargo tree -p ksp-logging-lib -d +cargo tree -p ksp-logging-lib -e features +``` + +Aucune validation Cargo n'est déclarée réussie tant qu'elle n'a pas été exécutée sur le workspace utilisateur. diff --git a/docs/plans/000-README.md b/docs/plans/000-README.md index e829d90..310992e 100644 --- a/docs/plans/000-README.md +++ b/docs/plans/000-README.md @@ -1,5 +1,5 @@ - + # Plans KSP @@ -13,7 +13,7 @@ Un plan décrit le périmètre, les décisions déjà acquises, les questions ou - [`002-FUNCTIONAL_RELEASE_SEQUENCE.md`](002-FUNCTIONAL_RELEASE_SEQUENCE.md) — séquence active de référence des premières releases fonctionnelles ; - [`003-V0_1_1_CORE_FOUNDATION_PLAN.md`](003-V0_1_1_CORE_FOUNDATION_PLAN.md) — plan historique clôturé de la release stable `0.1.1`, établi par `0.1.1-pre.001` puis consolidé jusqu'à `0.1.1-rel.001`. - [`004-V0_1_2_LOGGING_FOUNDATION_PLAN.md`](004-V0_1_2_LOGGING_FOUNDATION_PLAN.md) — plan historique clôturé de la release stable `0.1.2`, établi par `0.1.2-pre.001` puis consolidé jusqu'à `0.1.2-rel.001`. -- [`005-V0_1_3_CONFIG_FOUNDATION_PLAN.md`](005-V0_1_3_CONFIG_FOUNDATION_PLAN.md) — plan actif de `0.1.3 — Configuration foundation`, établi par `0.1.3-pre.001`, corrigé par `pre.001-fix.001`, complété par `pre.001-fix.002` pour le registre `file_id`/bootstrap/non-régression Logging, regranularisé par `pre.001-fix.003`, puis rescindé pendant `pre.005` pour traiter séparément le routing structuré `domain` avant le premier schema Logging. +- [`005-V0_1_3_CONFIG_FOUNDATION_PLAN.md`](005-V0_1_3_CONFIG_FOUNDATION_PLAN.md) — plan actif de `0.1.3 — Configuration foundation`, établi par `0.1.3-pre.001`, corrigé par `pre.001-fix.001`, complété par `pre.001-fix.002` pour le registre `file_id`/bootstrap/non-régression Logging, regranularisé par `pre.001-fix.003`, puis rescindé pendant `pre.005`; `pre.006` ferme maintenant le routing structuré `domain` avant le premier schema Logging de `pre.007`. Le `pre.001` de chaque release fonctionnelle peut introduire son propre plan détaillé lorsque la release s'ouvre. diff --git a/docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md b/docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md index 0a7392c..b6570d9 100644 --- a/docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md +++ b/docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md @@ -1,5 +1,5 @@ - + # Séquence des releases fonctionnelles KSP @@ -219,6 +219,8 @@ pre.015 clôture Cette prévision n'est pas un plafond : chaque prerelease doit rester une petite tranche, avec scission explicite si l'objectif dépasse environ 15–20 minutes de travail effectif. +Après validation de `pre.005-fix.001`, `pre.006` ferme le routing Logging structuré `domain`; `pre.007` redevient donc la prochaine tranche Config avec JSON/JSON Schema et le premier document `std.logging.json`. + ## `0.1.4` — Config desktop par défaut La décision `0.1.3-pre.001` conserve cette release comme étape suivante par défaut. diff --git a/docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md b/docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md index 3bdc3e7..1028242 100644 --- a/docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md +++ b/docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md @@ -1,11 +1,11 @@ - + # Plan `0.1.3` — Configuration foundation ## 1. Statut et objectif -Ce plan a été établi par `0.1.3-pre.001`, corrigé par `0.1.3-pre.001-fix.001/.002/.003`, puis exécuté par petites tranches. `pre.002` a livré le bootstrap Config, `pre.003` le registre `file_id`, `pre.004` les contrats publics multi-output de Logging, et `pre.005` active le runtime multi-sink sur niveau/target/formats. Le routing structuré `domain` est scindé en `pre.006` avant le gel du premier schema Logging. +Ce plan a été établi par `0.1.3-pre.001`, corrigé par `0.1.3-pre.001-fix.001/.002/.003`, puis exécuté par petites tranches. `pre.002` a livré le bootstrap Config, `pre.003` le registre `file_id`, `pre.004` les contrats publics multi-output de Logging, `pre.005` le runtime multi-sink sur niveau/target/formats et `pre.006` ferme le routing structuré `domain`. Le premier schema Logging peut donc commencer en `pre.007` après validation utilisateur de cette tranche. La base auditée reste la release stable `v0.1.2`. @@ -1677,23 +1677,31 @@ Implémentation candidate de `pre.005` : - JSON + ANSI console est refusé comme combinaison incohérente ; - aucune dépendance Config n'entre dans Logging. -La validation utilisateur de `pre.005` est requise avant l'ouverture de `pre.006`. +`pre.005` a d'abord révélé une assertion d'intégration JSON incorrecte : le message de test contenait lui-même une séquence ANSI que le formatter JSON sérialisait comme donnée échappée. `pre.005-fix.001` a séparé le test JSON du test de stripping ANSI sans modifier le runtime. L'utilisateur a ensuite validé `cargo fmt --all`, `cargo check --workspace`, `cargo clippy --workspace --all-targets`, `cargo test --workspace` et les trois vues `cargo tree -p ksp-logging-lib` sans warning ni échec. ### `0.1.3-pre.006` — `ksp-logging-lib` : routing structuré `domain` Objectif unique : fermer proprement la dimension `OutputFilter.domains[]` avant que Config ne fige le schema Logging. -- extraire le champ structuré `domain` des events/spans ; -- définir la sémantique d'un event portant son propre `domain` ; -- définir l'héritage depuis le span courant lorsqu'un event ne porte pas de `domain` ; -- définir le comportement des spans sans domain et des spans imbriqués ; -- appliquer les selectors domain par output sans casser level/target ; -- conserver les événements lifecycle `SpanEvents` cohérents avec le domain du span ; -- préserver les champs formatés requis par les différents formatters ; -- conserver hot reload, takeover et non-blocking ; -- tests unitaires/intégration sur events, spans, héritage et plusieurs sinks. +Implémentation candidate livrée par `pre.006` : -Une solution qui réduirait `domain` à une convention de target est interdite : il s'agit d'une dimension structurée distincte. +- `domain` reste un champ structuré distinct du target et n'est jamais converti en pseudo-target ; +- une couche interne `DomainContextLayer` capture le `domain` effectif avant les formatters de sortie ; +- un event portant directement `domain` remplace le domain hérité pour cet event ; +- un event sans `domain` hérite du domain effectif de son span ; +- un span portant `domain` définit son domain effectif ; +- un span sans `domain` hérite du domain effectif de son parent au moment de sa création ; +- les lifecycle events `NEW/ENTER/EXIT/CLOSE` utilisent le domain effectif du span concerné ; +- `domains = ["*"]` reste l'absence de restriction et accepte aussi les events/spans sans domain ; +- un selector domain nommé correspond par préfixe et ne sélectionne pas une entrée sans domain ; +- le writer applique conjointement `level`, `target` et le domain effectif ; +- la politique globale de takeover KSP, le subscriber unique, le hot reload transactionnel et les writers non bloquants restent inchangés ; +- le contexte domain est thread-local interne au dispatch Logging et n'entre pas dans les contrats publics ; +- les tests couvrent domain direct, absence de domain, héritage parent/enfant, override d'event, override de span enfant, lifecycle et trois sinks domain distincts. + +Aucune dépendance externe ou feature Cargo supplémentaire n'est nécessaire pour cette tranche. `pre.007` peut donc construire `std.logging.schema.json` sur une surface Logging qui représente et exécute réellement les trois dimensions de routing retenues : level, target et domain. + +La validation utilisateur de `pre.006` reste requise avant l'ouverture de `pre.007`. ### `0.1.3-pre.007` — JSON/JSON Schema + premier document Logging