diff --git a/Cargo.toml b/Cargo.toml index 4fc3aa7..65a6807 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 28 +# version: 29 [workspace] resolver = "3" members = ["crates/ksp-core-lib", "crates/ksp-logging-lib"] [workspace.package] -version = "0.1.2-pre.2.fix.1" +version = "0.1.2-pre.3" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" @@ -16,6 +16,7 @@ publish = false [workspace.dependencies] solana-pubkey = { version = "^4.3", default-features = false } tracing = { version = "^0.1", default-features = false, features = ["std"] } +tracing-subscriber = { version = "^0.3", default-features = false, features = ["fmt"] } [workspace.lints.rust] missing_docs = "warn" diff --git a/crates/ksp-logging-lib/Cargo.toml b/crates/ksp-logging-lib/Cargo.toml index eb1804c..a395e65 100644 --- a/crates/ksp-logging-lib/Cargo.toml +++ b/crates/ksp-logging-lib/Cargo.toml @@ -1,5 +1,5 @@ # file: crates/ksp-logging-lib/Cargo.toml -# version: 1 +# version: 2 [package] name = "ksp-logging-lib" @@ -10,6 +10,7 @@ repository.workspace = true [dependencies] ksp-core-lib = { path = "../ksp-core-lib" } tracing.workspace = true +tracing-subscriber.workspace = true [lints] workspace = true diff --git a/crates/ksp-logging-lib/src/error.rs b/crates/ksp-logging-lib/src/error.rs index caaaa2e..781b999 100644 --- a/crates/ksp-logging-lib/src/error.rs +++ b/crates/ksp-logging-lib/src/error.rs @@ -1,5 +1,9 @@ // file: crates/ksp-logging-lib/src/error.rs -// version: 1 +// version: 2 /// Error code used when runtime logging settings are invalid. pub const ERROR_CODE_INVALID_SETTINGS: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "invalid_settings"); +/// Error code used when a global logging subscriber is already installed. +pub const ERROR_CODE_ALREADY_INITIALIZED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "already_initialized"); +/// Error code used when a hot reload cannot replace the active runtime layers. +pub const ERROR_CODE_RELOAD_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "reload_failed"); diff --git a/crates/ksp-logging-lib/src/lib.rs b/crates/ksp-logging-lib/src/lib.rs index 89a1732..dd42616 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: 1 +// version: 2 #![warn(missing_docs)] #![deny(unreachable_pub)] #![forbid(unsafe_code)] @@ -7,15 +7,27 @@ //! 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. Runtime subscriber initialization, filtering, outputs and hot reload are added by the following `0.1.2` prereleases. +//! `tracing` stack. `0.1.2-pre.003` installs the single global subscriber, applies KSP takeover filtering, provides the initial console layer and establishes +//! hot reload. Non-blocking writers, file output, ANSI stripping and writer guards are completed by the following prerelease. mod error; mod macros; +mod runtime; mod settings; mod span; +/// Error code used when a global logging subscriber is already installed. +pub use self::error::ERROR_CODE_ALREADY_INITIALIZED; /// Error code used when runtime logging settings are invalid. pub use self::error::ERROR_CODE_INVALID_SETTINGS; +/// Error code used when a hot reload cannot replace the active runtime layers. +pub use self::error::ERROR_CODE_RELOAD_FAILED; +/// Guard owning the mutable runtime state of the installed KSP logging subscriber. +pub use self::runtime::LoggingGuard; +/// Installs the global KSP tracing subscriber. +pub use self::runtime::initialize; +/// Replaces the active KSP logging settings without reinstalling the global subscriber. +pub use self::runtime::reinitialize; /// Console stream selected for human-readable logs. pub use self::settings::ConsoleOutput; /// Runtime settings for the optional console output. diff --git a/crates/ksp-logging-lib/src/runtime.rs b/crates/ksp-logging-lib/src/runtime.rs new file mode 100644 index 0000000..9abfc65 --- /dev/null +++ b/crates/ksp-logging-lib/src/runtime.rs @@ -0,0 +1,126 @@ +// file: crates/ksp-logging-lib/src/runtime.rs +// version: 1 + +use tracing_subscriber::Layer; // rust-rules: trait-import +use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import + +/// Guard owning the mutable runtime state of the installed KSP logging subscriber. +pub struct LoggingGuard { + reload_handle: RuntimeReloadHandle, + settings: crate::LoggingSettings, +} + +impl LoggingGuard { + /// Returns the settings currently active in the KSP logging runtime. + #[must_use] + pub fn settings(&self) -> &crate::LoggingSettings { + return &self.settings; + } +} + +type BoxedRuntimeLayer = std::boxed::Box + std::marker::Send + std::marker::Sync + 'static>; +type RuntimeLayers = std::vec::Vec; +type RuntimeReloadHandle = tracing_subscriber::reload::Handle; + +/// Installs the global KSP tracing subscriber. +/// +/// This function may succeed only once for the lifetime of the process. The returned guard is then used by [`crate::reinitialize`] to replace the active +/// KSP logging configuration without installing a second global subscriber. +pub fn initialize(settings: &crate::LoggingSettings) -> ksp_core_lib::Result { + return prepare_runtime_layers(settings).and_then(|runtime_layers| -> ksp_core_lib::Result { + let (reload_layer, reload_handle) = tracing_subscriber::reload::Layer::new(runtime_layers); + let subscriber = tracing_subscriber::registry().with(reload_layer); + let install_result = tracing::subscriber::set_global_default(subscriber); + return match install_result { + std::result::Result::Ok(()) => std::result::Result::Ok(crate::LoggingGuard { reload_handle, settings: settings.clone() }), + std::result::Result::Err(error) => std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_ALREADY_INITIALIZED, "the global KSP tracing subscriber is already installed").with_source(error), + ), + }; + }); +} + +/// Replaces the active KSP logging settings without reinstalling the global subscriber. +/// +/// New runtime layers are fully prepared before the reload is attempted. If validation or preparation fails, the currently active configuration is left +/// unchanged. +pub fn reinitialize(guard: &mut crate::LoggingGuard, settings: &crate::LoggingSettings) -> ksp_core_lib::Result<()> { + return prepare_runtime_layers(settings).and_then(|runtime_layers| -> ksp_core_lib::Result<()> { + let reload_result = guard.reload_handle.reload(runtime_layers); + return match reload_result { + std::result::Result::Ok(()) => { + guard.settings = settings.clone(); + std::result::Result::Ok(()) + }, + std::result::Result::Err(error) => std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_RELOAD_FAILED, "unable to reload the KSP logging runtime").with_source(error), + ), + }; + }); +} + +fn prepare_runtime_layers(settings: &crate::LoggingSettings) -> ksp_core_lib::Result { + let validation_error = settings.validate().err(); + if let std::option::Option::Some(error) = validation_error { + return std::result::Result::Err(error); + } + if settings.file().is_some() { + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "file output is not available until the file backend is introduced") + .with_context("field", "file"), + ); + } + let mut layers = RuntimeLayers::new(); + if let std::option::Option::Some(console) = settings.console() { + layers.push(build_console_layer(console, settings)); + } + return std::result::Result::Ok(layers); +} + +fn build_console_layer(console: &crate::ConsoleSettings, settings: &crate::LoggingSettings) -> BoxedRuntimeLayer { + let writer = match console.output() { + crate::ConsoleOutput::Stdout => tracing_subscriber::fmt::writer::BoxMakeWriter::new(std::io::stdout), + crate::ConsoleOutput::Stderr => tracing_subscriber::fmt::writer::BoxMakeWriter::new(std::io::stderr), + }; + let layer = tracing_subscriber::fmt::layer() + .with_writer(writer) + .with_ansi(false) + .with_target(true) + .with_span_events(map_span_events(settings.span_events())) + .with_filter(build_target_filter(settings)) + .boxed(); + return layer; +} + +fn build_target_filter(settings: &crate::LoggingSettings) -> tracing_subscriber::filter::Targets { + let mut filter = tracing_subscriber::filter::Targets::new() + .with_default(tracing_subscriber::filter::LevelFilter::OFF) + .with_target("ksp-", map_filter_level(settings.default_filter())); + for target_filter in settings.target_filters() { + filter = filter.with_target(target_filter.target_prefix(), map_filter_level(target_filter.level())); + } + return filter; +} + +const fn map_filter_level(level: crate::LogFilterLevel) -> tracing_subscriber::filter::LevelFilter { + return match level { + crate::LogFilterLevel::Off => tracing_subscriber::filter::LevelFilter::OFF, + crate::LogFilterLevel::Error => tracing_subscriber::filter::LevelFilter::ERROR, + crate::LogFilterLevel::Warn => tracing_subscriber::filter::LevelFilter::WARN, + crate::LogFilterLevel::Info => tracing_subscriber::filter::LevelFilter::INFO, + crate::LogFilterLevel::Debug => tracing_subscriber::filter::LevelFilter::DEBUG, + crate::LogFilterLevel::Trace => tracing_subscriber::filter::LevelFilter::TRACE, + }; +} + +fn map_span_events(span_events: crate::SpanEvents) -> tracing_subscriber::fmt::format::FmtSpan { + return match span_events { + crate::SpanEvents::Off => tracing_subscriber::fmt::format::FmtSpan::NONE, + crate::SpanEvents::NewAndClose => tracing_subscriber::fmt::format::FmtSpan::NEW | tracing_subscriber::fmt::format::FmtSpan::CLOSE, + crate::SpanEvents::Full => tracing_subscriber::fmt::format::FmtSpan::FULL, + }; +} + +#[cfg(test)] +#[path = "../unit_tests/runtime.rs"] +mod tests; diff --git a/crates/ksp-logging-lib/tests/public_api.rs b/crates/ksp-logging-lib/tests/public_api.rs index 4a0b270..24166eb 100644 --- a/crates/ksp-logging-lib/tests/public_api.rs +++ b/crates/ksp-logging-lib/tests/public_api.rs @@ -1,5 +1,5 @@ // file: crates/ksp-logging-lib/tests/public_api.rs -// version: 2 +// version: 3 //! Integration tests for the public crate-root surface of `ksp-logging-lib`. @@ -52,3 +52,11 @@ fn all_span_levels_are_usable() { let _debug = ksp_logging_lib::debug_span!(target: TEST_TARGET, "debug_span"); let _trace = ksp_logging_lib::trace_span!(target: TEST_TARGET, "trace_span"); } + +#[test] +fn public_runtime_surface_is_addressable_without_installing_it() { + let _initialize = ksp_logging_lib::initialize; + let _reinitialize = ksp_logging_lib::reinitialize; + let _already_initialized = ksp_logging_lib::ERROR_CODE_ALREADY_INITIALIZED; + let _reload_failed = ksp_logging_lib::ERROR_CODE_RELOAD_FAILED; +} diff --git a/crates/ksp-logging-lib/tests/runtime.rs b/crates/ksp-logging-lib/tests/runtime.rs new file mode 100644 index 0000000..aa19810 --- /dev/null +++ b/crates/ksp-logging-lib/tests/runtime.rs @@ -0,0 +1,75 @@ +// file: crates/ksp-logging-lib/tests/runtime.rs +// version: 1 + +//! Integration tests for global initialization, takeover filtering and hot reload. + +const LOGGING_TARGET: &str = "ksp-logging-lib"; +const OTHER_KSP_TARGET: &str = "ksp-store-lib"; +const EXTERNAL_TARGET: &str = "sqlx"; + +fn logging_trace_enabled() -> bool { + return tracing::enabled!(target: LOGGING_TARGET, tracing::Level::TRACE); +} + +fn other_ksp_info_enabled() -> bool { + return tracing::enabled!(target: OTHER_KSP_TARGET, tracing::Level::INFO); +} + +fn other_ksp_debug_enabled() -> bool { + return tracing::enabled!(target: OTHER_KSP_TARGET, tracing::Level::DEBUG); +} + +fn external_error_enabled() -> bool { + return tracing::enabled!(target: EXTERNAL_TARGET, tracing::Level::ERROR); +} + +#[test] +fn global_runtime_supports_takeover_hot_reload_and_single_initialization() { + let disabled = ksp_logging_lib::LoggingSettings::new( + ksp_logging_lib::LogFilterLevel::Info, + ksp_logging_lib::SpanEvents::Off, + std::option::Option::None, + std::option::Option::None, + ); + let initialize_result = ksp_logging_lib::initialize(&disabled); + assert!(initialize_result.is_ok()); + let mut guard = match initialize_result { + std::result::Result::Ok(guard) => guard, + std::result::Result::Err(_) => return, + }; + assert!(!logging_trace_enabled()); + assert!(!other_ksp_info_enabled()); + assert!(!external_error_enabled()); + let enabled = ksp_logging_lib::LoggingSettings::new( + ksp_logging_lib::LogFilterLevel::Info, + ksp_logging_lib::SpanEvents::NewAndClose, + std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()), + std::option::Option::None, + ) + .with_target_filter(ksp_logging_lib::TargetFilter::new(LOGGING_TARGET, ksp_logging_lib::LogFilterLevel::Trace)); + let reload_result = ksp_logging_lib::reinitialize(&mut guard, &enabled); + assert!(reload_result.is_ok()); + assert_eq!(guard.settings(), &enabled); + assert!(logging_trace_enabled()); + assert!(other_ksp_info_enabled()); + assert!(!other_ksp_debug_enabled()); + assert!(!external_error_enabled()); + let unsupported_file = ksp_logging_lib::LoggingSettings::new( + ksp_logging_lib::LogFilterLevel::Error, + ksp_logging_lib::SpanEvents::Full, + std::option::Option::None, + std::option::Option::Some(ksp_logging_lib::FileSettings::new("logs", "ksp", ksp_logging_lib::FileRotation::Daily)), + ); + let failed_reload = ksp_logging_lib::reinitialize(&mut guard, &unsupported_file); + assert!(failed_reload.is_err()); + assert_eq!(guard.settings(), &enabled); + assert!(logging_trace_enabled()); + assert!(!external_error_enabled()); + let second_initialize = ksp_logging_lib::initialize(&disabled); + assert!(second_initialize.is_err()); + let error = match second_initialize { + std::result::Result::Ok(_) => return, + std::result::Result::Err(error) => error, + }; + assert_eq!(error.code(), ksp_logging_lib::ERROR_CODE_ALREADY_INITIALIZED); +} diff --git a/crates/ksp-logging-lib/unit_tests/runtime.rs b/crates/ksp-logging-lib/unit_tests/runtime.rs new file mode 100644 index 0000000..34fa2bc --- /dev/null +++ b/crates/ksp-logging-lib/unit_tests/runtime.rs @@ -0,0 +1,56 @@ +// file: crates/ksp-logging-lib/unit_tests/runtime.rs +// version: 1 + +#[test] +fn level_mapping_covers_all_ksp_levels() { + assert_eq!(super::map_filter_level(crate::LogFilterLevel::Off), tracing_subscriber::filter::LevelFilter::OFF); + assert_eq!(super::map_filter_level(crate::LogFilterLevel::Error), tracing_subscriber::filter::LevelFilter::ERROR); + assert_eq!(super::map_filter_level(crate::LogFilterLevel::Warn), tracing_subscriber::filter::LevelFilter::WARN); + assert_eq!(super::map_filter_level(crate::LogFilterLevel::Info), tracing_subscriber::filter::LevelFilter::INFO); + assert_eq!(super::map_filter_level(crate::LogFilterLevel::Debug), tracing_subscriber::filter::LevelFilter::DEBUG); + assert_eq!(super::map_filter_level(crate::LogFilterLevel::Trace), tracing_subscriber::filter::LevelFilter::TRACE); +} + +#[test] +fn takeover_filter_silences_external_targets_and_applies_ksp_overrides() { + let settings = crate::LoggingSettings::new( + crate::LogFilterLevel::Info, + crate::SpanEvents::Off, + std::option::Option::Some(crate::ConsoleSettings::stdout()), + std::option::Option::None, + ) + .with_target_filter(crate::TargetFilter::new("ksp-logging-lib", crate::LogFilterLevel::Trace)); + let filter = super::build_target_filter(&settings); + assert!(filter.would_enable("ksp-store-lib", &tracing::Level::INFO)); + assert!(!filter.would_enable("ksp-store-lib", &tracing::Level::DEBUG)); + assert!(filter.would_enable("ksp-logging-lib", &tracing::Level::TRACE)); + assert!(!filter.would_enable("sqlx", &tracing::Level::ERROR)); + assert!(!filter.would_enable("hyper", &tracing::Level::ERROR)); +} + +#[test] +fn span_event_mapping_supports_disabled_timing_and_full_lifecycle() { + assert_eq!(super::map_span_events(crate::SpanEvents::Off), tracing_subscriber::fmt::format::FmtSpan::NONE); + assert_eq!( + super::map_span_events(crate::SpanEvents::NewAndClose), + tracing_subscriber::fmt::format::FmtSpan::NEW | tracing_subscriber::fmt::format::FmtSpan::CLOSE, + ); + assert_eq!(super::map_span_events(crate::SpanEvents::Full), tracing_subscriber::fmt::format::FmtSpan::FULL); +} + +#[test] +fn file_output_is_rejected_until_file_runtime_is_introduced() { + let settings = crate::LoggingSettings::new( + crate::LogFilterLevel::Info, + crate::SpanEvents::Off, + std::option::Option::None, + std::option::Option::Some(crate::FileSettings::new("logs", "ksp", crate::FileRotation::Daily)), + ); + let result = super::prepare_runtime_layers(&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); +} diff --git a/deltas/0.1.2/pre.003.md b/deltas/0.1.2/pre.003.md new file mode 100644 index 0000000..20f4889 --- /dev/null +++ b/deltas/0.1.2/pre.003.md @@ -0,0 +1,305 @@ + + + +# Delta 0.1.2-pre.003 + +## Base requise + +Livraison précédente validée : + +```text +0.1.2-pre.002-fix.001 +``` + +La base de développement validée porte : + +```text +workspace.package.version = "0.1.2-pre.2.fix.1" +Cargo.toml header version = 28 +``` + +Les validations remontées avant l'ouverture de cette tranche sont propres : + +```bash +cargo fmt --all +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test --workspace +``` + +## Objectif + +Introduire le runtime subscriber de Logging sans encore ouvrir le backend fichier/non bloquant : + +- ajouter `tracing-subscriber` avec la feature minimale `fmt` ; +- installer une seule fois le subscriber global KSP ; +- appliquer le takeover KSP et rendre silencieux les targets externes par défaut ; +- mapper `LogFilterLevel` vers `LevelFilter` ; +- appliquer un niveau KSP global puis les overrides par préfixe de target ; +- introduire une première couche console stdout/stderr ; +- intégrer les événements de lifecycle des spans `Off`, `NewAndClose` et `Full` ; +- introduire `LoggingGuard`, `initialize()` et `reinitialize()` ; +- permettre un démarrage sans sink puis une activation à chaud ; +- vérifier le hot reload sans second subscriber global. + +La console reste volontairement synchrone dans cette tranche intermédiaire. `pre.004` la remplacera par un writer `tracing-appender` non bloquant et ajoutera fichier, guards, dropped-line counters et stripping ANSI avant toute stabilisation de `0.1.2`. + +## Version Cargo + +`workspace.package.version` passe de : + +```text +0.1.2-pre.2.fix.1 +``` + +à : + +```text +0.1.2-pre.3 +``` + +L'identifiant de livraison est : + +```text +0.1.2-pre.003 +``` + +Le header du `Cargo.toml` racine passe de version 28 à 29. + +## Dépendance `tracing-subscriber` + +L'audit du 2026-08-14 confirme `tracing-subscriber 0.3.23` dans la génération `^0.3`. + +La dépendance est centralisée sous `[workspace.dependencies]` : + +```toml +tracing-subscriber = { version = "^0.3", default-features = false, features = ["fmt"] } +``` + +`ksp-logging-lib` la consomme avec : + +```toml +tracing-subscriber.workspace = true +``` + +La feature `fmt` fournit le formatter et entraîne les capacités `registry`/`std` nécessaires à la composition retenue. Ne sont pas activés par anticipation : + +- `env-filter` ; +- `ansi` ; +- `tracing-log` ; +- `json` ; +- `time` ; +- `chrono` ; +- `parking_lot`. + +`tracing-appender` reste absent jusqu'à `pre.004`. + +## Takeover et filtering + +Le runtime utilise `tracing_subscriber::filter::Targets`. + +La construction est conceptuellement : + +```text +default unmatched targets = OFF +ksp-* = LoggingSettings.default_filter +target overrides = TargetFilter entries +``` + +Conséquences : + +- un événement `sqlx`, `hyper`, `rustls` ou autre target externe reste silencieux même à `ERROR` tant qu'aucune couche KSP ne le réémet explicitement ; +- les crates KSP utilisent leur nom Cargo comme target ; +- `ksp-logging-lib`, `ksp-store-lib`, etc. suivent le niveau global KSP ; +- un `TargetFilter` plus spécifique peut relever ou abaisser le niveau d'une crate KSP donnée ; +- aucune chaîne `RUST_LOG` ou `EnvFilter` n'est introduite. + +## Console initiale + +`ConsoleSettings::stdout()` et `ConsoleSettings::stderr()` construisent une couche `fmt` avec : + +- target affiché ; +- ANSI explicitement désactivé ; +- lifecycle de span selon `SpanEvents` ; +- filtering KSP `Targets`. + +Cette couche utilise encore directement `std::io::stdout` / `std::io::stderr`. Ce writer synchrone est uniquement la fondation de `pre.003`; il n'est pas le contrat final de la release. + +## Spans runtime + +Le mapping retenu est : + +```text +SpanEvents::Off -> FmtSpan::NONE +SpanEvents::NewAndClose -> FmtSpan::NEW | FmtSpan::CLOSE +SpanEvents::Full -> FmtSpan::FULL +``` + +`NewAndClose` active ainsi la surface nécessaire aux diagnostics de début/fin et de temps busy/idle fournis par le formatter sans obliger les consumers à utiliser directement `tracing-subscriber`. + +## Subscriber global + +La nouvelle API publique est : + +```text +ksp_logging_lib::LoggingGuard +ksp_logging_lib::initialize(&LoggingSettings) -> Result +ksp_logging_lib::reinitialize(&mut LoggingGuard, &LoggingSettings) -> Result<()> +``` + +`initialize()` : + +1. valide/prépare les layers ; +2. crée une unique infrastructure `reload::Layer` ; +3. installe le subscriber global avec l'API fallible `tracing::subscriber::set_global_default` ; +4. retourne un `LoggingGuard` possédant le handle de reload et les settings actifs. + +Une seconde installation globale retourne : + +```text +logging.already_initialized +``` + +La cause `SetGlobalDefaultError` est conservée comme `source` Core. + +## Hot reload + +La composition interne retenue est : + +```text +Registry + -> reload::Layer + -> Vec + Send + Sync>> +``` + +Le `Vec` peut être vide. Cela permet : + +```text +initialize(no sink) + -> subscriber global installé mais silencieux + +reinitialize(console enabled) + -> console activée sans second subscriber global +``` + +Le choix d'un `Vec` de layers boxed prépare directement `pre.004`, qui pourra ajouter ou retirer console/fichier sans changer la surface publique de reload. + +`reinitialize()` prépare d'abord complètement la nouvelle représentation. Une erreur de validation/préparation retourne avant le swap et conserve : + +- les settings actifs du `LoggingGuard` ; +- les layers actuellement installés ; +- le comportement de filtering en cours. + +Une erreur effective du handle `reload` retourne : + +```text +logging.reload_failed +``` + +et conserve sa cause externe via le contrat `source` Core. + +## File settings pendant `pre.003` + +`FileSettings` reste dans la surface publique définie par `pre.002`, mais le backend fichier n'est pas encore construit dans cette tranche. + +`initialize()` / `reinitialize()` refusent donc temporairement une configuration avec `file = Some(...)` avec `logging.invalid_settings` et contexte `field = file` au lieu d'ignorer silencieusement la demande. + +Cette restriction transitoire disparaîtra lorsque le backend fichier réel sera introduit en `pre.004`. + +## Erreurs ajoutées + +```text +logging.already_initialized +logging.reload_failed +``` + +Elles s'ajoutent à : + +```text +logging.invalid_settings +``` + +Aucune connaissance Logging n'est ajoutée à Core. + +## Tests ajoutés + +### Unitaires runtime + +- mapping complet des niveaux KSP ; +- silence des targets externes ; +- default KSP `Info` ; +- override `ksp-logging-lib = Trace` ; +- mapping des événements de span ; +- rejet temporaire du backend fichier avant `pre.004`. + +### Intégration runtime global + +Un seul test global dans sa crate de test dédiée vérifie : + +1. `initialize()` avec aucun sink ; +2. absence d'admission des callsites tant que Logging est désactivé ; +3. `reinitialize()` avec console active ; +4. activation `Trace` de `ksp-logging-lib` par override ; +5. maintien de `ksp-store-lib` à `Info` ; +6. maintien de `sqlx` à `Off` même pour `Error` ; +7. échec d'un reload demandant le backend fichier non encore disponible ; +8. conservation des anciens settings/filtering après cet échec ; +9. refus d'un deuxième `initialize()`. + +Le changement de filtering est observé via des fonctions contenant des callsites `tracing::enabled!` stables, afin de vérifier que le reload invalide correctement l'intérêt mis en cache. + +## Fichiers ajoutés + +- `crates/ksp-logging-lib/src/runtime.rs` +- `crates/ksp-logging-lib/unit_tests/runtime.rs` +- `crates/ksp-logging-lib/tests/runtime.rs` +- `deltas/0.1.2/pre.003.md` + +## Fichiers modifiés + +- `Cargo.toml` +- `crates/ksp-logging-lib/Cargo.toml` +- `crates/ksp-logging-lib/src/error.rs` +- `crates/ksp-logging-lib/src/lib.rs` +- `crates/ksp-logging-lib/tests/public_api.rs` +- `docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md` + +## Fichiers supprimés + +Aucun. + +## Validations exécutées pendant la préparation + +- revérification documentaire de `tracing-subscriber 0.3.23` et de ses features ; +- contrôle TOML des manifests ; +- contrôle des headers `file:` / `version:` ; +- contrôle de la centralisation de `tracing-subscriber` sous `[workspace.dependencies]` ; +- contrôle que `tracing-appender` reste absent ; +- contrôle que le code production ajouté n'utilise ni `unwrap`, ni `expect`, ni `panic`, ni opérateur `?`, ni `unsafe` ; +- contrôle que les usages directs de la stack tracing restent dans `ksp-logging-lib` ; +- contrôle du contenu du delta contre la base reconstruite `0.1.2-pre.2.fix.1`. + +## Validations à exécuter dans le workspace + +```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 non exécutable dans l'environnement de préparation n'est déclarée réussie. + +## Suite + +Après validation de `pre.003`, passer à `0.1.2-pre.004` : + +- `tracing-appender` ; +- console non bloquante ; +- fichier Never/Hourly/Daily ; +- `WorkerGuard` / `ErrorCounter` ; +- stripping ANSI fichier ; +- hot reload des sinks non bloquants et de leurs guards. diff --git a/docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md b/docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md index 6c61f74..2c9f4ad 100644 --- a/docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md +++ b/docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md @@ -1,13 +1,13 @@ - + # Plan KSP 0.1.2 — Logging foundation ## Statut -Plan actif de `0.1.2`, établi par `0.1.2-pre.001`, corrigé par `0.1.2-pre.001-fix.001` puis concrétisé par la première surface fonctionnelle de `0.1.2-pre.002`. +Plan actif de `0.1.2`, établi par `0.1.2-pre.001`, corrigé par `0.1.2-pre.001-fix.001`, concrétisé par la façade de `0.1.2-pre.002` puis étendu au runtime subscriber par `0.1.2-pre.003`. -`pre.002` crée `ksp-logging-lib`, ses settings runtime, sa façade d'événements/spans et son instrumentation async. Le subscriber runtime, le takeover effectif, les sorties et le hot reload restent réservés aux tranches suivantes. +`pre.002-fix.001` a été validé dans l'environnement de développement avec `cargo fmt`, `cargo check`, `cargo clippy --workspace --all-targets` et `cargo test --workspace` propres sur la version Cargo `0.1.2-pre.2.fix.1`. `pre.003` ajoute le subscriber global, le takeover/filtering KSP, la console initiale et le hot reload des layers. Les writers non bloquants, le fichier, les guards et le stripping ANSI restent réservés à `pre.004`. ## Base auditée @@ -147,9 +147,15 @@ tracing-appender = { version = "^0.2", default-features = false } ### Politique d'ajout au manifest -`pre.002` ajoute uniquement `tracing` sous `[workspace.dependencies]`, car la façade événements/spans et l'instrumentation async l'utilisent réellement. `crates/ksp-logging-lib/Cargo.toml` l'hérite avec `tracing.workspace = true`. +`pre.002` ajoute `tracing` sous `[workspace.dependencies]`, car la façade événements/spans et l'instrumentation async l'utilisent réellement. `crates/ksp-logging-lib/Cargo.toml` l'hérite avec `tracing.workspace = true`. -`tracing-subscriber` et `tracing-appender` restent absents jusqu'aux prereleases qui utilisent effectivement leurs APIs. +`pre.003` revérifie `tracing-subscriber 0.3.23` et ajoute : + +```toml +tracing-subscriber = { version = "^0.3", default-features = false, features = ["fmt"] } +``` + +La feature `fmt` apporte `registry` et `std`, nécessaires à la composition des layers, `Targets`, `reload` et au formatter console. `env-filter`, `ansi`, `tracing-log`, `json`, `time` et les autres features optionnelles ne sont pas activées. `tracing-appender` reste absent jusqu'à `pre.004`, où il sera réellement consommé. Après chaque ajout réel : @@ -920,15 +926,24 @@ Objectifs : ### `0.1.2-pre.003` — subscriber + takeover + console + reload foundation -Objectifs : +Statut : implémenté dans la tranche `pre.003`, sous réserve des validations Cargo à exécuter dans l'environnement de développement. -- revérifier puis ajouter `tracing-subscriber` ; -- implémenter le mapping `LogFilterLevel` ; -- implémenter le silence externe + default KSP + overrides ; -- implémenter la couche console ; -- installer le subscriber global avec API fallible ; -- introduire l'infrastructure reloadable et les premiers tests de hot reload ; -- intégrer les événements lifecycle de spans `Off/NewAndClose/Full`. +Réalisé : + +- `tracing-subscriber` ajouté avec uniquement la feature `fmt` ; +- mapping complet `LogFilterLevel -> LevelFilter` ; +- `Targets` configuré avec default externe `OFF`, préfixe KSP `ksp-` au niveau global demandé et overrides par target prefix ; +- console stdout/stderr initiale avec ANSI explicitement désactivé ; +- subscriber global installé par `initialize()` avec API fallible et erreur `logging.already_initialized` ; +- `LoggingGuard` public possédant le handle de reload et les settings actifs ; +- `reinitialize(&mut LoggingGuard, &LoggingSettings)` remplaçant le `Vec` de layers sans réinstaller le subscriber global ; +- configuration sans sink supportée à l'initialisation afin de permettre une activation ultérieure par hot reload ; +- mapping `SpanEvents::Off/NewAndClose/Full` vers `FmtSpan::NONE`, `NEW | CLOSE` et `FULL` ; +- tests du takeover, des overrides, du démarrage sans sink, de l'activation à chaud, du refus d'un second `initialize()` et de la persistance du silence des targets externes. + +La console de `pre.003` utilise encore directement `stdout`/`stderr` comme writer synchrone. C'est un état transitoire volontaire : `pre.004` remplace ces writers par `tracing-appender::non_blocking`, introduit les `WorkerGuard`/`ErrorCounter`, puis ajoute le fichier et le stripping ANSI. La release stable `0.1.2` ne sera pas déclarée conforme tant que ce remplacement n'est pas terminé. + +Le runtime reloadable est un `Vec>>` placé derrière une unique `reload::Layer`. Cette composition permet de changer à chaud le nombre et le type des sinks tout en conservant un seul subscriber global et prépare directement l'ajout du layer fichier de `pre.004`. ### `0.1.2-pre.004` — non-blocking console/fichier + guards + ANSI + reload sinks @@ -1016,17 +1031,19 @@ La release peut être stabilisée lorsque : - les validations workspace et audits présents sont propres ; - la documentation finale et le prompt `0.1.3` sont prêts. -## Questions ouvertes après `pre.002` +## Questions ouvertes après `pre.003` Les deux questions d'API propres à `pre.002` sont résolues : 1. les macros KSP délèguent aux macros `tracing` au point d'appel via un bridge interne caché et exigent un `target:` explicite ; 2. la surface span publique est `Span::in_scope(...)` pour le synchrone et `instrument(span, future)` pour l'async, avec type de future retourné opaque. -Restent à confirmer par les prereleases runtime sans remettre en cause ce contrat : +La composition de reload est désormais fixée pour cette release à un `Vec` de layers boxed derrière une `reload::Layer`, ce qui autorise l'activation/désactivation des sinks et le remplacement de leurs paramètres sans second subscriber global. -1. la composition interne la moins coûteuse pour le hot reload (reload de filters/layers ciblés ou routing dynamique KSP), tout en conservant un seul subscriber global ; -2. l'API publique exacte d'observation des dropped lines ; -3. le détail visuel exact du formatter humain, sans transformer sa ponctuation en contrat public. +Restent à confirmer par les prereleases suivantes sans remettre en cause ce contrat : -La prochaine action après validation de `pre.002` est `0.1.2-pre.003` : subscriber, takeover, filtering, console initiale et fondation du hot reload. +1. l'API publique exacte d'observation des dropped lines ; +2. le détail visuel exact du formatter humain, sans transformer sa ponctuation en contrat public ; +3. le comportement de flush/rotation et le swap transactionnel des `WorkerGuard` lorsque les sinks non bloquants seront introduits. + +La prochaine action après validation de `pre.003` est `0.1.2-pre.004` : `tracing-appender`, console/fichier non bloquants, guards, rotation, stripping ANSI et reload des sinks.