v0.1.2-pre.003-fix.001

This commit is contained in:
2026-08-14 18:29:56 +02:00
parent 6e06802e38
commit 488d8ae0a8
5 changed files with 174 additions and 12 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml # file: Cargo.toml
# version: 29 # version: 30
[workspace] [workspace]
resolver = "3" resolver = "3"
members = ["crates/ksp-core-lib", "crates/ksp-logging-lib"] members = ["crates/ksp-core-lib", "crates/ksp-logging-lib"]
[workspace.package] [workspace.package]
version = "0.1.2-pre.3" version = "0.1.2-pre.3.fix.1"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-logging-lib/src/runtime.rs // file: crates/ksp-logging-lib/src/runtime.rs
// version: 1 // version: 2
use tracing_subscriber::Layer; // rust-rules: trait-import use tracing_subscriber::Layer; // rust-rules: trait-import
use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import
@@ -72,6 +72,8 @@ fn prepare_runtime_layers(settings: &crate::LoggingSettings) -> ksp_core_lib::Re
} }
let mut layers = RuntimeLayers::new(); let mut layers = RuntimeLayers::new();
if let std::option::Option::Some(console) = settings.console() { if let std::option::Option::Some(console) = settings.console() {
let takeover_filter: BoxedRuntimeLayer = std::boxed::Box::new(build_target_filter(settings));
layers.push(takeover_filter);
layers.push(build_console_layer(console, settings)); layers.push(build_console_layer(console, settings));
} }
return std::result::Result::Ok(layers); return std::result::Result::Ok(layers);
@@ -87,7 +89,6 @@ fn build_console_layer(console: &crate::ConsoleSettings, settings: &crate::Loggi
.with_ansi(false) .with_ansi(false)
.with_target(true) .with_target(true)
.with_span_events(map_span_events(settings.span_events())) .with_span_events(map_span_events(settings.span_events()))
.with_filter(build_target_filter(settings))
.boxed(); .boxed();
return layer; return layer;
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-logging-lib/unit_tests/runtime.rs // file: crates/ksp-logging-lib/unit_tests/runtime.rs
// version: 1 // version: 2
#[test] #[test]
fn level_mapping_covers_all_ksp_levels() { fn level_mapping_covers_all_ksp_levels() {
@@ -54,3 +54,20 @@ fn file_output_is_rejected_until_file_runtime_is_introduced() {
}; };
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_SETTINGS); assert_eq!(error.code(), crate::ERROR_CODE_INVALID_SETTINGS);
} }
#[test]
fn console_runtime_keeps_takeover_filter_separate_from_formatter() {
let settings = crate::LoggingSettings::new(
crate::LogFilterLevel::Info,
crate::SpanEvents::Off,
std::option::Option::Some(crate::ConsoleSettings::stdout()),
std::option::Option::None,
);
let result = super::prepare_runtime_layers(&settings);
assert!(result.is_ok());
let layers = match result {
std::result::Result::Ok(layers) => layers,
std::result::Result::Err(_) => return,
};
assert_eq!(layers.len(), 2);
}

View File

