From d55903b13e75f077bbeac7789ecad2c7f6134362 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Fri, 14 Aug 2026 19:37:37 +0200 Subject: [PATCH] v0.1.2-pre.004-fix.003 --- Cargo.toml | 4 +- crates/ksp-logging-lib/src/runtime.rs | 12 +- crates/ksp-logging-lib/unit_tests/runtime.rs | 6 +- deltas/0.1.2/pre.004-fix.003.md | 131 ++++++++++++++++++ .../004-V0_1_2_LOGGING_FOUNDATION_PLAN.md | 8 +- 5 files changed, 147 insertions(+), 14 deletions(-) create mode 100644 deltas/0.1.2/pre.004-fix.003.md diff --git a/Cargo.toml b/Cargo.toml index 2f47005..c3a0ec6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 33 +# version: 34 [workspace] resolver = "3" members = ["crates/ksp-core-lib", "crates/ksp-logging-lib"] [workspace.package] -version = "0.1.2-pre.4.fix.2" +version = "0.1.2-pre.4.fix.3" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-logging-lib/src/runtime.rs b/crates/ksp-logging-lib/src/runtime.rs index b9502e3..b2b1ac8 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: 5 +// version: 6 use tracing_subscriber::Layer; // rust-rules: trait-import use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import @@ -178,17 +178,19 @@ fn prepare_runtime(settings: &crate::LoggingSettings) -> ksp_core_lib::Result PreparedOutput { return build_console_output(console, settings); }); - let mut layers = RuntimeLayers::new(); - layers.push(std::boxed::Box::new(build_target_filter(settings))); + let mut output_layers = RuntimeLayers::new(); let mut outputs = RuntimeOutputs::default(); if let std::option::Option::Some(console) = prepared_console { - layers.push(console.layer); + output_layers.push(console.layer); outputs.console = std::option::Option::Some(console.output); } if let std::option::Option::Some(file) = prepared_file { - layers.push(file.layer); + output_layers.push(file.layer); outputs.file = std::option::Option::Some(file.output); } + let takeover_layer = build_target_filter(settings).and_then(output_layers).boxed(); + let mut layers = RuntimeLayers::new(); + layers.push(takeover_layer); return std::result::Result::Ok(PreparedRuntime { layers, outputs }); } diff --git a/crates/ksp-logging-lib/unit_tests/runtime.rs b/crates/ksp-logging-lib/unit_tests/runtime.rs index eeeaaea..dddfe9e 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: 3 +// version: 4 #[test] fn level_mapping_covers_all_ksp_levels() { @@ -60,7 +60,7 @@ fn disabled_runtime_has_no_layers_or_outputs() { } #[test] -fn console_runtime_keeps_takeover_filter_separate_from_formatter_and_owns_guard() { +fn console_runtime_composes_takeover_filter_before_formatter_and_owns_guard() { let settings = crate::LoggingSettings::new( crate::LogFilterLevel::Info, crate::SpanEvents::Off, @@ -73,7 +73,7 @@ fn console_runtime_keeps_takeover_filter_separate_from_formatter_and_owns_guard( std::result::Result::Ok(prepared) => prepared, std::result::Result::Err(_) => return, }; - assert_eq!(prepared.layers.len(), 2); + assert_eq!(prepared.layers.len(), 1); assert!(prepared.outputs.console.is_some()); assert!(prepared.outputs.file.is_none()); assert_eq!(prepared.outputs.dropped_lines(), crate::DroppedLines::zero()); diff --git a/deltas/0.1.2/pre.004-fix.003.md b/deltas/0.1.2/pre.004-fix.003.md new file mode 100644 index 0000000..b480dc1 --- /dev/null +++ b/deltas/0.1.2/pre.004-fix.003.md @@ -0,0 +1,131 @@ + + + +# Delta 0.1.2-pre.004-fix.003 + +## Base requise + +Livraison précédente : + +```text +0.1.2-pre.004-fix.002 +``` + +La base porte : + +```text +workspace.package.version = "0.1.2-pre.4.fix.2" +Cargo.toml header version = 33 +``` + +## Validation remontée + +Après application de `pre.004-fix.002` : + +```text +cargo fmt --all OK +cargo check --workspace OK +cargo clippy --workspace --all-targets OK +cargo test --workspace ECHEC +``` + +Le marqueur fichier précédemment absent est désormais correctement persisté et les assertions de présence du target, du callsite et d'absence d'ANSI passent. Le test runtime global échoue plus loin sur : + +```text +assertion failed: !file_text.contains("external marker must remain silent") +``` + +Le défaut restant concerne donc exclusivement le takeover : un événement `tracing` émis directement avec le target externe `sqlx` atteint encore le sink fichier alors que la politique KSP exige son silence total. + +## Cause réelle + +`pre.003-fix.001` avait évité le panic `Filtered`/`FilterId` en plaçant `Targets` comme layer global distinct dans le même : + +```text +Vec>> +``` + +que les formatters. + +Cette composition n'est toutefois pas correcte pour le filtrage global au niveau des callsites. L'implémentation `Layer` de `Vec` agrège `register_callsite` en conservant l'intérêt le plus élevé retourné par ses enfants. Un `fmt::Layer` intéressé par le callsite peut donc produire un intérêt actif alors que `Targets` retourne `Interest::never()` pour un target externe. + +Lorsque le callsite est enregistré comme toujours actif, `enabled()` n'est ensuite pas consulté à chaque émission. Le `Targets` frère du formatter ne peut donc plus bloquer l'événement externe. + +Le test avec `sqlx` expose précisément cette fuite. + +## Correction + +Les layers de sortie sont d'abord construits dans un `Vec` : + +```text +outputs +├── console fmt layer, si actif +└── file fmt layer, si actif +``` + +Le takeover est ensuite composé **devant tout ce groupe** : + +```text +Targets + .and_then(outputs) +``` + +et ce composite unique devient l'élément du `Vec` reloadable : + +```text +reload::Layer +└── Vec + └── Targets -> output layers +``` + +Cette forme rétablit la sémantique de filtre global : un `Interest::never()` produit par `Targets` court-circuite le groupe de sinks avant leur formatter. + +Elle conserve simultanément les propriétés requises : + +- aucun `Layer::with_filter` n'est utilisé sur un layer remplacé à chaud ; +- aucun `Filtered` et donc aucun `FilterId` reloadable n'est introduit ; +- console et fichier restent activables/désactivables dynamiquement ; +- `Handle::modify` continue de récupérer l'ancien composite avant destruction de ses `WorkerGuard` ; +- la sanitization console et le stripping ANSI fichier de `fix.002` restent inchangés ; +- l'API publique reste inchangée. + +Le test d'intégration runtime conserve son assertion directe sur un événement `tracing::error!` de target `sqlx`. Il reste donc le test de non-régression du takeover effectif, au-delà du test unitaire de `Targets::would_enable`. + +## Version technique + +Ce correctif modifie du Rust. La version workspace devient : + +```text +workspace.package.version = "0.1.2-pre.4.fix.3" +``` + +et l'en-tête du `Cargo.toml` racine devient : + +```text +# version: 34 +``` + +## Fichiers du delta + +```text +Cargo.toml +crates/ksp-logging-lib/src/runtime.rs +crates/ksp-logging-lib/unit_tests/runtime.rs +docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md +deltas/0.1.2/pre.004-fix.003.md +``` + +## Validations à exécuter + +```bash +cargo fmt --all +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test --workspace +``` + +Aucune dépendance n'est modifiée par ce fix. Après validation propre, la tranche suivante reste : + +```text +0.1.2-pre.005 — intégration + concurrence + saturation + audits +``` 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 a137e0c..ba92089 100644 --- a/docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md +++ b/docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md @@ -1,5 +1,5 @@ - + # Plan KSP 0.1.2 — Logging foundation @@ -7,7 +7,7 @@ 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`, étendu au runtime subscriber par `0.1.2-pre.003`/`pre.003-fix.001`, puis complété par les sorties non bloquantes de `0.1.2-pre.004`. `pre.004-fix.001` a rendu explicite l'ordre de retrait des layers et des `WorkerGuard`; `pre.004-fix.002` corrige ensuite l'interaction entre la sanitization ANSI native de `tracing-subscriber 0.3.23` et le stripping fichier KSP. -`pre.003-fix.001` a été validé dans l'environnement de développement avec `cargo fmt --all`, `cargo check --workspace`, `cargo clippy --workspace --all-targets` et `cargo test --workspace` propres sur la version Cargo `0.1.2-pre.3.fix.1`. `pre.004` ajoute `tracing-appender`, remplace la console synchrone par un writer non bloquant, introduit le fichier `Never/Hourly/Daily`, possède les `WorkerGuard`, expose des compteurs cumulés de dropped lines, déporte le stripping ANSI côté worker fichier et conserve le hot reload transactionnel des sinks. `pre.004-fix.001` a rendu explicite le retrait des anciens layers avant leurs `WorkerGuard`, mais la validation utilisateur après `cargo clean` a reproduit exactement le même échec du marqueur fichier. Cette seconde validation a invalidé l'hypothèse selon laquelle ce défaut précis provenait du drain. La cause réelle est la sanitization ANSI native de `tracing-subscriber 0.3.23`, activée par défaut dans `fmt::Layer` : elle transforme les séquences de contrôle présentes dans les valeurs avant qu'elles n'atteignent le writer. `pre.004-fix.002` la conserve pour la console mais la désactive sur le formatter fichier afin que le `StripAnsiWriter` KSP, placé côté worker, reçoive les séquences ANSI originales et les supprime réellement. Le lifecycle explicite de `fix.001` est conservé car il reste correct pour le retrait des sinks non bloquants. +`pre.003-fix.001` a été validé dans l'environnement de développement avec `cargo fmt --all`, `cargo check --workspace`, `cargo clippy --workspace --all-targets` et `cargo test --workspace` propres sur la version Cargo `0.1.2-pre.3.fix.1`. `pre.004` ajoute `tracing-appender`, remplace la console synchrone par un writer non bloquant, introduit le fichier `Never/Hourly/Daily`, possède les `WorkerGuard`, expose des compteurs cumulés de dropped lines, déporte le stripping ANSI côté worker fichier et conserve le hot reload transactionnel des sinks. `pre.004-fix.001` a rendu explicite le retrait des anciens layers avant leurs `WorkerGuard`, mais la validation utilisateur après `cargo clean` a reproduit exactement le même échec du marqueur fichier. Cette seconde validation a invalidé l'hypothèse selon laquelle ce défaut précis provenait du drain. La cause réelle est la sanitization ANSI native de `tracing-subscriber 0.3.23`, activée par défaut dans `fmt::Layer` : elle transforme les séquences de contrôle présentes dans les valeurs avant qu'elles n'atteignent le writer. `pre.004-fix.002` la conserve pour la console mais la désactive sur le formatter fichier afin que le `StripAnsiWriter` KSP, placé côté worker, reçoive les séquences ANSI originales et les supprime réellement. Le lifecycle explicite de `fix.001` est conservé car il reste correct pour le retrait des sinks non bloquants. La validation de `pre.004-fix.002` confirme ensuite que l'écriture fichier et le stripping ANSI fonctionnent, mais révèle que le target externe `sqlx` est encore persisté. La cause est la composition du filtre global comme enfant frère des formatters dans un `Vec` : le `Vec` agrège l'intérêt de callsite en conservant l'intérêt le plus élevé de ses enfants, de sorte qu'un formatter intéressé peut masquer le `Interest::never()` du `Targets`. `pre.004-fix.003` compose donc `Targets` devant le `Vec` des sinks avec `Layer::and_then`; le filtre redevient global pour tout le groupe de sorties sans utiliser `with_filter`/`Filtered`. ## Base auditée @@ -960,7 +960,7 @@ Réalisé : 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 reste un `Vec>>` placé derrière une unique `reload::Layer`. En revanche, les sinks reloadables ne doivent pas encapsuler leur `Targets` via `Layer::with_filter`, car cela crée un `Filtered` dont le `FilterId` est enregistré lors de son attachement initial au subscriber et ne peut pas être remplacé directement avec `Handle::reload`. `pre.003-fix.001` place donc `Targets` comme layer de filtrage global distinct dans le même `Vec`, à côté du layer `fmt` non filtré. Le `Vec` entier reste remplaçable à chaud, mais aucun `Filtered` nouvellement construit n'a besoin d'être enregistré après l'installation globale du subscriber. Cette composition conserve le takeover externe, permet de changer à chaud le nombre et le type des sinks et prépare l'ajout du layer fichier de `pre.004`. +Le runtime reloadable reste fondé sur un `Vec>>` placé derrière une unique `reload::Layer`, mais `pre.004-fix.003` corrige la composition du takeover. Les sinks reloadables ne doivent pas encapsuler leur `Targets` via `Layer::with_filter`, car cela crée un `Filtered` dont le `FilterId` est enregistré lors de son attachement initial au subscriber et ne peut pas être remplacé directement avec `Handle::reload`. Cependant, `Targets` ne doit pas non plus être un enfant frère des `fmt` layers dans le même `Vec` : `Vec::register_callsite` conserve l'intérêt le plus élevé de ses enfants, et un formatter intéressé peut alors masquer le `Interest::never()` du filtre global. Le runtime construit désormais le `Vec` des sinks puis le compose derrière `Targets` avec `Layer::and_then`; ce composite unique est placé dans le `Vec` reloadable. Le filtre gouverne ainsi tout le groupe de sinks, aucun `Filtered` n'est créé, et le nombre/type de sorties reste modifiable à chaud. ### `0.1.2-pre.004` — non-blocking console/fichier + guards + ANSI + reload sinks @@ -1064,7 +1064,7 @@ 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. -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. Le takeover `Targets` est un layer global distinct dans ce `Vec`; les layers de sortie reloadables ne doivent pas être des `Filtered` remplacés directement. Pour les changements de sinks, `pre.004-fix.001` utilise `Handle::modify` afin de récupérer le `Vec` retiré : les anciens layers sont détruits avant les `WorkerGuard` correspondants, ce qui fixe explicitement le lifecycle de drain des writers non bloquants. `pre.004-fix.002` distingue en outre la sanitization des valeurs : activée côté console, désactivée côté formatter fichier car le `StripAnsiWriter` KSP possède la responsabilité de suppression avant persistence. +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. Le takeover `Targets` est composé avec `Layer::and_then` devant le `Vec` des sinks afin que son `Interest::never()` gouverne réellement tout le groupe de sorties ; les layers de sortie reloadables ne sont pas des `Filtered` remplacés directement. Pour les changements de sinks, `pre.004-fix.001` utilise `Handle::modify` afin de récupérer le `Vec` retiré : les anciens layers sont détruits avant les `WorkerGuard` correspondants, ce qui fixe explicitement le lifecycle de drain des writers non bloquants. `pre.004-fix.002` distingue en outre la sanitization des valeurs : activée côté console, désactivée côté formatter fichier car le `StripAnsiWriter` KSP possède la responsabilité de suppression avant persistence. `pre.004-fix.003` ferme enfin la fuite des targets externes révélée par le test d'intégration fichier. Restent à confirmer par les prereleases suivantes sans remettre en cause ce contrat :