@@ -0,0 +1,144 @@
<!-- file: deltas/0.1.2/pre.003-fix.001.md -->
<!-- version: 1 -->
# Delta 0.1.2-pre.003-fix.001
## Base requise
Livraison précédente :
```text
0.1.2-pre.003
```
La base porte :
```text
workspace.package.version = "0.1.2-pre.3"
Cargo.toml header version = 29
```
## Motif du correctif
Les validations remontées pour `pre.003` sont :
```text
cargo fmt --all OK
cargo check --workspace OK
cargo clippy --workspace --all-targets OK
cargo test --workspace ECHEC
```
Le test d'intégration :
```text
global_runtime_supports_takeover_hot_reload_and_single_initialization
```
panique pendant le premier `reinitialize()` activant la console :
```text
a `Filtered` layer was used, but it had no `FilterId`; was it registered with the subscriber?
```
## Cause
`pre.003` construisait le sink console sous cette forme conceptuelle :
```text
fmt layer
.with_filter(Targets)
-> Filtered<fmt, Targets, Registry>
```
Ce `Filtered` était ensuite boxed dans le `Vec<Box<dyn Layer<Registry>>>` placé derrière `tracing_subscriber::reload::Layer`.
Au démarrage sans sink, le `Vec` initial était vide. Le premier hot reload construisait donc un nouveau `Filtered` après l'installation du subscriber global puis remplaçait le `Vec` via `Handle::reload`. Or un per-layer `Filtered` a besoin que son `FilterId` soit enregistré lors de son attachement au subscriber. La documentation de `tracing-subscriber 0.3.23` indique explicitement que `Handle::reload` ne doit pas être utilisé pour remplacer directement un `Filtered`.
Le panic n'indique donc pas un défaut du contrat public KSP de hot reload, mais une composition interne incorrecte des layers de `pre.003`.
## Correction
Le runtime conserve :
```text
reload::Layer<Vec<Box<dyn Layer<Registry>>>>
```
mais la composition devient :
```text
Vec reloadable
├── Targets global takeover filter
└── fmt console layer
```
au lieu de :
```text
Vec reloadable
└── Filtered<fmt console layer, Targets>
```
`Targets` est utilisé comme layer de filtrage global. Le layer `fmt` n'appelle plus `with_filter`.
Conséquences :
- aucun nouveau `Filtered` n'est injecté par `Handle::reload` ;
- aucun `FilterId` tardif n'est nécessaire ;
- le takeover reste global : les targets externes restent `OFF` ;
- les niveaux KSP et overrides par préfixe restent inchangés ;
- le `Vec` complet peut toujours être remplacé pour activer/désactiver des sinks à chaud ;
- l'API publique `initialize` / `reinitialize` / `LoggingGuard` ne change pas ;
- `pre.004` peut toujours ajouter le backend fichier au même runtime reloadable.
Une configuration sans sink conserve un `Vec` vide, donc le logging reste effectivement désactivé jusqu'à un `reinitialize()` qui ajoute une sortie.
## Tests
Le test d'intégration déjà présent qui a révélé la régression reste le test de non-régression principal :
```text
global_runtime_supports_takeover_hot_reload_and_single_initialization
```
Un test unitaire supplémentaire vérifie que la console prépare deux layers distincts : le takeover filter global et le formatter.
## Version technique
Ce correctif modifie du Rust. Conformément à la règle KSP de signal technique, la version workspace devient :
```text
workspace.package.version = "0.1.2-pre.3.fix.1"
```
et l'en-tête du `Cargo.toml` racine devient :
```text
# version: 30
```
## 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.003-fix.001.md
```
## Validations à exécuter
```bash
cargo fmt --all
cargo check --workspace
cargo test --workspace
cargo clippy --workspace --all-targets
```
Si ces validations sont propres, la tranche suivante reste :
```text
0.1.2-pre.004 — non-blocking console/file + guards + ANSI + reload sinks
```

View File

@@ -1,13 +1,13 @@
<!-- file: docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md --> <!-- file: docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md -->
<!-- version: 5 --> <!-- version: 6 -->
# Plan KSP 0.1.2 — Logging foundation # Plan KSP 0.1.2 — Logging foundation
## Statut ## Statut
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`. 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` et son correctif `0.1.2-pre.003-fix.001`.
`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`. `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`. Pour `pre.003`, `cargo fmt`, `cargo check` et `cargo clippy --workspace --all-targets` sont propres, mais `cargo test --workspace` a révélé un panic de `tracing-subscriber` lors du premier hot reload avec console : un `Filtered` nouvellement injecté dans le `Vec` reloadable ne possédait pas de `FilterId` enregistré. `pre.003-fix.001` sépare donc le `Targets` global du formatter afin qu'aucun `Filtered` ne soit remplacé par `Handle::reload`. Les writers non bloquants, le fichier, les guards et le stripping ANSI restent réservés à `pre.004`.
## Base auditée ## Base auditée
@@ -926,7 +926,7 @@ Objectifs :
### `0.1.2-pre.003` — subscriber + takeover + console + reload foundation ### `0.1.2-pre.003` — subscriber + takeover + console + reload foundation
Statut : implémenté dans la tranche `pre.003`, sous réserve des validations Cargo à exécuter dans l'environnement de développement. Statut : implémenté dans `pre.003`, puis corrigé par `pre.003-fix.001` après détection d'un panic de reload dans le test d'intégration global.
Réalisé : Réalisé :
@@ -943,7 +943,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é. 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<Box<dyn Layer<Registry>>>` 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`. Le runtime reloadable reste un `Vec<Box<dyn Layer<Registry>>>` 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`.
### `0.1.2-pre.004` — non-blocking console/fichier + guards + ANSI + reload sinks ### `0.1.2-pre.004` — non-blocking console/fichier + guards + ANSI + reload sinks
@@ -1038,7 +1038,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 ; 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. 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. 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 par `Handle::reload`.
Restent à confirmer par les prereleases suivantes sans remettre en cause ce contrat : Restent à confirmer par les prereleases suivantes sans remettre en cause ce contrat :
@@ -1046,4 +1046,4 @@ Restent à confirmer par les prereleases suivantes sans remettre en cause ce con
2. le détail visuel exact du formatter humain, sans transformer sa ponctuation en contrat public ; 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. 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. La prochaine action après validation de `pre.003-fix.001` est `0.1.2-pre.004` : `tracing-appender`, console/fichier non bloquants, guards, rotation, stripping ANSI et reload des sinks.