Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a7219fa59 | |||
| 77f6eaf487 | |||
| 8a0c119878 | |||
| d96f41fb8e | |||
| ea87a58e32 | |||
| 20c5643701 | |||
| d55903b13e | |||
| 37f53f3080 | |||
| 9f23eb950c | |||
| 151590422d | |||
| 488d8ae0a8 | |||
| 6e06802e38 | |||
| 697527675a | |||
| 7b4444fb07 | |||
| bd8401c0d0 | |||
| 18ae0e4873 |
10
Cargo.toml
10
Cargo.toml
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 25
|
# version: 39
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = ["crates/ksp-core-lib"]
|
members = ["crates/ksp-core-lib", "crates/ksp-logging-lib"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.1"
|
version = "0.1.2"
|
||||||
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"
|
||||||
@@ -15,6 +15,10 @@ publish = false
|
|||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
solana-pubkey = { version = "^4.3", default-features = false }
|
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"] }
|
||||||
|
tracing-appender = { version = "^0.2", default-features = false }
|
||||||
|
tokio = { version = "^1.53", default-features = false, features = ["rt", "rt-multi-thread", "macros"] }
|
||||||
|
|
||||||
[workspace.lints.rust]
|
[workspace.lints.rust]
|
||||||
missing_docs = "warn"
|
missing_docs = "warn"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: ROADMAP.md -->
|
<!-- file: ROADMAP.md -->
|
||||||
<!-- version: 13 -->
|
<!-- version: 14 -->
|
||||||
|
|
||||||
# Roadmap KSP
|
# Roadmap KSP
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ Regrouper les releases consacrées aux fondations N1. Chaque release concrète e
|
|||||||
### Releases concrètes
|
### Releases concrètes
|
||||||
|
|
||||||
- [X] `0.1.1` — Stabiliser `ksp-core-lib` : `Error`/`Result`, Program IDs fondamentaux et primitives réellement N1.
|
- [X] `0.1.1` — Stabiliser `ksp-core-lib` : `Error`/`Result`, Program IDs fondamentaux et primitives réellement N1.
|
||||||
- [ ] `0.1.2` — Introduire `ksp-logging-lib` comme façade KSP de `tracing`, `tracing-appender` et `tracing-subscriber`.
|
- [X] `0.1.2` — Introduire `ksp-logging-lib` comme façade KSP de `tracing`, `tracing-appender` et `tracing-subscriber`.
|
||||||
- [ ] `0.1.3` — Introduire `ksp-config-lib` : documents, profils, résolution, validation et modifications autorisées.
|
- [ ] `0.1.3` — Introduire `ksp-config-lib` : documents, profils, résolution, validation et modifications autorisées.
|
||||||
- [ ] `0.1.4` — Introduire `ksp-app-config-desk` pour valider réellement Config et la frontière Tauri.
|
- [ ] `0.1.4` — Introduire `ksp-app-config-desk` pour valider réellement Config et la frontière Tauri.
|
||||||
|
|
||||||
|
|||||||
20
crates/ksp-logging-lib/Cargo.toml
Normal file
20
crates/ksp-logging-lib/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# file: crates/ksp-logging-lib/Cargo.toml
|
||||||
|
# version: 4
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "ksp-logging-lib"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
ksp-core-lib = { path = "../ksp-core-lib" }
|
||||||
|
tracing.workspace = true
|
||||||
|
tracing-subscriber.workspace = true
|
||||||
|
tracing-appender.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio.workspace = true
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
35
crates/ksp-logging-lib/README.md
Normal file
35
crates/ksp-logging-lib/README.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<!-- file: crates/ksp-logging-lib/README.md -->
|
||||||
|
<!-- version: 2 -->
|
||||||
|
|
||||||
|
# ksp-logging-lib
|
||||||
|
|
||||||
|
`ksp-logging-lib` est la façade commune de logging/tracing runtime de Khadhroony Solana Project.
|
||||||
|
|
||||||
|
## Responsabilités
|
||||||
|
|
||||||
|
La crate possède :
|
||||||
|
|
||||||
|
- les cinq niveaux KSP `error`, `warn`, `info`, `debug` et `trace` ;
|
||||||
|
- les macros d'événements et de spans qui préservent le callsite du consommateur ;
|
||||||
|
- `LoggingSettings` et les settings console/fichier indépendants de Config ;
|
||||||
|
- 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 ;
|
||||||
|
- les sorties console et fichier non bloquantes ;
|
||||||
|
- les `WorkerGuard`, compteurs de lignes abandonnées, rotation fichier et stripping ANSI ;
|
||||||
|
- l'instrumentation de scopes synchrones et de `Future` async.
|
||||||
|
|
||||||
|
L'API async de production reste indépendante de tout executor. Tokio est utilisé uniquement comme `dev-dependency` afin de valider `instrument(...)` sur un executor réel en mode current-thread et multi-thread ; il ne fait pas partie des dépendances runtime de la crate.
|
||||||
|
|
||||||
|
## Frontières
|
||||||
|
|
||||||
|
Une crate KSP comportementale qui journalise son activité dépend de `ksp-logging-lib` et n'utilise pas directement `tracing`, `tracing-subscriber` ou `tracing-appender`.
|
||||||
|
|
||||||
|
Les événements utiles issus d'une dépendance externe ne sont pas renommés : la crate KSP propriétaire de l'opération réémet explicitement l'information utile sous son propre target KSP.
|
||||||
|
|
||||||
|
`ksp-logging-lib` ne dépend pas de `ksp-config-lib`. Config pourra construire un `LoggingSettings` puis appeler `initialize` ou `reinitialize`.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- [`USAGE.md`](USAGE.md) — utilisation concrète de la façade et du runtime ;
|
||||||
|
- [`TODO.md`](TODO.md) — capacités explicitement différées ou points restant à fermer.
|
||||||
22
crates/ksp-logging-lib/TODO.md
Normal file
22
crates/ksp-logging-lib/TODO.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<!-- file: crates/ksp-logging-lib/TODO.md -->
|
||||||
|
<!-- version: 2 -->
|
||||||
|
|
||||||
|
# TODO ksp-logging-lib
|
||||||
|
|
||||||
|
## À fermer avant la stable 0.1.2
|
||||||
|
|
||||||
|
- exécuter les validations Cargo complètes de `pre.006`, y compris les tests Tokio current-thread/multi-thread ;
|
||||||
|
- vérifier que `cargo tree -p ksp-logging-lib -e normal` ne contient pas Tokio et que Tokio apparaît uniquement dans le graphe dev attendu ;
|
||||||
|
- refaire l'audit final du graphe/features et de l'ownership de la stack tracing ;
|
||||||
|
- après validation de la prerelease finale, préparer `rel.001`, publier `workspace.package.version = "0.1.2"` et taguer `v0.1.2` conformément aux règles de release.
|
||||||
|
|
||||||
|
## Capacités différées
|
||||||
|
|
||||||
|
Ces éléments ne font pas partie du contrat `0.1.2` et ne doivent être ajoutés qu'après besoin concret :
|
||||||
|
|
||||||
|
- plusieurs routes fichier indépendantes ;
|
||||||
|
- rotation par taille, rétention/compression et symlink `latest` ;
|
||||||
|
- formats JSON ou autres formats structurés alternatifs ;
|
||||||
|
- OpenTelemetry/export réseau ;
|
||||||
|
- watcher de fichiers de configuration, qui appartient à Config ou à une couche supérieure ;
|
||||||
|
- benchmark/profiling de précision destiné aux chemins de trading sensibles à la latence.
|
||||||
130
crates/ksp-logging-lib/USAGE.md
Normal file
130
crates/ksp-logging-lib/USAGE.md
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<!-- file: crates/ksp-logging-lib/USAGE.md -->
|
||||||
|
<!-- version: 2 -->
|
||||||
|
|
||||||
|
# Utilisation de ksp-logging-lib
|
||||||
|
|
||||||
|
## Target d'une crate consommatrice
|
||||||
|
|
||||||
|
Chaque crate KSP comportementale fournit explicitement son target, égal au nom Cargo de la crate :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
const LOGGING_TARGET: &str = "ksp-store-lib";
|
||||||
|
|
||||||
|
ksp_logging_lib::trace!(
|
||||||
|
target: LOGGING_TARGET,
|
||||||
|
domain = "store",
|
||||||
|
component = "postgres",
|
||||||
|
operation = "load_transactions",
|
||||||
|
"executing store operation"
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Les champs `domain`, `component`, `operation` et autres champs structurés sont ajoutés par le caller lorsqu'ils sont utiles ; ils ne remplacent pas le target propriétaire.
|
||||||
|
|
||||||
|
## Initialisation
|
||||||
|
|
||||||
|
`initialize` installe le subscriber global KSP une seule fois et retourne le `LoggingGuard` qui doit rester vivant pendant la durée du runtime :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let settings = 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::Some(ksp_logging_lib::FileSettings::new(
|
||||||
|
"logs",
|
||||||
|
"worker.log",
|
||||||
|
ksp_logging_lib::FileRotation::Daily,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let initialize_result = ksp_logging_lib::initialize(&settings);
|
||||||
|
let mut logging_guard = match initialize_result {
|
||||||
|
std::result::Result::Ok(guard) => guard,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Une configuration sans console ni fichier est valide et installe une infrastructure initialement silencieuse qui pourra être activée plus tard par hot reload.
|
||||||
|
|
||||||
|
## Hot reload
|
||||||
|
|
||||||
|
Une nouvelle configuration peut être appliquée sans redémarrer le processus ou le worker :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let debug_settings = 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(
|
||||||
|
"ksp-store-lib",
|
||||||
|
ksp_logging_lib::LogFilterLevel::Debug,
|
||||||
|
));
|
||||||
|
|
||||||
|
let reload_result = ksp_logging_lib::reinitialize(&mut logging_guard, &debug_settings);
|
||||||
|
if let std::result::Result::Err(error) = reload_result {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
La nouvelle configuration est préparée avant la bascule. Si sa validation ou la création d'un nouveau sink échoue, l'ancienne configuration reste active.
|
||||||
|
|
||||||
|
## Spans synchrones
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let span = ksp_logging_lib::trace_span!(
|
||||||
|
target: LOGGING_TARGET,
|
||||||
|
"materialize_transaction",
|
||||||
|
domain = "store"
|
||||||
|
);
|
||||||
|
|
||||||
|
let output = span.in_scope(|| {
|
||||||
|
return materialize_transaction();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Avec `SpanEvents::NewAndClose`, le formatter produit les événements de création/fermeture et les temps `busy` / `idle` à la fermeture.
|
||||||
|
|
||||||
|
## Spans async
|
||||||
|
|
||||||
|
Une `Future` doit être instrumentée avec `ksp_logging_lib::instrument` ; un guard d'entrée de span ne doit pas être conservé à travers `.await` :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let span = ksp_logging_lib::trace_span!(
|
||||||
|
target: LOGGING_TARGET,
|
||||||
|
"fetch_account",
|
||||||
|
domain = "transport"
|
||||||
|
);
|
||||||
|
|
||||||
|
let output = ksp_logging_lib::instrument(span, fetch_account()).await;
|
||||||
|
```
|
||||||
|
|
||||||
|
La future instrumentée entre/sort du span pendant ses polls et lors de son `Drop`, conformément au contrat de la primitive `tracing` sous-jacente.
|
||||||
|
|
||||||
|
## Lignes abandonnées
|
||||||
|
|
||||||
|
Les sorties utilisent des queues lossy afin de ne pas appliquer de backpressure au hot path. Les pertes restent observables :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let dropped = logging_guard.dropped_lines();
|
||||||
|
ksp_logging_lib::warn!(
|
||||||
|
target: LOGGING_TARGET,
|
||||||
|
console = dropped.console(),
|
||||||
|
file = dropped.file(),
|
||||||
|
total = dropped.total(),
|
||||||
|
"logging queues dropped lines"
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Instrumentation async et executor
|
||||||
|
|
||||||
|
`instrument(span, future)` accepte une `Future` standard et ne dépend d'aucun executor particulier :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let span = ksp_logging_lib::trace_span!(target: LOGGING_TARGET, "load_transactions");
|
||||||
|
let result = ksp_logging_lib::instrument(span, async_operation()).await;
|
||||||
|
```
|
||||||
|
|
||||||
|
La crate ne requiert pas Tokio en production. Tokio n'est présent qu'en `dev-dependency` pour valider la surface sur un executor réel, y compris après plusieurs suspensions et sur un runtime multi-thread. Un consumer peut donc utiliser l'executor adapté à son propre contexte sans que Logging lui en impose un.
|
||||||
|
|
||||||
11
crates/ksp-logging-lib/src/error.rs
Normal file
11
crates/ksp-logging-lib/src/error.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// file: crates/ksp-logging-lib/src/error.rs
|
||||||
|
// version: 3
|
||||||
|
|
||||||
|
/// 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");
|
||||||
|
/// Error code used when the rolling file output cannot be initialized.
|
||||||
|
pub const ERROR_CODE_FILE_OUTPUT_INITIALIZATION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "file_output_initialization_failed");
|
||||||
60
crates/ksp-logging-lib/src/lib.rs
Normal file
60
crates/ksp-logging-lib/src/lib.rs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// file: crates/ksp-logging-lib/src/lib.rs
|
||||||
|
// version: 4
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
#![deny(unreachable_pub)]
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
//! 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. `0.1.2-pre.005` owns the single global subscriber, KSP takeover filtering, hot reload, non-blocking console/file outputs, rolling file
|
||||||
|
//! appenders, ANSI stripping, dropped-line counters and the worker guards required to flush active queues. The integration surface is hardened by
|
||||||
|
//! deterministic saturation, concurrent reload and ownership audits before final release validation.
|
||||||
|
|
||||||
|
mod error;
|
||||||
|
mod macros;
|
||||||
|
mod runtime;
|
||||||
|
mod settings;
|
||||||
|
mod span;
|
||||||
|
mod writer;
|
||||||
|
|
||||||
|
/// Error code used when a global logging subscriber is already installed.
|
||||||
|
pub use self::error::ERROR_CODE_ALREADY_INITIALIZED;
|
||||||
|
/// Error code used when the rolling file output cannot be initialized.
|
||||||
|
pub use self::error::ERROR_CODE_FILE_OUTPUT_INITIALIZATION_FAILED;
|
||||||
|
/// 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;
|
||||||
|
/// Cumulative number of log lines dropped by non-blocking KSP outputs.
|
||||||
|
pub use self::runtime::DroppedLines;
|
||||||
|
/// Guard owning the mutable runtime state and non-blocking writers 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.
|
||||||
|
pub use self::settings::ConsoleSettings;
|
||||||
|
/// Rotation cadence for the optional file output.
|
||||||
|
pub use self::settings::FileRotation;
|
||||||
|
/// Runtime settings for the optional file output.
|
||||||
|
pub use self::settings::FileSettings;
|
||||||
|
/// Runtime filter level used by KSP logging settings.
|
||||||
|
pub use self::settings::LogFilterLevel;
|
||||||
|
/// Complete runtime settings consumed by Logging initialization and reload.
|
||||||
|
pub use self::settings::LoggingSettings;
|
||||||
|
/// Lifecycle events emitted for spans by the formatted subscriber.
|
||||||
|
pub use self::settings::SpanEvents;
|
||||||
|
/// Per-target filter override owned by Logging.
|
||||||
|
pub use self::settings::TargetFilter;
|
||||||
|
/// KSP-owned handle to a tracing span.
|
||||||
|
pub use self::span::Span;
|
||||||
|
/// Instruments an asynchronous future with a KSP span.
|
||||||
|
pub use self::span::instrument;
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
/// Internal macro bridge. KSP consumers must not use this reexport directly.
|
||||||
|
pub extern crate tracing as __private_tracing;
|
||||||
97
crates/ksp-logging-lib/src/macros.rs
Normal file
97
crates/ksp-logging-lib/src/macros.rs
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
// file: crates/ksp-logging-lib/src/macros.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
/// Emits a KSP error event with an explicit owning target.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! error {
|
||||||
|
(target: $target:expr, $($argument:tt)+) => {{
|
||||||
|
$crate::__private_tracing::error!(target: $target, $($argument)+);
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emits a KSP warning event with an explicit owning target.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! warn {
|
||||||
|
(target: $target:expr, $($argument:tt)+) => {{
|
||||||
|
$crate::__private_tracing::warn!(target: $target, $($argument)+);
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emits a KSP informational event with an explicit owning target.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! info {
|
||||||
|
(target: $target:expr, $($argument:tt)+) => {{
|
||||||
|
$crate::__private_tracing::info!(target: $target, $($argument)+);
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emits a KSP debug event with an explicit owning target.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! debug {
|
||||||
|
(target: $target:expr, $($argument:tt)+) => {{
|
||||||
|
$crate::__private_tracing::debug!(target: $target, $($argument)+);
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emits a KSP trace event with an explicit owning target.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! trace {
|
||||||
|
(target: $target:expr, $($argument:tt)+) => {{
|
||||||
|
$crate::__private_tracing::trace!(target: $target, $($argument)+);
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a KSP error span with an explicit owning target.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! error_span {
|
||||||
|
(target: $target:expr, $name:expr) => {{
|
||||||
|
$crate::Span::__from_tracing($crate::__private_tracing::error_span!(target: $target, $name))
|
||||||
|
}};
|
||||||
|
(target: $target:expr, $name:expr, $($field:tt)+) => {{
|
||||||
|
$crate::Span::__from_tracing($crate::__private_tracing::error_span!(target: $target, $name, $($field)+))
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a KSP warning span with an explicit owning target.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! warn_span {
|
||||||
|
(target: $target:expr, $name:expr) => {{
|
||||||
|
$crate::Span::__from_tracing($crate::__private_tracing::warn_span!(target: $target, $name))
|
||||||
|
}};
|
||||||
|
(target: $target:expr, $name:expr, $($field:tt)+) => {{
|
||||||
|
$crate::Span::__from_tracing($crate::__private_tracing::warn_span!(target: $target, $name, $($field)+))
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a KSP informational span with an explicit owning target.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! info_span {
|
||||||
|
(target: $target:expr, $name:expr) => {{
|
||||||
|
$crate::Span::__from_tracing($crate::__private_tracing::info_span!(target: $target, $name))
|
||||||
|
}};
|
||||||
|
(target: $target:expr, $name:expr, $($field:tt)+) => {{
|
||||||
|
$crate::Span::__from_tracing($crate::__private_tracing::info_span!(target: $target, $name, $($field)+))
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a KSP debug span with an explicit owning target.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! debug_span {
|
||||||
|
(target: $target:expr, $name:expr) => {{
|
||||||
|
$crate::Span::__from_tracing($crate::__private_tracing::debug_span!(target: $target, $name))
|
||||||
|
}};
|
||||||
|
(target: $target:expr, $name:expr, $($field:tt)+) => {{
|
||||||
|
$crate::Span::__from_tracing($crate::__private_tracing::debug_span!(target: $target, $name, $($field)+))
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a KSP trace span with an explicit owning target.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! trace_span {
|
||||||
|
(target: $target:expr, $name:expr) => {{
|
||||||
|
$crate::Span::__from_tracing($crate::__private_tracing::trace_span!(target: $target, $name))
|
||||||
|
}};
|
||||||
|
(target: $target:expr, $name:expr, $($field:tt)+) => {{
|
||||||
|
$crate::Span::__from_tracing($crate::__private_tracing::trace_span!(target: $target, $name, $($field)+))
|
||||||
|
}};
|
||||||
|
}
|
||||||
288
crates/ksp-logging-lib/src/runtime.rs
Normal file
288
crates/ksp-logging-lib/src/runtime.rs
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
// file: crates/ksp-logging-lib/src/runtime.rs
|
||||||
|
// version: 8
|
||||||
|
|
||||||
|
use tracing_subscriber::Layer; // rust-rules: trait-import
|
||||||
|
use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import
|
||||||
|
|
||||||
|
/// Cumulative number of log lines dropped by non-blocking KSP outputs.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
|
pub struct DroppedLines {
|
||||||
|
console: usize,
|
||||||
|
file: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DroppedLines {
|
||||||
|
/// Returns an empty dropped-line snapshot.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn zero() -> Self {
|
||||||
|
return Self { console: 0, file: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of console lines dropped since Logging initialization.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn console(&self) -> usize {
|
||||||
|
return self.console;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of file lines dropped since Logging initialization.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn file(&self) -> usize {
|
||||||
|
return self.file;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the total number of dropped lines across console and file outputs.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn total(&self) -> usize {
|
||||||
|
return self.console.saturating_add(self.file);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn saturating_add(self, other: Self) -> Self {
|
||||||
|
return Self { console: self.console.saturating_add(other.console), file: self.file.saturating_add(other.file) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Guard owning the mutable runtime state and non-blocking writers of the installed KSP logging subscriber.
|
||||||
|
pub struct LoggingGuard {
|
||||||
|
reload_handle: RuntimeReloadHandle,
|
||||||
|
settings: crate::LoggingSettings,
|
||||||
|
outputs: RuntimeOutputs,
|
||||||
|
retired_dropped_lines: crate::DroppedLines,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoggingGuard {
|
||||||
|
/// Returns the settings currently active in the KSP logging runtime.
|
||||||
|
#[must_use]
|
||||||
|
pub fn settings(&self) -> &crate::LoggingSettings {
|
||||||
|
return &self.settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns cumulative dropped-line counters across active and previously reloaded outputs.
|
||||||
|
#[must_use]
|
||||||
|
pub fn dropped_lines(&self) -> crate::DroppedLines {
|
||||||
|
return self.retired_dropped_lines.saturating_add(self.outputs.dropped_lines());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type BoxedRuntimeLayer = std::boxed::Box<dyn tracing_subscriber::Layer<tracing_subscriber::Registry> + std::marker::Send + std::marker::Sync + 'static>;
|
||||||
|
type RuntimeLayers = std::vec::Vec<BoxedRuntimeLayer>;
|
||||||
|
type RuntimeReloadHandle = tracing_subscriber::reload::Handle<RuntimeLayers, tracing_subscriber::Registry>;
|
||||||
|
|
||||||
|
struct PreparedRuntime {
|
||||||
|
layers: RuntimeLayers,
|
||||||
|
outputs: RuntimeOutputs,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct RuntimeOutputs {
|
||||||
|
console: std::option::Option<RuntimeOutput>,
|
||||||
|
file: std::option::Option<RuntimeOutput>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RuntimeOutputs {
|
||||||
|
fn dropped_lines(&self) -> crate::DroppedLines {
|
||||||
|
let console = match self.console.as_ref() {
|
||||||
|
std::option::Option::Some(output) => output.dropped_lines(),
|
||||||
|
std::option::Option::None => 0,
|
||||||
|
};
|
||||||
|
let file = match self.file.as_ref() {
|
||||||
|
std::option::Option::Some(output) => output.dropped_lines(),
|
||||||
|
std::option::Option::None => 0,
|
||||||
|
};
|
||||||
|
return crate::DroppedLines { console, file };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RuntimeOutput {
|
||||||
|
_worker_guard: tracing_appender::non_blocking::WorkerGuard,
|
||||||
|
error_counter: tracing_appender::non_blocking::ErrorCounter,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RuntimeOutput {
|
||||||
|
fn dropped_lines(&self) -> usize {
|
||||||
|
return self.error_counter.dropped_lines();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PreparedOutput {
|
||||||
|
layer: BoxedRuntimeLayer,
|
||||||
|
output: RuntimeOutput,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Installs the global KSP tracing subscriber.
|
||||||
|
///
|
||||||
|
/// This function may succeed only once for the lifetime of the process. The returned guard owns all non-blocking writer guards and 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<crate::LoggingGuard> {
|
||||||
|
return prepare_runtime(settings).and_then(|prepared| -> ksp_core_lib::Result<crate::LoggingGuard> {
|
||||||
|
let PreparedRuntime { layers, outputs } = prepared;
|
||||||
|
let (reload_layer, reload_handle) = tracing_subscriber::reload::Layer::new(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(),
|
||||||
|
outputs,
|
||||||
|
retired_dropped_lines: crate::DroppedLines::zero(),
|
||||||
|
}),
|
||||||
|
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 and non-blocking outputs without reinstalling the global subscriber.
|
||||||
|
///
|
||||||
|
/// New runtime layers, writers and guards are fully prepared before the reload is attempted. If validation or preparation fails, the currently active
|
||||||
|
/// configuration remains unchanged. After a successful layer swap, dropped-line counters from the retired outputs are retained cumulatively. Retired
|
||||||
|
/// layers are then dropped before their worker guards so all retired `NonBlocking` senders are released before shutdown asks the workers to drain/flush.
|
||||||
|
pub fn reinitialize(guard: &mut crate::LoggingGuard, settings: &crate::LoggingSettings) -> ksp_core_lib::Result<()> {
|
||||||
|
return prepare_runtime(settings).and_then(|prepared| -> ksp_core_lib::Result<()> {
|
||||||
|
let PreparedRuntime { layers, outputs } = prepared;
|
||||||
|
let mut retired_layers = RuntimeLayers::new();
|
||||||
|
let reload_result = guard.reload_handle.modify(|active_layers| {
|
||||||
|
retired_layers = std::mem::replace(active_layers, layers);
|
||||||
|
});
|
||||||
|
return match reload_result {
|
||||||
|
std::result::Result::Ok(()) => {
|
||||||
|
guard.retired_dropped_lines = guard.retired_dropped_lines.saturating_add(guard.outputs.dropped_lines());
|
||||||
|
let retired_outputs = std::mem::replace(&mut guard.outputs, outputs);
|
||||||
|
guard.settings = settings.clone();
|
||||||
|
drop(retired_layers);
|
||||||
|
drop(retired_outputs);
|
||||||
|
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(settings: &crate::LoggingSettings) -> ksp_core_lib::Result<PreparedRuntime> {
|
||||||
|
let validation_error = settings.validate().err();
|
||||||
|
if let std::option::Option::Some(error) = validation_error {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
if settings.console().is_none() && settings.file().is_none() {
|
||||||
|
return std::result::Result::Ok(PreparedRuntime { layers: RuntimeLayers::new(), outputs: RuntimeOutputs::default() });
|
||||||
|
}
|
||||||
|
let prepared_file = match settings.file() {
|
||||||
|
std::option::Option::Some(file) => match build_file_output(file, settings) {
|
||||||
|
std::result::Result::Ok(output) => std::option::Option::Some(output),
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
},
|
||||||
|
std::option::Option::None => std::option::Option::None,
|
||||||
|
};
|
||||||
|
let prepared_console = settings.console().map(|console| -> PreparedOutput {
|
||||||
|
return build_console_output(console, settings);
|
||||||
|
});
|
||||||
|
let mut output_layers = RuntimeLayers::new();
|
||||||
|
let mut outputs = RuntimeOutputs::default();
|
||||||
|
if let std::option::Option::Some(console) = prepared_console {
|
||||||
|
output_layers.push(console.layer);
|
||||||
|
outputs.console = std::option::Option::Some(console.output);
|
||||||
|
}
|
||||||
|
if let std::option::Option::Some(file) = prepared_file {
|
||||||
|
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 layers = vec![takeover_layer];
|
||||||
|
return std::result::Result::Ok(PreparedRuntime { layers, outputs });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_console_output(console: &crate::ConsoleSettings, settings: &crate::LoggingSettings) -> PreparedOutput {
|
||||||
|
return match console.output() {
|
||||||
|
crate::ConsoleOutput::Stdout => build_non_blocking_output(std::io::stdout(), "ksp-logging-console", settings, true),
|
||||||
|
crate::ConsoleOutput::Stderr => build_non_blocking_output(std::io::stderr(), "ksp-logging-console", settings, true),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_file_output(file: &crate::FileSettings, settings: &crate::LoggingSettings) -> ksp_core_lib::Result<PreparedOutput> {
|
||||||
|
let appender_result = tracing_appender::rolling::RollingFileAppender::builder()
|
||||||
|
.rotation(map_file_rotation(file.rotation()))
|
||||||
|
.filename_prefix(file.file_name_prefix())
|
||||||
|
.build(file.directory());
|
||||||
|
let appender = match appender_result {
|
||||||
|
std::result::Result::Ok(appender) => appender,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_FILE_OUTPUT_INITIALIZATION_FAILED, "unable to initialize the KSP rolling file appender")
|
||||||
|
.with_context("directory", file.directory().display().to_string())
|
||||||
|
.with_context("file_name_prefix", file.file_name_prefix())
|
||||||
|
.with_source(error),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let stripped_writer = crate::writer::StripAnsiWriter::new(appender);
|
||||||
|
return std::result::Result::Ok(build_non_blocking_output(stripped_writer, "ksp-logging-file", settings, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_non_blocking_output<W>(writer: W, thread_name: &str, settings: &crate::LoggingSettings, ansi_sanitization: bool) -> PreparedOutput
|
||||||
|
where
|
||||||
|
W: std::io::Write + std::marker::Send + 'static,
|
||||||
|
{
|
||||||
|
let (non_blocking, worker_guard) = non_blocking_builder(thread_name).finish(writer);
|
||||||
|
let error_counter = non_blocking.error_counter();
|
||||||
|
let layer = build_format_layer(non_blocking, settings, ansi_sanitization);
|
||||||
|
return PreparedOutput { layer, output: RuntimeOutput { _worker_guard: worker_guard, error_counter } };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn non_blocking_builder(thread_name: &str) -> tracing_appender::non_blocking::NonBlockingBuilder {
|
||||||
|
return tracing_appender::non_blocking::NonBlockingBuilder::default().lossy(true).thread_name(thread_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_format_layer(writer: tracing_appender::non_blocking::NonBlocking, settings: &crate::LoggingSettings, ansi_sanitization: bool) -> BoxedRuntimeLayer {
|
||||||
|
return tracing_subscriber::fmt::layer()
|
||||||
|
.with_writer(writer)
|
||||||
|
.with_ansi(false)
|
||||||
|
.with_ansi_sanitization(ansi_sanitization)
|
||||||
|
.with_target(true)
|
||||||
|
.with_file(true)
|
||||||
|
.with_line_number(true)
|
||||||
|
.with_span_events(map_span_events(settings.span_events()))
|
||||||
|
.boxed();
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn map_file_rotation(rotation: crate::FileRotation) -> tracing_appender::rolling::Rotation {
|
||||||
|
return match rotation {
|
||||||
|
crate::FileRotation::Never => tracing_appender::rolling::Rotation::NEVER,
|
||||||
|
crate::FileRotation::Hourly => tracing_appender::rolling::Rotation::HOURLY,
|
||||||
|
crate::FileRotation::Daily => tracing_appender::rolling::Rotation::DAILY,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
233
crates/ksp-logging-lib/src/settings.rs
Normal file
233
crates/ksp-logging-lib/src/settings.rs
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
// file: crates/ksp-logging-lib/src/settings.rs
|
||||||
|
// version: 2
|
||||||
|
|
||||||
|
/// Runtime filter level used by KSP logging settings.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
|
pub enum LogFilterLevel {
|
||||||
|
/// Disables matching logging events and spans.
|
||||||
|
Off,
|
||||||
|
/// Enables only error-level events and spans.
|
||||||
|
Error,
|
||||||
|
/// Enables warning and error events and spans.
|
||||||
|
Warn,
|
||||||
|
/// Enables informational, warning and error events and spans.
|
||||||
|
Info,
|
||||||
|
/// Enables debug and less verbose events and spans.
|
||||||
|
Debug,
|
||||||
|
/// Enables all KSP logging events and spans.
|
||||||
|
Trace,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Per-target filter override owned by Logging.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct TargetFilter {
|
||||||
|
target_prefix: std::string::String,
|
||||||
|
level: crate::LogFilterLevel,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TargetFilter {
|
||||||
|
/// Creates a filter override for a KSP target prefix.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(target_prefix: impl std::convert::Into<std::string::String>, level: crate::LogFilterLevel) -> Self {
|
||||||
|
return Self { target_prefix: target_prefix.into(), level };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the configured target prefix.
|
||||||
|
#[must_use]
|
||||||
|
pub fn target_prefix(&self) -> &str {
|
||||||
|
return self.target_prefix.as_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the configured filter level.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn level(&self) -> crate::LogFilterLevel {
|
||||||
|
return self.level;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lifecycle events emitted for spans by the formatted subscriber.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
|
pub enum SpanEvents {
|
||||||
|
/// Does not synthesize span lifecycle events.
|
||||||
|
Off,
|
||||||
|
/// Emits span creation and closure events for timing-oriented diagnostics.
|
||||||
|
NewAndClose,
|
||||||
|
/// Emits all supported span lifecycle events.
|
||||||
|
Full,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Console stream selected for human-readable logs.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
|
pub enum ConsoleOutput {
|
||||||
|
/// Writes console logs to standard output.
|
||||||
|
Stdout,
|
||||||
|
/// Writes console logs to standard error.
|
||||||
|
Stderr,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runtime settings for the optional console output.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
|
pub struct ConsoleSettings {
|
||||||
|
output: crate::ConsoleOutput,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConsoleSettings {
|
||||||
|
/// Creates console settings targeting standard output.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn stdout() -> Self {
|
||||||
|
return Self { output: crate::ConsoleOutput::Stdout };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates console settings targeting standard error.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn stderr() -> Self {
|
||||||
|
return Self { output: crate::ConsoleOutput::Stderr };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the selected console stream.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn output(&self) -> crate::ConsoleOutput {
|
||||||
|
return self.output;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rotation cadence for the optional file output.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
|
pub enum FileRotation {
|
||||||
|
/// Keeps a single non-rotating file.
|
||||||
|
Never,
|
||||||
|
/// Rotates the file every hour.
|
||||||
|
Hourly,
|
||||||
|
/// Rotates the file every day.
|
||||||
|
Daily,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runtime settings for the optional file output.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct FileSettings {
|
||||||
|
directory: std::path::PathBuf,
|
||||||
|
file_name_prefix: std::string::String,
|
||||||
|
rotation: crate::FileRotation,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileSettings {
|
||||||
|
/// Creates file output settings.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(
|
||||||
|
directory: impl std::convert::Into<std::path::PathBuf>,
|
||||||
|
file_name_prefix: impl std::convert::Into<std::string::String>,
|
||||||
|
rotation: crate::FileRotation,
|
||||||
|
) -> Self {
|
||||||
|
return Self { directory: directory.into(), file_name_prefix: file_name_prefix.into(), rotation };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the directory containing log files.
|
||||||
|
#[must_use]
|
||||||
|
pub fn directory(&self) -> &std::path::Path {
|
||||||
|
return self.directory.as_path();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the file-name prefix passed to the file appender.
|
||||||
|
#[must_use]
|
||||||
|
pub fn file_name_prefix(&self) -> &str {
|
||||||
|
return self.file_name_prefix.as_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the selected file rotation cadence.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn rotation(&self) -> crate::FileRotation {
|
||||||
|
return self.rotation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Complete runtime settings consumed by `ksp-logging-lib` initialization and reload.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct LoggingSettings {
|
||||||
|
default_filter: crate::LogFilterLevel,
|
||||||
|
target_filters: std::vec::Vec<crate::TargetFilter>,
|
||||||
|
span_events: crate::SpanEvents,
|
||||||
|
console: std::option::Option<crate::ConsoleSettings>,
|
||||||
|
file: std::option::Option<crate::FileSettings>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoggingSettings {
|
||||||
|
/// Creates explicit Logging settings without any target override.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(
|
||||||
|
default_filter: crate::LogFilterLevel,
|
||||||
|
span_events: crate::SpanEvents,
|
||||||
|
console: std::option::Option<crate::ConsoleSettings>,
|
||||||
|
file: std::option::Option<crate::FileSettings>,
|
||||||
|
) -> Self {
|
||||||
|
return Self { default_filter, target_filters: std::vec::Vec::new(), span_events, console, file };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds one target-prefix override and returns the updated settings.
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_target_filter(mut self, target_filter: crate::TargetFilter) -> Self {
|
||||||
|
self.target_filters.push(target_filter);
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the default level applied to KSP-owned targets.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn default_filter(&self) -> crate::LogFilterLevel {
|
||||||
|
return self.default_filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns target-prefix overrides in insertion order.
|
||||||
|
#[must_use]
|
||||||
|
pub fn target_filters(&self) -> &[crate::TargetFilter] {
|
||||||
|
return self.target_filters.as_slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the selected span lifecycle event policy.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn span_events(&self) -> crate::SpanEvents {
|
||||||
|
return self.span_events;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns console settings when console output is enabled.
|
||||||
|
#[must_use]
|
||||||
|
pub fn console(&self) -> std::option::Option<&crate::ConsoleSettings> {
|
||||||
|
return self.console.as_ref();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns file settings when file output is enabled.
|
||||||
|
#[must_use]
|
||||||
|
pub fn file(&self) -> std::option::Option<&crate::FileSettings> {
|
||||||
|
return self.file.as_ref();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates backend-independent invariants of the runtime settings.
|
||||||
|
pub fn validate(&self) -> ksp_core_lib::Result<()> {
|
||||||
|
for target_filter in &self.target_filters {
|
||||||
|
if target_filter.target_prefix().is_empty() {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "target filter prefix must not be empty")
|
||||||
|
.with_context("field", "target_filters.target_prefix"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !target_filter.target_prefix().starts_with("ksp-") {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "target filter prefix must identify a KSP-owned target")
|
||||||
|
.with_context("field", "target_filters.target_prefix")
|
||||||
|
.with_context("target_prefix", target_filter.target_prefix()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let std::option::Option::Some(file) = self.file.as_ref()
|
||||||
|
&& file.file_name_prefix().is_empty()
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "file name prefix must not be empty")
|
||||||
|
.with_context("field", "file.file_name_prefix"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/settings.rs"]
|
||||||
|
mod tests;
|
||||||
41
crates/ksp-logging-lib/src/span.rs
Normal file
41
crates/ksp-logging-lib/src/span.rs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// file: crates/ksp-logging-lib/src/span.rs
|
||||||
|
// version: 2
|
||||||
|
|
||||||
|
/// KSP-owned handle to a tracing span.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Span {
|
||||||
|
inner: tracing::Span,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Span {
|
||||||
|
/// Runs synchronous work while this span is entered.
|
||||||
|
pub fn in_scope<T>(&self, operation: impl std::ops::FnOnce() -> T) -> T {
|
||||||
|
return self.inner.in_scope(operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
/// Constructs the KSP span wrapper for macro expansion support.
|
||||||
|
#[must_use]
|
||||||
|
pub fn __from_tracing(inner: tracing::Span) -> Self {
|
||||||
|
return Self { inner };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consumes this wrapper and returns the internal tracing span.
|
||||||
|
pub(crate) fn into_tracing(self) -> tracing::Span {
|
||||||
|
return self.inner;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Instruments an asynchronous future with a KSP span.
|
||||||
|
///
|
||||||
|
/// The span is entered whenever the future is polled or dropped and exited when that operation returns, so no enter guard is held across an `.await` point.
|
||||||
|
pub fn instrument<F>(span: crate::Span, future: F) -> impl std::future::Future<Output = F::Output>
|
||||||
|
where
|
||||||
|
F: std::future::Future,
|
||||||
|
{
|
||||||
|
return tracing::Instrument::instrument(future, span.into_tracing());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/span.rs"]
|
||||||
|
mod tests;
|
||||||
105
crates/ksp-logging-lib/src/writer.rs
Normal file
105
crates/ksp-logging-lib/src/writer.rs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
// file: crates/ksp-logging-lib/src/writer.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
enum StripAnsiState {
|
||||||
|
Text,
|
||||||
|
Escape,
|
||||||
|
Csi,
|
||||||
|
Osc,
|
||||||
|
OscEscape,
|
||||||
|
String,
|
||||||
|
StringEscape,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct StripAnsiWriter<W> {
|
||||||
|
inner: W,
|
||||||
|
state: StripAnsiState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W> StripAnsiWriter<W> {
|
||||||
|
pub(crate) const fn new(inner: W) -> Self {
|
||||||
|
return Self { inner, state: StripAnsiState::Text };
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
fn into_inner(self) -> W {
|
||||||
|
return self.inner;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W> std::io::Write for StripAnsiWriter<W>
|
||||||
|
where
|
||||||
|
W: std::io::Write,
|
||||||
|
{
|
||||||
|
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||||
|
let mut stripped = std::vec::Vec::with_capacity(buf.len());
|
||||||
|
for byte in buf {
|
||||||
|
self.consume_byte(*byte, &mut stripped);
|
||||||
|
}
|
||||||
|
let write_result = std::io::Write::write_all(&mut self.inner, stripped.as_slice());
|
||||||
|
return match write_result {
|
||||||
|
std::result::Result::Ok(()) => std::result::Result::Ok(buf.len()),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> std::io::Result<()> {
|
||||||
|
return std::io::Write::flush(&mut self.inner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W> StripAnsiWriter<W> {
|
||||||
|
fn consume_byte(&mut self, byte: u8, output: &mut std::vec::Vec<u8>) {
|
||||||
|
self.state = match self.state {
|
||||||
|
StripAnsiState::Text => {
|
||||||
|
if byte == 0x1B {
|
||||||
|
StripAnsiState::Escape
|
||||||
|
} else {
|
||||||
|
output.push(byte);
|
||||||
|
StripAnsiState::Text
|
||||||
|
}
|
||||||
|
},
|
||||||
|
StripAnsiState::Escape => match byte {
|
||||||
|
b'[' => StripAnsiState::Csi,
|
||||||
|
b']' => StripAnsiState::Osc,
|
||||||
|
b'P' | b'X' | b'^' | b'_' => StripAnsiState::String,
|
||||||
|
0x1B => StripAnsiState::Escape,
|
||||||
|
_ => StripAnsiState::Text,
|
||||||
|
},
|
||||||
|
StripAnsiState::Csi => {
|
||||||
|
if (0x40..=0x7E).contains(&byte) {
|
||||||
|
StripAnsiState::Text
|
||||||
|
} else {
|
||||||
|
StripAnsiState::Csi
|
||||||
|
}
|
||||||
|
},
|
||||||
|
StripAnsiState::Osc => match byte {
|
||||||
|
0x07 => StripAnsiState::Text,
|
||||||
|
0x1B => StripAnsiState::OscEscape,
|
||||||
|
_ => StripAnsiState::Osc,
|
||||||
|
},
|
||||||
|
StripAnsiState::OscEscape => match byte {
|
||||||
|
b'\\' => StripAnsiState::Text,
|
||||||
|
0x1B => StripAnsiState::OscEscape,
|
||||||
|
_ => StripAnsiState::Osc,
|
||||||
|
},
|
||||||
|
StripAnsiState::String => {
|
||||||
|
if byte == 0x1B {
|
||||||
|
StripAnsiState::StringEscape
|
||||||
|
} else {
|
||||||
|
StripAnsiState::String
|
||||||
|
}
|
||||||
|
},
|
||||||
|
StripAnsiState::StringEscape => match byte {
|
||||||
|
b'\\' => StripAnsiState::Text,
|
||||||
|
0x1B => StripAnsiState::StringEscape,
|
||||||
|
_ => StripAnsiState::String,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/writer.rs"]
|
||||||
|
mod tests;
|
||||||
163
crates/ksp-logging-lib/tests/callsite.rs
Normal file
163
crates/ksp-logging-lib/tests/callsite.rs
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
// file: crates/ksp-logging-lib/tests/callsite.rs
|
||||||
|
// version: 2
|
||||||
|
|
||||||
|
//! Integration tests for KSP logging callsite and async span instrumentation behavior.
|
||||||
|
|
||||||
|
const TEST_TARGET: &str = "ksp-logging-lib";
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
struct CapturedMetadata {
|
||||||
|
target: std::string::String,
|
||||||
|
file: std::option::Option<std::string::String>,
|
||||||
|
module_path: std::option::Option<std::string::String>,
|
||||||
|
line: std::option::Option<u32>,
|
||||||
|
is_event: bool,
|
||||||
|
is_span: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CapturedMetadata {
|
||||||
|
fn from_metadata(metadata: &tracing::Metadata<'_>) -> Self {
|
||||||
|
return Self {
|
||||||
|
target: metadata.target().to_owned(),
|
||||||
|
file: metadata.file().map(str::to_owned),
|
||||||
|
module_path: metadata.module_path().map(str::to_owned),
|
||||||
|
line: metadata.line(),
|
||||||
|
is_event: metadata.is_event(),
|
||||||
|
is_span: metadata.is_span(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct CaptureSubscriber {
|
||||||
|
captured: std::sync::Arc<std::sync::Mutex<std::vec::Vec<CapturedMetadata>>>,
|
||||||
|
enters: std::sync::Arc<std::sync::atomic::AtomicU64>,
|
||||||
|
exits: std::sync::Arc<std::sync::atomic::AtomicU64>,
|
||||||
|
next_id: std::sync::Arc<std::sync::atomic::AtomicU64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CaptureSubscriber {
|
||||||
|
fn new(
|
||||||
|
captured: std::sync::Arc<std::sync::Mutex<std::vec::Vec<CapturedMetadata>>>,
|
||||||
|
enters: std::sync::Arc<std::sync::atomic::AtomicU64>,
|
||||||
|
exits: std::sync::Arc<std::sync::atomic::AtomicU64>,
|
||||||
|
) -> Self {
|
||||||
|
return Self { captured, enters, exits, next_id: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(1)) };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn capture(&self, metadata: &tracing::Metadata<'_>) {
|
||||||
|
let lock = self.captured.lock();
|
||||||
|
if let std::result::Result::Ok(mut values) = lock {
|
||||||
|
values.push(CapturedMetadata::from_metadata(metadata));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl tracing::Subscriber for CaptureSubscriber {
|
||||||
|
fn enabled(&self, _metadata: &tracing::Metadata<'_>) -> bool {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_span(&self, span: &tracing::span::Attributes<'_>) -> tracing::span::Id {
|
||||||
|
self.capture(span.metadata());
|
||||||
|
let id = self.next_id.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
return tracing::span::Id::from_u64(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record(&self, _span: &tracing::span::Id, _values: &tracing::span::Record<'_>) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_follows_from(&self, _span: &tracing::span::Id, _follows: &tracing::span::Id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event(&self, event: &tracing::Event<'_>) {
|
||||||
|
self.capture(event.metadata());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enter(&self, _span: &tracing::span::Id) {
|
||||||
|
self.enters.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exit(&self, _span: &tracing::span::Id) {
|
||||||
|
self.exits.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn captured_values(captured: &std::sync::Arc<std::sync::Mutex<std::vec::Vec<CapturedMetadata>>>) -> std::vec::Vec<CapturedMetadata> {
|
||||||
|
let lock = captured.lock();
|
||||||
|
return match lock {
|
||||||
|
std::result::Result::Ok(values) => values.clone(),
|
||||||
|
std::result::Result::Err(error) => error.into_inner().clone(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn event_macro_preserves_consumer_callsite() {
|
||||||
|
let captured = std::sync::Arc::new(std::sync::Mutex::new(std::vec::Vec::new()));
|
||||||
|
let enters = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
|
||||||
|
let exits = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
|
||||||
|
let subscriber = CaptureSubscriber::new(captured.clone(), enters, exits);
|
||||||
|
let expected_line = line!() + 2;
|
||||||
|
tracing::subscriber::with_default(subscriber, || {
|
||||||
|
ksp_logging_lib::info!(target: TEST_TARGET, domain = "logging", "callsite event");
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
let values = captured_values(&captured);
|
||||||
|
assert_eq!(values.len(), 1);
|
||||||
|
assert_eq!(values[0].target, TEST_TARGET);
|
||||||
|
assert_eq!(values[0].file.as_deref(), std::option::Option::Some(file!()));
|
||||||
|
assert_eq!(values[0].module_path.as_deref(), std::option::Option::Some(module_path!()));
|
||||||
|
assert_eq!(values[0].line, std::option::Option::Some(expected_line));
|
||||||
|
assert!(values[0].is_event);
|
||||||
|
assert!(!values[0].is_span);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn span_macro_preserves_consumer_callsite() {
|
||||||
|
let captured = std::sync::Arc::new(std::sync::Mutex::new(std::vec::Vec::new()));
|
||||||
|
let enters = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
|
||||||
|
let exits = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
|
||||||
|
let subscriber = CaptureSubscriber::new(captured.clone(), enters, exits);
|
||||||
|
let expected_line = line!() + 2;
|
||||||
|
tracing::subscriber::with_default(subscriber, || {
|
||||||
|
let _span = ksp_logging_lib::trace_span!(target: TEST_TARGET, "callsite_span", component = "test");
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
let values = captured_values(&captured);
|
||||||
|
assert_eq!(values.len(), 1);
|
||||||
|
assert_eq!(values[0].target, TEST_TARGET);
|
||||||
|
assert_eq!(values[0].file.as_deref(), std::option::Option::Some(file!()));
|
||||||
|
assert_eq!(values[0].module_path.as_deref(), std::option::Option::Some(module_path!()));
|
||||||
|
assert_eq!(values[0].line, std::option::Option::Some(expected_line));
|
||||||
|
assert!(!values[0].is_event);
|
||||||
|
assert!(values[0].is_span);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn async_instrumentation_enters_and_exits_span_during_poll_and_drop() {
|
||||||
|
let captured = std::sync::Arc::new(std::sync::Mutex::new(std::vec::Vec::new()));
|
||||||
|
let enters = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
|
||||||
|
let exits = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
|
||||||
|
let subscriber = CaptureSubscriber::new(captured, std::sync::Arc::clone(&enters), std::sync::Arc::clone(&exits));
|
||||||
|
tracing::subscriber::with_default(subscriber, || {
|
||||||
|
let span = ksp_logging_lib::trace_span!(target: TEST_TARGET, "async_poll_span", domain = "logging");
|
||||||
|
let future = ksp_logging_lib::instrument(span, std::future::ready(42_u32));
|
||||||
|
let mut future = std::boxed::Box::pin(future);
|
||||||
|
let waker = std::task::Waker::noop();
|
||||||
|
let mut context = std::task::Context::from_waker(waker);
|
||||||
|
let poll = std::future::Future::poll(future.as_mut(), &mut context);
|
||||||
|
assert_eq!(poll, std::task::Poll::Ready(42_u32));
|
||||||
|
assert_eq!(enters.load(std::sync::atomic::Ordering::Relaxed), 1);
|
||||||
|
assert_eq!(exits.load(std::sync::atomic::Ordering::Relaxed), 1);
|
||||||
|
std::mem::drop(future);
|
||||||
|
assert_eq!(enters.load(std::sync::atomic::Ordering::Relaxed), 2);
|
||||||
|
assert_eq!(exits.load(std::sync::atomic::Ordering::Relaxed), 2);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
assert_eq!(enters.load(std::sync::atomic::Ordering::Relaxed), exits.load(std::sync::atomic::Ordering::Relaxed));
|
||||||
|
}
|
||||||
47
crates/ksp-logging-lib/tests/overhead.rs
Normal file
47
crates/ksp-logging-lib/tests/overhead.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// file: crates/ksp-logging-lib/tests/overhead.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Diagnostic gross-overhead probe for the reload layer used by KSP Logging.
|
||||||
|
|
||||||
|
use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import
|
||||||
|
|
||||||
|
const TEST_TARGET: &str = "ksp-logging-lib";
|
||||||
|
const ITERATIONS: u64 = 200_000;
|
||||||
|
|
||||||
|
fn emit_probe_events() {
|
||||||
|
for sequence in 0..ITERATIONS {
|
||||||
|
tracing::trace!(target: TEST_TARGET, sequence, "reload overhead probe");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trace_filter() -> tracing_subscriber::filter::Targets {
|
||||||
|
return tracing_subscriber::filter::Targets::new()
|
||||||
|
.with_default(tracing_subscriber::filter::LevelFilter::OFF)
|
||||||
|
.with_target(TEST_TARGET, tracing_subscriber::filter::LevelFilter::TRACE);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "diagnostic timing probe; run explicitly with --ignored --nocapture"]
|
||||||
|
fn reload_layer_overhead_remains_within_a_gross_regression_guardrail() {
|
||||||
|
let baseline_subscriber = tracing_subscriber::registry().with(trace_filter());
|
||||||
|
let baseline_start = std::time::Instant::now();
|
||||||
|
tracing::subscriber::with_default(baseline_subscriber, || {
|
||||||
|
emit_probe_events();
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
let baseline_elapsed = baseline_start.elapsed();
|
||||||
|
let (reload_layer, _reload_handle) = tracing_subscriber::reload::Layer::new(trace_filter());
|
||||||
|
let reload_subscriber = tracing_subscriber::registry().with(reload_layer);
|
||||||
|
let reload_start = std::time::Instant::now();
|
||||||
|
tracing::subscriber::with_default(reload_subscriber, || {
|
||||||
|
emit_probe_events();
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
let reload_elapsed = reload_start.elapsed();
|
||||||
|
let gross_ceiling = baseline_elapsed.saturating_mul(100).saturating_add(std::time::Duration::from_millis(100));
|
||||||
|
println!("KSP reload overhead probe: baseline={baseline_elapsed:?}, reload={reload_elapsed:?}, iterations={ITERATIONS}");
|
||||||
|
assert!(
|
||||||
|
reload_elapsed <= gross_ceiling,
|
||||||
|
"reload layer exceeded the gross regression guardrail: baseline={baseline_elapsed:?}, reload={reload_elapsed:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
81
crates/ksp-logging-lib/tests/ownership.rs
Normal file
81
crates/ksp-logging-lib/tests/ownership.rs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
// file: crates/ksp-logging-lib/tests/ownership.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Integration audit ensuring KSP crates do not bypass the logging facade.
|
||||||
|
|
||||||
|
fn collect_rust_files(directory: &std::path::Path, files: &mut std::vec::Vec<std::path::PathBuf>) {
|
||||||
|
let entries_result = std::fs::read_dir(directory);
|
||||||
|
let entries = match entries_result {
|
||||||
|
std::result::Result::Ok(entries) => entries,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
for entry_result in entries {
|
||||||
|
let entry = match entry_result {
|
||||||
|
std::result::Result::Ok(entry) => entry,
|
||||||
|
std::result::Result::Err(_) => continue,
|
||||||
|
};
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
collect_rust_files(path.as_path(), files);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if path.extension().and_then(std::ffi::OsStr::to_str) == std::option::Option::Some("rs") {
|
||||||
|
files.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn workspace_crates_do_not_bypass_ksp_logging_facade() {
|
||||||
|
let logging_manifest_directory = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
let workspace_root = match logging_manifest_directory.parent().and_then(std::path::Path::parent) {
|
||||||
|
std::option::Option::Some(root) => root,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let crates_directory = workspace_root.join("crates");
|
||||||
|
let entries_result = std::fs::read_dir(crates_directory.as_path());
|
||||||
|
assert!(entries_result.is_ok(), "unable to inspect workspace crates at {}", crates_directory.display());
|
||||||
|
let entries = match entries_result {
|
||||||
|
std::result::Result::Ok(entries) => entries,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
for entry_result in entries {
|
||||||
|
let entry = match entry_result {
|
||||||
|
std::result::Result::Ok(entry) => entry,
|
||||||
|
std::result::Result::Err(_) => continue,
|
||||||
|
};
|
||||||
|
let crate_path = entry.path();
|
||||||
|
if !crate_path.is_dir() || entry.file_name() == std::ffi::OsStr::new("ksp-logging-lib") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let manifest_path = crate_path.join("Cargo.toml");
|
||||||
|
if manifest_path.exists() {
|
||||||
|
let manifest_result = std::fs::read_to_string(manifest_path.as_path());
|
||||||
|
assert!(manifest_result.is_ok(), "unable to read {}", manifest_path.display());
|
||||||
|
let manifest = match manifest_result {
|
||||||
|
std::result::Result::Ok(manifest) => manifest,
|
||||||
|
std::result::Result::Err(_) => continue,
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
!manifest.contains("tracing.workspace") && !manifest.contains("\ntracing =") && !manifest.contains("[dependencies.tracing]"),
|
||||||
|
"{} depends directly on tracing",
|
||||||
|
manifest_path.display(),
|
||||||
|
);
|
||||||
|
assert!(!manifest.contains("tracing-subscriber"), "{} depends directly on tracing-subscriber", manifest_path.display());
|
||||||
|
assert!(!manifest.contains("tracing-appender"), "{} depends directly on tracing-appender", manifest_path.display());
|
||||||
|
}
|
||||||
|
let mut rust_files = std::vec::Vec::new();
|
||||||
|
collect_rust_files(crate_path.as_path(), &mut rust_files);
|
||||||
|
for rust_file in rust_files {
|
||||||
|
let source_result = std::fs::read_to_string(rust_file.as_path());
|
||||||
|
assert!(source_result.is_ok(), "unable to read {}", rust_file.display());
|
||||||
|
let source = match source_result {
|
||||||
|
std::result::Result::Ok(source) => source,
|
||||||
|
std::result::Result::Err(_) => continue,
|
||||||
|
};
|
||||||
|
assert!(!source.contains("tracing::"), "{} bypasses ksp-logging-lib via tracing", rust_file.display());
|
||||||
|
assert!(!source.contains("tracing_subscriber::"), "{} bypasses ksp-logging-lib via tracing-subscriber", rust_file.display());
|
||||||
|
assert!(!source.contains("tracing_appender::"), "{} bypasses ksp-logging-lib via tracing-appender", rust_file.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
67
crates/ksp-logging-lib/tests/public_api.rs
Normal file
67
crates/ksp-logging-lib/tests/public_api.rs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
// file: crates/ksp-logging-lib/tests/public_api.rs
|
||||||
|
// version: 4
|
||||||
|
|
||||||
|
//! Integration tests for the public crate-root surface of `ksp-logging-lib`.
|
||||||
|
|
||||||
|
const TEST_TARGET: &str = "ksp-logging-lib";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn public_settings_surface_is_usable() {
|
||||||
|
let settings = ksp_logging_lib::LoggingSettings::new(
|
||||||
|
ksp_logging_lib::LogFilterLevel::Info,
|
||||||
|
ksp_logging_lib::SpanEvents::NewAndClose,
|
||||||
|
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stdout()),
|
||||||
|
std::option::Option::Some(ksp_logging_lib::FileSettings::new("logs", "ksp", ksp_logging_lib::FileRotation::Daily)),
|
||||||
|
)
|
||||||
|
.with_target_filter(ksp_logging_lib::TargetFilter::new(TEST_TARGET, ksp_logging_lib::LogFilterLevel::Trace));
|
||||||
|
assert!(settings.validate().is_ok());
|
||||||
|
assert_eq!(settings.default_filter(), ksp_logging_lib::LogFilterLevel::Info);
|
||||||
|
assert_eq!(settings.console().map(ksp_logging_lib::ConsoleSettings::output), std::option::Option::Some(ksp_logging_lib::ConsoleOutput::Stdout));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn public_event_macros_are_usable() {
|
||||||
|
ksp_logging_lib::error!(target: TEST_TARGET, operation = "public_api", "error event");
|
||||||
|
ksp_logging_lib::warn!(target: TEST_TARGET, operation = "public_api", "warn event");
|
||||||
|
ksp_logging_lib::info!(target: TEST_TARGET, operation = "public_api", "info event");
|
||||||
|
ksp_logging_lib::debug!(target: TEST_TARGET, operation = "public_api", "debug event");
|
||||||
|
ksp_logging_lib::trace!(target: TEST_TARGET, operation = "public_api", "trace event");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn public_span_surface_is_usable_for_sync_and_async() {
|
||||||
|
let span = ksp_logging_lib::trace_span!(target: TEST_TARGET, "public_sync", domain = "logging");
|
||||||
|
let value = span.in_scope(|| -> u32 {
|
||||||
|
return 7;
|
||||||
|
});
|
||||||
|
assert_eq!(value, 7);
|
||||||
|
let async_span = ksp_logging_lib::debug_span!(target: TEST_TARGET, "public_async", component = "test");
|
||||||
|
let future = ksp_logging_lib::instrument(async_span, std::future::ready(9_u32));
|
||||||
|
let mut future = std::boxed::Box::pin(future);
|
||||||
|
let waker = std::task::Waker::noop();
|
||||||
|
let mut context = std::task::Context::from_waker(waker);
|
||||||
|
let poll = std::future::Future::poll(future.as_mut(), &mut context);
|
||||||
|
assert_eq!(poll, std::task::Poll::Ready(9_u32));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn all_span_levels_are_usable() {
|
||||||
|
let _error = ksp_logging_lib::error_span!(target: TEST_TARGET, "error_span");
|
||||||
|
let _warn = ksp_logging_lib::warn_span!(target: TEST_TARGET, "warn_span");
|
||||||
|
let _info = ksp_logging_lib::info_span!(target: TEST_TARGET, "info_span");
|
||||||
|
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;
|
||||||
|
let _file_initialization_failed = ksp_logging_lib::ERROR_CODE_FILE_OUTPUT_INITIALIZATION_FAILED;
|
||||||
|
let dropped = ksp_logging_lib::DroppedLines::zero();
|
||||||
|
assert_eq!(dropped.console(), 0);
|
||||||
|
assert_eq!(dropped.file(), 0);
|
||||||
|
assert_eq!(dropped.total(), 0);
|
||||||
|
}
|
||||||
196
crates/ksp-logging-lib/tests/runtime.rs
Normal file
196
crates/ksp-logging-lib/tests/runtime.rs
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
// file: crates/ksp-logging-lib/tests/runtime.rs
|
||||||
|
// version: 4
|
||||||
|
|
||||||
|
//! Integration tests for global initialization, takeover filtering, non-blocking outputs 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_root_directory() -> std::path::PathBuf {
|
||||||
|
return std::env::temp_dir().join(format!("ksp-logging-lib-runtime-{}", std::process::id()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset_directory(path: &std::path::Path) {
|
||||||
|
if path.exists() {
|
||||||
|
let remove_result = std::fs::remove_dir_all(path);
|
||||||
|
assert!(remove_result.is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_directory_text(path: &std::path::Path) -> std::string::String {
|
||||||
|
let read_result = std::fs::read_dir(path);
|
||||||
|
let entries = match read_result {
|
||||||
|
std::result::Result::Ok(entries) => entries,
|
||||||
|
std::result::Result::Err(_) => return std::string::String::new(),
|
||||||
|
};
|
||||||
|
let mut output = std::string::String::new();
|
||||||
|
for entry_result in entries {
|
||||||
|
let entry = match entry_result {
|
||||||
|
std::result::Result::Ok(entry) => entry,
|
||||||
|
std::result::Result::Err(_) => continue,
|
||||||
|
};
|
||||||
|
let file_type = match entry.file_type() {
|
||||||
|
std::result::Result::Ok(file_type) => file_type,
|
||||||
|
std::result::Result::Err(_) => continue,
|
||||||
|
};
|
||||||
|
if !file_type.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let content = match std::fs::read_to_string(entry.path()) {
|
||||||
|
std::result::Result::Ok(content) => content,
|
||||||
|
std::result::Result::Err(_) => continue,
|
||||||
|
};
|
||||||
|
output.push_str(content.as_str());
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exercise_concurrent_reload(guard: &mut ksp_logging_lib::LoggingGuard, disabled: &ksp_logging_lib::LoggingSettings) {
|
||||||
|
let quiet_console = ksp_logging_lib::LoggingSettings::new(
|
||||||
|
ksp_logging_lib::LogFilterLevel::Off,
|
||||||
|
ksp_logging_lib::SpanEvents::Off,
|
||||||
|
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
|
||||||
|
std::option::Option::None,
|
||||||
|
);
|
||||||
|
let quiet_reload = ksp_logging_lib::reinitialize(guard, &quiet_console);
|
||||||
|
assert!(quiet_reload.is_ok());
|
||||||
|
let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||||
|
let barrier = std::sync::Arc::new(std::sync::Barrier::new(5));
|
||||||
|
let mut threads = std::vec::Vec::new();
|
||||||
|
for worker_index in 0..4_u32 {
|
||||||
|
let worker_stop = std::sync::Arc::clone(&stop);
|
||||||
|
let worker_barrier = std::sync::Arc::clone(&barrier);
|
||||||
|
threads.push(std::thread::spawn(move || {
|
||||||
|
worker_barrier.wait();
|
||||||
|
let mut sequence = 0_u64;
|
||||||
|
while !worker_stop.load(std::sync::atomic::Ordering::Relaxed) {
|
||||||
|
ksp_logging_lib::trace!(target: LOGGING_TARGET, worker_index, sequence, "concurrent reload probe");
|
||||||
|
sequence = sequence.wrapping_add(1);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
barrier.wait();
|
||||||
|
let mut reloads_succeeded = true;
|
||||||
|
for reload_index in 0..32_u32 {
|
||||||
|
let settings = if reload_index % 2 == 0 { &quiet_console } else { disabled };
|
||||||
|
let reload_result = ksp_logging_lib::reinitialize(guard, settings);
|
||||||
|
if reload_result.is_err() {
|
||||||
|
reloads_succeeded = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stop.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
let mut joins_succeeded = true;
|
||||||
|
for thread in threads {
|
||||||
|
if thread.join().is_err() {
|
||||||
|
joins_succeeded = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(reloads_succeeded);
|
||||||
|
assert!(joins_succeeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn global_runtime_supports_takeover_non_blocking_outputs_hot_reload_and_single_initialization() {
|
||||||
|
let root = test_root_directory();
|
||||||
|
reset_directory(root.as_path());
|
||||||
|
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_eq!(guard.dropped_lines(), ksp_logging_lib::DroppedLines::zero());
|
||||||
|
assert!(!logging_trace_enabled());
|
||||||
|
assert!(!other_ksp_info_enabled());
|
||||||
|
assert!(!external_error_enabled());
|
||||||
|
let console_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, &console_enabled);
|
||||||
|
assert!(reload_result.is_ok());
|
||||||
|
assert_eq!(guard.settings(), &console_enabled);
|
||||||
|
assert!(logging_trace_enabled());
|
||||||
|
assert!(other_ksp_info_enabled());
|
||||||
|
assert!(!other_ksp_debug_enabled());
|
||||||
|
assert!(!external_error_enabled());
|
||||||
|
let blocked_directory = root.join("not-a-directory");
|
||||||
|
let create_root = std::fs::create_dir_all(root.as_path());
|
||||||
|
assert!(create_root.is_ok());
|
||||||
|
let create_blocker = std::fs::write(blocked_directory.as_path(), b"file blocks directory creation");
|
||||||
|
assert!(create_blocker.is_ok());
|
||||||
|
let invalid_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(blocked_directory.as_path(), "invalid", ksp_logging_lib::FileRotation::Daily)),
|
||||||
|
);
|
||||||
|
let failed_reload = ksp_logging_lib::reinitialize(&mut guard, &invalid_file);
|
||||||
|
assert!(failed_reload.is_err());
|
||||||
|
let file_error = match failed_reload {
|
||||||
|
std::result::Result::Ok(()) => return,
|
||||||
|
std::result::Result::Err(error) => error,
|
||||||
|
};
|
||||||
|
assert_eq!(file_error.code(), ksp_logging_lib::ERROR_CODE_FILE_OUTPUT_INITIALIZATION_FAILED);
|
||||||
|
assert_eq!(guard.settings(), &console_enabled);
|
||||||
|
assert!(logging_trace_enabled());
|
||||||
|
assert!(!external_error_enabled());
|
||||||
|
exercise_concurrent_reload(&mut guard, &disabled);
|
||||||
|
let log_directory = root.join("logs");
|
||||||
|
let file_enabled = ksp_logging_lib::LoggingSettings::new(
|
||||||
|
ksp_logging_lib::LogFilterLevel::Info,
|
||||||
|
ksp_logging_lib::SpanEvents::Off,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::Some(ksp_logging_lib::FileSettings::new(log_directory.as_path(), "runtime-test.log", ksp_logging_lib::FileRotation::Never)),
|
||||||
|
);
|
||||||
|
let file_reload = ksp_logging_lib::reinitialize(&mut guard, &file_enabled);
|
||||||
|
assert!(file_reload.is_ok());
|
||||||
|
ksp_logging_lib::info!(target: LOGGING_TARGET, "file \x1b[31moutput\x1b[0m marker");
|
||||||
|
tracing::error!(target: EXTERNAL_TARGET, "external marker must remain silent");
|
||||||
|
let disable_after_file = ksp_logging_lib::reinitialize(&mut guard, &disabled);
|
||||||
|
assert!(disable_after_file.is_ok());
|
||||||
|
let file_text = read_directory_text(log_directory.as_path());
|
||||||
|
assert!(file_text.contains("file output marker"));
|
||||||
|
assert!(file_text.contains(LOGGING_TARGET));
|
||||||
|
assert!(file_text.contains("runtime.rs"));
|
||||||
|
assert!(!file_text.contains("\x1b["));
|
||||||
|
assert!(!file_text.contains("external marker must remain silent"));
|
||||||
|
let dropped = guard.dropped_lines();
|
||||||
|
assert_eq!(dropped.total(), dropped.console().saturating_add(dropped.file()));
|
||||||
|
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);
|
||||||
|
reset_directory(root.as_path());
|
||||||
|
}
|
||||||
73
crates/ksp-logging-lib/tests/span_lifecycle.rs
Normal file
73
crates/ksp-logging-lib/tests/span_lifecycle.rs
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
// file: crates/ksp-logging-lib/tests/span_lifecycle.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Integration tests for formatted KSP span lifecycle timing output.
|
||||||
|
|
||||||
|
use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import
|
||||||
|
|
||||||
|
const TEST_TARGET: &str = "ksp-logging-lib";
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct SharedWriter {
|
||||||
|
buffer: std::sync::Arc<std::sync::Mutex<std::vec::Vec<u8>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SharedWriter {
|
||||||
|
fn new(buffer: std::sync::Arc<std::sync::Mutex<std::vec::Vec<u8>>>) -> Self {
|
||||||
|
return Self { buffer };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::io::Write for SharedWriter {
|
||||||
|
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
|
||||||
|
let lock_result = self.buffer.lock();
|
||||||
|
let mut buffer = match lock_result {
|
||||||
|
std::result::Result::Ok(buffer) => buffer,
|
||||||
|
std::result::Result::Err(_) => return std::result::Result::Err(std::io::Error::other("span test buffer is poisoned")),
|
||||||
|
};
|
||||||
|
buffer.extend_from_slice(bytes);
|
||||||
|
return std::result::Result::Ok(bytes.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> std::io::Result<()> {
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn captured_text(buffer: &std::sync::Arc<std::sync::Mutex<std::vec::Vec<u8>>>) -> std::string::String {
|
||||||
|
let lock_result = buffer.lock();
|
||||||
|
let bytes = match lock_result {
|
||||||
|
std::result::Result::Ok(bytes) => bytes.clone(),
|
||||||
|
std::result::Result::Err(error) => error.into_inner().clone(),
|
||||||
|
};
|
||||||
|
return std::string::String::from_utf8_lossy(bytes.as_slice()).into_owned();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn new_and_close_span_events_expose_busy_and_idle_timing_fields() {
|
||||||
|
let buffer = std::sync::Arc::new(std::sync::Mutex::new(std::vec::Vec::new()));
|
||||||
|
let writer_buffer = std::sync::Arc::clone(&buffer);
|
||||||
|
let layer = tracing_subscriber::fmt::layer()
|
||||||
|
.with_writer(move || -> SharedWriter {
|
||||||
|
return SharedWriter::new(std::sync::Arc::clone(&writer_buffer));
|
||||||
|
})
|
||||||
|
.with_ansi(false)
|
||||||
|
.with_target(true)
|
||||||
|
.with_span_events(tracing_subscriber::fmt::format::FmtSpan::NEW | tracing_subscriber::fmt::format::FmtSpan::CLOSE);
|
||||||
|
let subscriber = tracing_subscriber::registry().with(layer);
|
||||||
|
tracing::subscriber::with_default(subscriber, || {
|
||||||
|
let span = ksp_logging_lib::trace_span!(target: TEST_TARGET, "timed_scope", domain = "logging");
|
||||||
|
span.in_scope(|| {
|
||||||
|
std::hint::black_box(42_u32);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
drop(span);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
let text = captured_text(&buffer);
|
||||||
|
assert!(text.contains("timed_scope"));
|
||||||
|
assert!(text.contains("new"));
|
||||||
|
assert!(text.contains("close"));
|
||||||
|
assert!(text.contains("time.busy"));
|
||||||
|
assert!(text.contains("time.idle"));
|
||||||
|
}
|
||||||
118
crates/ksp-logging-lib/tests/tokio_span.rs
Normal file
118
crates/ksp-logging-lib/tests/tokio_span.rs
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
// file: crates/ksp-logging-lib/tests/tokio_span.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Integration tests for KSP span instrumentation on a real Tokio executor.
|
||||||
|
|
||||||
|
const TEST_TARGET: &str = "ksp-logging-lib";
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct CountingSubscriber {
|
||||||
|
enters: std::sync::Arc<std::sync::atomic::AtomicU64>,
|
||||||
|
exits: std::sync::Arc<std::sync::atomic::AtomicU64>,
|
||||||
|
next_id: std::sync::Arc<std::sync::atomic::AtomicU64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CountingSubscriber {
|
||||||
|
fn new(enters: std::sync::Arc<std::sync::atomic::AtomicU64>, exits: std::sync::Arc<std::sync::atomic::AtomicU64>) -> Self {
|
||||||
|
return Self { enters, exits, next_id: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(1)) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl tracing::Subscriber for CountingSubscriber {
|
||||||
|
fn enabled(&self, _metadata: &tracing::Metadata<'_>) -> bool {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_span(&self, _span: &tracing::span::Attributes<'_>) -> tracing::span::Id {
|
||||||
|
let id = self.next_id.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
return tracing::span::Id::from_u64(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record(&self, _span: &tracing::span::Id, _values: &tracing::span::Record<'_>) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_follows_from(&self, _span: &tracing::span::Id, _follows: &tracing::span::Id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event(&self, _event: &tracing::Event<'_>) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enter(&self, _span: &tracing::span::Id) {
|
||||||
|
self.enters.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exit(&self, _span: &tracing::span::Id) {
|
||||||
|
self.exits.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_span(enters: std::sync::Arc<std::sync::atomic::AtomicU64>, exits: std::sync::Arc<std::sync::atomic::AtomicU64>) -> ksp_logging_lib::Span {
|
||||||
|
let subscriber = CountingSubscriber::new(enters, exits);
|
||||||
|
return tracing::subscriber::with_default(subscriber, || -> ksp_logging_lib::Span {
|
||||||
|
return ksp_logging_lib::trace_span!(target: TEST_TARGET, "tokio_runtime_span", domain = "logging", executor = "tokio");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn instrumented_span_reenters_across_real_tokio_suspensions() {
|
||||||
|
let enters = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
|
||||||
|
let exits = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
|
||||||
|
let span = test_span(std::sync::Arc::clone(&enters), std::sync::Arc::clone(&exits));
|
||||||
|
let observed_enters = std::sync::Arc::clone(&enters);
|
||||||
|
let future = ksp_logging_lib::instrument(span, async move {
|
||||||
|
assert!(observed_enters.load(std::sync::atomic::Ordering::Relaxed) >= 1);
|
||||||
|
tokio::task::yield_now().await;
|
||||||
|
assert!(observed_enters.load(std::sync::atomic::Ordering::Relaxed) >= 2);
|
||||||
|
tokio::task::yield_now().await;
|
||||||
|
assert!(observed_enters.load(std::sync::atomic::Ordering::Relaxed) >= 3);
|
||||||
|
return 42_u32;
|
||||||
|
});
|
||||||
|
let value = future.await;
|
||||||
|
assert_eq!(value, 42_u32);
|
||||||
|
let enter_count = enters.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
|
let exit_count = exits.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
|
assert!(enter_count >= 3);
|
||||||
|
assert_eq!(enter_count, exit_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn instrumented_spans_are_usable_on_tokio_multithread_runtime() {
|
||||||
|
let enters = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
|
||||||
|
let exits = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
|
||||||
|
let first_span = test_span(std::sync::Arc::clone(&enters), std::sync::Arc::clone(&exits));
|
||||||
|
let second_span = test_span(std::sync::Arc::clone(&enters), std::sync::Arc::clone(&exits));
|
||||||
|
let first_task = tokio::spawn(ksp_logging_lib::instrument(first_span, async {
|
||||||
|
for _iteration in 0..32 {
|
||||||
|
tokio::task::yield_now().await;
|
||||||
|
}
|
||||||
|
return 20_u32;
|
||||||
|
}));
|
||||||
|
let second_task = tokio::spawn(ksp_logging_lib::instrument(second_span, async {
|
||||||
|
for _iteration in 0..32 {
|
||||||
|
tokio::task::yield_now().await;
|
||||||
|
}
|
||||||
|
return 22_u32;
|
||||||
|
}));
|
||||||
|
let first_result = first_task.await;
|
||||||
|
assert!(first_result.is_ok(), "first Tokio task must complete successfully");
|
||||||
|
let first_value = match first_result {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let second_result = second_task.await;
|
||||||
|
assert!(second_result.is_ok(), "second Tokio task must complete successfully");
|
||||||
|
let second_value = match second_result {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
assert_eq!(first_value + second_value, 42_u32);
|
||||||
|
let enter_count = enters.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
|
let exit_count = exits.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
|
assert!(enter_count >= 4);
|
||||||
|
assert_eq!(enter_count, exit_count);
|
||||||
|
}
|
||||||
194
crates/ksp-logging-lib/unit_tests/runtime.rs
Normal file
194
crates/ksp-logging-lib/unit_tests/runtime.rs
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
// file: crates/ksp-logging-lib/unit_tests/runtime.rs
|
||||||
|
// version: 5
|
||||||
|
|
||||||
|
#[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_rotation_mapping_covers_supported_cadences() {
|
||||||
|
assert_eq!(super::map_file_rotation(crate::FileRotation::Never), tracing_appender::rolling::Rotation::NEVER);
|
||||||
|
assert_eq!(super::map_file_rotation(crate::FileRotation::Hourly), tracing_appender::rolling::Rotation::HOURLY);
|
||||||
|
assert_eq!(super::map_file_rotation(crate::FileRotation::Daily), tracing_appender::rolling::Rotation::DAILY);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn disabled_runtime_has_no_layers_or_outputs() {
|
||||||
|
let settings = crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::option::Option::None);
|
||||||
|
let result = super::prepare_runtime(&settings);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let prepared = match result {
|
||||||
|
std::result::Result::Ok(prepared) => prepared,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
assert!(prepared.layers.is_empty());
|
||||||
|
assert!(prepared.outputs.console.is_none());
|
||||||
|
assert!(prepared.outputs.file.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn console_runtime_composes_takeover_filter_before_formatter_and_owns_guard() {
|
||||||
|
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(&settings);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let prepared = match result {
|
||||||
|
std::result::Result::Ok(prepared) => prepared,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dropped_line_snapshots_add_saturating_by_sink() {
|
||||||
|
let first = crate::DroppedLines { console: usize::MAX, file: 4 };
|
||||||
|
let second = crate::DroppedLines { console: 1, file: 7 };
|
||||||
|
let combined = first.saturating_add(second);
|
||||||
|
assert_eq!(combined.console(), usize::MAX);
|
||||||
|
assert_eq!(combined.file(), 11);
|
||||||
|
assert_eq!(combined.total(), usize::MAX);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn takeover_filter_prefers_more_specific_ksp_prefixes_and_supports_off() {
|
||||||
|
let settings = crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::option::Option::None)
|
||||||
|
.with_target_filter(crate::TargetFilter::new("ksp-store-", crate::LogFilterLevel::Debug))
|
||||||
|
.with_target_filter(crate::TargetFilter::new("ksp-store-lib", crate::LogFilterLevel::Trace))
|
||||||
|
.with_target_filter(crate::TargetFilter::new("ksp-wallet-lib", crate::LogFilterLevel::Off));
|
||||||
|
let filter = super::build_target_filter(&settings);
|
||||||
|
assert!(filter.would_enable("ksp-store-other", &tracing::Level::DEBUG));
|
||||||
|
assert!(!filter.would_enable("ksp-store-other", &tracing::Level::TRACE));
|
||||||
|
assert!(filter.would_enable("ksp-store-lib", &tracing::Level::TRACE));
|
||||||
|
assert!(!filter.would_enable("ksp-wallet-lib", &tracing::Level::ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BlockingWriter {
|
||||||
|
first_write: bool,
|
||||||
|
started: std::sync::mpsc::SyncSender<()>,
|
||||||
|
release: std::sync::Arc<(std::sync::Mutex<bool>, std::sync::Condvar)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BlockingWriter {
|
||||||
|
fn new(started: std::sync::mpsc::SyncSender<()>, release: std::sync::Arc<(std::sync::Mutex<bool>, std::sync::Condvar)>) -> Self {
|
||||||
|
return Self { first_write: true, started, release };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::io::Write for BlockingWriter {
|
||||||
|
fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
|
||||||
|
if self.first_write {
|
||||||
|
self.first_write = false;
|
||||||
|
if self.started.send(()).is_err() {
|
||||||
|
return std::result::Result::Err(std::io::Error::other("unable to notify saturation test that the writer is blocked"));
|
||||||
|
}
|
||||||
|
let (lock, condition) = self.release.as_ref();
|
||||||
|
let lock_result = lock.lock();
|
||||||
|
let mut released = match lock_result {
|
||||||
|
std::result::Result::Ok(released) => released,
|
||||||
|
std::result::Result::Err(_) => {
|
||||||
|
return std::result::Result::Err(std::io::Error::other("saturation test release lock is poisoned"));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
while !*released {
|
||||||
|
let wait_result = condition.wait(released);
|
||||||
|
released = match wait_result {
|
||||||
|
std::result::Result::Ok(released) => released,
|
||||||
|
std::result::Result::Err(_) => {
|
||||||
|
return std::result::Result::Err(std::io::Error::other("saturation test release wait is poisoned"));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(buffer.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> std::io::Result<()> {
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn release_blocked_writer(release: &std::sync::Arc<(std::sync::Mutex<bool>, std::sync::Condvar)>) {
|
||||||
|
let (lock, condition) = release.as_ref();
|
||||||
|
let lock_result = lock.lock();
|
||||||
|
let mut released = match lock_result {
|
||||||
|
std::result::Result::Ok(released) => released,
|
||||||
|
std::result::Result::Err(error) => error.into_inner(),
|
||||||
|
};
|
||||||
|
*released = true;
|
||||||
|
condition.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lossy_non_blocking_builder_drops_lines_instead_of_blocking_a_stalled_producer() {
|
||||||
|
let (started_sender, started_receiver) = std::sync::mpsc::sync_channel(1);
|
||||||
|
let release = std::sync::Arc::new((std::sync::Mutex::new(false), std::sync::Condvar::new()));
|
||||||
|
let writer = BlockingWriter::new(started_sender, std::sync::Arc::clone(&release));
|
||||||
|
let (mut non_blocking, worker_guard) = super::non_blocking_builder("ksp-logging-saturation-test").buffered_lines_limit(1).finish(writer);
|
||||||
|
let error_counter = non_blocking.error_counter();
|
||||||
|
let first_write = std::io::Write::write_all(&mut non_blocking, b"block worker\n");
|
||||||
|
assert!(first_write.is_ok());
|
||||||
|
let writer_started = started_receiver.recv_timeout(std::time::Duration::from_secs(2));
|
||||||
|
assert!(writer_started.is_ok());
|
||||||
|
let mut producer = non_blocking.clone();
|
||||||
|
let (finished_sender, finished_receiver) = std::sync::mpsc::sync_channel(1);
|
||||||
|
let producer_thread = std::thread::spawn(move || {
|
||||||
|
let mut succeeded = true;
|
||||||
|
for _ in 0..1_024 {
|
||||||
|
let write_result = std::io::Write::write_all(&mut producer, b"queued line\n");
|
||||||
|
if write_result.is_err() {
|
||||||
|
succeeded = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _send_result = finished_sender.send(succeeded);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
let producer_finished = finished_receiver.recv_timeout(std::time::Duration::from_secs(2));
|
||||||
|
release_blocked_writer(&release);
|
||||||
|
let join_result = producer_thread.join();
|
||||||
|
assert!(join_result.is_ok());
|
||||||
|
assert_eq!(producer_finished, std::result::Result::Ok(true));
|
||||||
|
assert!(error_counter.dropped_lines() > 0);
|
||||||
|
drop(non_blocking);
|
||||||
|
drop(worker_guard);
|
||||||
|
}
|
||||||
102
crates/ksp-logging-lib/unit_tests/settings.rs
Normal file
102
crates/ksp-logging-lib/unit_tests/settings.rs
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
// file: crates/ksp-logging-lib/unit_tests/settings.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn level_variants_are_distinct() {
|
||||||
|
assert_ne!(crate::LogFilterLevel::Off, crate::LogFilterLevel::Error);
|
||||||
|
assert_ne!(crate::LogFilterLevel::Error, crate::LogFilterLevel::Warn);
|
||||||
|
assert_ne!(crate::LogFilterLevel::Warn, crate::LogFilterLevel::Info);
|
||||||
|
assert_ne!(crate::LogFilterLevel::Info, crate::LogFilterLevel::Debug);
|
||||||
|
assert_ne!(crate::LogFilterLevel::Debug, crate::LogFilterLevel::Trace);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn target_filter_preserves_prefix_and_level() {
|
||||||
|
let filter = crate::TargetFilter::new("ksp-store-lib", crate::LogFilterLevel::Trace);
|
||||||
|
assert_eq!(filter.target_prefix(), "ksp-store-lib");
|
||||||
|
assert_eq!(filter.level(), crate::LogFilterLevel::Trace);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn console_settings_select_requested_stream() {
|
||||||
|
assert_eq!(crate::ConsoleSettings::stdout().output(), crate::ConsoleOutput::Stdout);
|
||||||
|
assert_eq!(crate::ConsoleSettings::stderr().output(), crate::ConsoleOutput::Stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn file_settings_preserve_values() {
|
||||||
|
let settings = crate::FileSettings::new("logs", "ksp", crate::FileRotation::Daily);
|
||||||
|
assert_eq!(settings.directory(), std::path::Path::new("logs"));
|
||||||
|
assert_eq!(settings.file_name_prefix(), "ksp");
|
||||||
|
assert_eq!(settings.rotation(), crate::FileRotation::Daily);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn logging_settings_preserve_explicit_values() {
|
||||||
|
let settings = crate::LoggingSettings::new(
|
||||||
|
crate::LogFilterLevel::Info,
|
||||||
|
crate::SpanEvents::NewAndClose,
|
||||||
|
std::option::Option::Some(crate::ConsoleSettings::stdout()),
|
||||||
|
std::option::Option::Some(crate::FileSettings::new("logs", "ksp", crate::FileRotation::Hourly)),
|
||||||
|
)
|
||||||
|
.with_target_filter(crate::TargetFilter::new("ksp-store-lib", crate::LogFilterLevel::Trace));
|
||||||
|
assert_eq!(settings.default_filter(), crate::LogFilterLevel::Info);
|
||||||
|
assert_eq!(settings.span_events(), crate::SpanEvents::NewAndClose);
|
||||||
|
assert_eq!(settings.target_filters().len(), 1);
|
||||||
|
assert_eq!(settings.target_filters()[0].target_prefix(), "ksp-store-lib");
|
||||||
|
assert_eq!(settings.console(), std::option::Option::Some(&crate::ConsoleSettings::stdout()));
|
||||||
|
assert_eq!(settings.file().map(crate::FileSettings::rotation), std::option::Option::Some(crate::FileRotation::Hourly));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validation_rejects_empty_target_prefix() {
|
||||||
|
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("", crate::LogFilterLevel::Debug));
|
||||||
|
assert!(settings.validate().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validation_rejects_external_target_prefix() {
|
||||||
|
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("sqlx", crate::LogFilterLevel::Debug));
|
||||||
|
assert!(settings.validate().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validation_rejects_empty_file_prefix() {
|
||||||
|
let settings = crate::LoggingSettings::new(
|
||||||
|
crate::LogFilterLevel::Info,
|
||||||
|
crate::SpanEvents::Off,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::Some(crate::FileSettings::new("logs", "", crate::FileRotation::Never)),
|
||||||
|
);
|
||||||
|
assert!(settings.validate().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validation_accepts_ksp_outputs_and_filters() {
|
||||||
|
let settings = crate::LoggingSettings::new(
|
||||||
|
crate::LogFilterLevel::Info,
|
||||||
|
crate::SpanEvents::Full,
|
||||||
|
std::option::Option::Some(crate::ConsoleSettings::stderr()),
|
||||||
|
std::option::Option::Some(crate::FileSettings::new("logs", "worker", crate::FileRotation::Daily)),
|
||||||
|
)
|
||||||
|
.with_target_filter(crate::TargetFilter::new("ksp-worker-", crate::LogFilterLevel::Debug));
|
||||||
|
assert!(settings.validate().is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn settings_allow_logging_to_be_disabled() {
|
||||||
|
let settings = crate::LoggingSettings::new(crate::LogFilterLevel::Off, crate::SpanEvents::Off, std::option::Option::None, std::option::Option::None);
|
||||||
|
assert!(settings.validate().is_ok());
|
||||||
|
}
|
||||||
22
crates/ksp-logging-lib/unit_tests/span.rs
Normal file
22
crates/ksp-logging-lib/unit_tests/span.rs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// file: crates/ksp-logging-lib/unit_tests/span.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn synchronous_scope_returns_operation_value() {
|
||||||
|
let span = crate::Span::__from_tracing(tracing::info_span!("unit_test_span"));
|
||||||
|
let value = span.in_scope(|| -> u32 {
|
||||||
|
return 42;
|
||||||
|
});
|
||||||
|
assert_eq!(value, 42);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn async_instrumentation_returns_future_output() {
|
||||||
|
let span = crate::Span::__from_tracing(tracing::info_span!("unit_test_async_span"));
|
||||||
|
let future = crate::instrument(span, std::future::ready(42_u32));
|
||||||
|
let mut future = std::boxed::Box::pin(future);
|
||||||
|
let waker = std::task::Waker::noop();
|
||||||
|
let mut context = std::task::Context::from_waker(waker);
|
||||||
|
let poll = std::future::Future::poll(future.as_mut(), &mut context);
|
||||||
|
assert_eq!(poll, std::task::Poll::Ready(42_u32));
|
||||||
|
}
|
||||||
30
crates/ksp-logging-lib/unit_tests/writer.rs
Normal file
30
crates/ksp-logging-lib/unit_tests/writer.rs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// file: crates/ksp-logging-lib/unit_tests/writer.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ansi_writer_strips_csi_sequences() {
|
||||||
|
let mut writer = super::StripAnsiWriter::new(std::vec::Vec::<u8>::new());
|
||||||
|
let write_result = std::io::Write::write_all(&mut writer, b"before\x1b[31mred\x1b[0mafter");
|
||||||
|
assert!(write_result.is_ok());
|
||||||
|
assert_eq!(writer.into_inner(), b"beforeredafter");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ansi_writer_preserves_state_across_split_writes() {
|
||||||
|
let mut writer = super::StripAnsiWriter::new(std::vec::Vec::<u8>::new());
|
||||||
|
let first = std::io::Write::write_all(&mut writer, b"a\x1b[");
|
||||||
|
let second = std::io::Write::write_all(&mut writer, b"32mb");
|
||||||
|
assert!(first.is_ok());
|
||||||
|
assert!(second.is_ok());
|
||||||
|
assert_eq!(writer.into_inner(), b"ab");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ansi_writer_strips_osc_sequences_terminated_by_bell_or_st() {
|
||||||
|
let mut writer = super::StripAnsiWriter::new(std::vec::Vec::<u8>::new());
|
||||||
|
let first = std::io::Write::write_all(&mut writer, b"a\x1b]0;title\x07b");
|
||||||
|
let second = std::io::Write::write_all(&mut writer, b"c\x1b]8;;https://example.invalid\x1b\\d");
|
||||||
|
assert!(first.is_ok());
|
||||||
|
assert!(second.is_ok());
|
||||||
|
assert_eq!(writer.into_inner(), b"abcd");
|
||||||
|
}
|
||||||
162
deltas/0.1.2/pre.001-fix.001.md
Normal file
162
deltas/0.1.2/pre.001-fix.001.md
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
<!-- file: deltas/0.1.2/pre.001-fix.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta 0.1.2-pre.001-fix.001
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
Livraison précédente :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-pre.001
|
||||||
|
```
|
||||||
|
|
||||||
|
Ce correctif est documentaire et corrige le plan de `pre.001` sans réécrire son delta historique.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Corriger le cadrage Logging avant validation du plan afin de fixer :
|
||||||
|
|
||||||
|
- le takeover complet du logging/tracing KSP par `ksp-logging-lib` ;
|
||||||
|
- le target KSP explicite égal au nom Cargo de la crate propriétaire ;
|
||||||
|
- le silence par défaut des targets tiers et la réémission explicite des informations utiles par le composant KSP propriétaire ;
|
||||||
|
- console et fichier non bloquants avec guards et compteurs de lignes abandonnées ;
|
||||||
|
- le stripping ANSI des fichiers ;
|
||||||
|
- un `initialize` global unique suivi d'un hot reload via `reinitialize` sans second subscriber global ;
|
||||||
|
- une surface de spans KSP synchrones et async avec diagnostic de durée `NEW/CLOSE`, `busy` et `idle` ;
|
||||||
|
- la responsabilité des données loggées au caller, sans détection/redaction automatique par Logging.
|
||||||
|
|
||||||
|
Aucun développement fonctionnel de `ksp-logging-lib` n'est introduit par ce fix.
|
||||||
|
|
||||||
|
## Décisions prises
|
||||||
|
|
||||||
|
### Takeover tracing
|
||||||
|
|
||||||
|
`ksp-logging-lib` devient le seul propriétaire KSP direct de la stack tracing et la seule façade autorisée pour les événements/spans KSP.
|
||||||
|
|
||||||
|
Le subscriber applique une politique de takeover :
|
||||||
|
|
||||||
|
```text
|
||||||
|
external targets = Off by default
|
||||||
|
ksp-* targets = configured KSP default
|
||||||
|
specific ksp-* = optional override
|
||||||
|
```
|
||||||
|
|
||||||
|
KSP ne renomme pas un événement tiers. Lorsqu'un détail provenant d'une dépendance externe est utile, la crate KSP propriétaire le réémet sous son propre target.
|
||||||
|
|
||||||
|
Exemple attendu pour Store : les logs SQLx natifs sont désactivés/silencieux ; les opérations SQL utiles sont journalisées explicitement par `ksp-store-lib`, typiquement au niveau `trace`.
|
||||||
|
|
||||||
|
### Targets
|
||||||
|
|
||||||
|
Les macros événements et spans exigent un target explicite correspondant au nom Cargo de la crate propriétaire. `domain`, `component` et autres fields restent des subdivisions structurées, pas des remplacements du target.
|
||||||
|
|
||||||
|
### Settings runtime
|
||||||
|
|
||||||
|
`LoggingSettings` reste propriétaire de Logging et indépendant de Config. Il couvre niveau KSP default, overrides de target, sorties console/fichier et politique d'événements de spans.
|
||||||
|
|
||||||
|
Une future `ksp-config-lib` pourra construire ces settings puis appeler la façade Logging.
|
||||||
|
|
||||||
|
### Non-blocking
|
||||||
|
|
||||||
|
Console et fichier utilisent des writers non bloquants avec leurs `WorkerGuard` possédés par `LoggingGuard`.
|
||||||
|
|
||||||
|
Le mode retenu privilégie l'absence de backpressure sur le hot path : une saturation peut abandonner des lignes. Les `ErrorCounter` sont conservés afin que ces pertes restent observables.
|
||||||
|
|
||||||
|
### Stripping ANSI
|
||||||
|
|
||||||
|
Les fichiers passent par un stripping ANSI générique avant persistence. Logging ne dépend pas de Tauri ; cette protection évite seulement de persister des séquences de terminal déjà présentes dans les données écrites.
|
||||||
|
|
||||||
|
### Initialisation et hot reload
|
||||||
|
|
||||||
|
`initialize(settings)` installe le subscriber global une seule fois et retourne `LoggingGuard`.
|
||||||
|
|
||||||
|
Après succès, `reinitialize(&mut guard, settings)` ou une méthode équivalente peut être appelée 0..N fois. Elle ne réinstalle pas le subscriber global ; elle modifie les filters/layers/sinks de l'infrastructure déjà installée.
|
||||||
|
|
||||||
|
Le reload vise une sémantique transactionnelle : une nouvelle configuration invalide ou impossible à construire laisse l'ancienne configuration active.
|
||||||
|
|
||||||
|
Le mécanisme interne exact (`tracing_subscriber::reload` ciblé ou routing KSP dynamique) sera choisi par implémentation/tests selon correction et overhead, sans modifier le contrat public.
|
||||||
|
|
||||||
|
### Spans sync/async et durée
|
||||||
|
|
||||||
|
`0.1.2` inclut désormais une surface de spans KSP par niveau, sans dépendance directe `tracing` dans les crates consommatrices.
|
||||||
|
|
||||||
|
Le code sync doit pouvoir exécuter un scope dans un span. Le code async doit instrumenter la `Future` elle-même et ne pas maintenir un enter guard à travers `.await`.
|
||||||
|
|
||||||
|
Les settings permettent au minimum `Off` et `NewAndClose`; `NEW | CLOSE` fournit des repères de début/fin et, lorsque les timestamps sont actifs, le close fournit `busy`/`idle`. Cette capacité sert au diagnostic rapide de latence/blocage et n'est pas présentée comme un benchmark de précision absolue.
|
||||||
|
|
||||||
|
### Contenu sensible
|
||||||
|
|
||||||
|
`ksp-logging-lib` n'essaie pas de détecter ou redacter automatiquement les données sensibles. La crate appelante est responsable du contenu qu'elle choisit de logger.
|
||||||
|
|
||||||
|
## Fichiers ajoutés
|
||||||
|
|
||||||
|
- `deltas/0.1.2/pre.001-fix.001.md`
|
||||||
|
|
||||||
|
## Fichiers modifiés
|
||||||
|
|
||||||
|
- `docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md`
|
||||||
|
- `docs/rules/RULES_DEPENDENCIES.md`
|
||||||
|
- `docs/architecture/003-COMPONENT_CONTRACTS.md`
|
||||||
|
- `docs/architecture/005-DEPENDENCY_GRAPH.md`
|
||||||
|
|
||||||
|
## Fichiers supprimés
|
||||||
|
|
||||||
|
Aucun.
|
||||||
|
|
||||||
|
## Version Cargo
|
||||||
|
|
||||||
|
Aucune modification de `Cargo.toml`.
|
||||||
|
|
||||||
|
Ce fix est limité à la documentation et respecte `VER-ID-008` : `workspace.package.version` reste donc :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-pre.1
|
||||||
|
```
|
||||||
|
|
||||||
|
L'identifiant de livraison est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-pre.001-fix.001
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validations exécutées
|
||||||
|
|
||||||
|
- vérification du delta `pre.001` fourni et des règles de version/delta/archive de la base `0.1.1` ;
|
||||||
|
- vérification de la documentation officielle `tracing` indiquant que le subscriber global ne peut être installé qu'une fois ;
|
||||||
|
- vérification de `tracing-subscriber::reload` pour le remplacement runtime d'une Layer/Filter ;
|
||||||
|
- vérification de l'avertissement officiel contre `Span::enter()` conservé à travers `.await` ;
|
||||||
|
- vérification de l'instrumentation de `Future` fournie par `tracing::Instrument` ;
|
||||||
|
- vérification de `FmtSpan::NEW | FmtSpan::CLOSE` et des champs `busy`/`idle` au close lorsque les timestamps sont actifs ;
|
||||||
|
- vérification du writer non bloquant, de `WorkerGuard` et `ErrorCounter` dans `tracing-appender 0.2.5` ;
|
||||||
|
- contrôle des headers `file:` / `version:` des fichiers livrés ;
|
||||||
|
- contrôle de l'absence de modification Cargo dans ce fix documentaire ;
|
||||||
|
- contrôle du contenu de l'archive selon `VER-ARCHIVE-004`.
|
||||||
|
|
||||||
|
## Validations non exécutées
|
||||||
|
|
||||||
|
Aucune validation Cargo n'est applicable à ce correctif documentaire et aucun code fonctionnel Logging n'existe encore dans la livraison.
|
||||||
|
|
||||||
|
Les commandes suivantes restent à exécuter dès que les tranches de développement les rendent applicables :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
cargo check --workspace
|
||||||
|
cargo test --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo tree -p ksp-logging-lib
|
||||||
|
cargo tree -p ksp-logging-lib -d
|
||||||
|
cargo tree -p ksp-logging-lib -e features
|
||||||
|
```
|
||||||
|
|
||||||
|
## Questions ouvertes
|
||||||
|
|
||||||
|
Aucune question architecturale bloquante.
|
||||||
|
|
||||||
|
Restent à trancher par implémentation/tests dans les prereleases suivantes :
|
||||||
|
|
||||||
|
- mécanisme exact des macros spans/events préservant le callsite sans fuite de types `tracing` ;
|
||||||
|
- abstraction KSP exacte pour instrumenter les futures async ;
|
||||||
|
- composition reloadable interne la moins coûteuse ;
|
||||||
|
- API exacte d'observation des dropped lines.
|
||||||
|
|
||||||
|
Après validation de ce fix, la prochaine tranche reste `0.1.2-pre.002`.
|
||||||
375
deltas/0.1.2/pre.001.md
Normal file
375
deltas/0.1.2/pre.001.md
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
<!-- file: deltas/0.1.2/pre.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta 0.1.2-pre.001
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
Release stable/taguée attendue :
|
||||||
|
|
||||||
|
```text
|
||||||
|
v0.1.1
|
||||||
|
```
|
||||||
|
|
||||||
|
L'archive Gitea fournie `khadhroony-solana-project-v0.1.1.zip` contient bien :
|
||||||
|
|
||||||
|
- `workspace.package.version = "0.1.1"` ;
|
||||||
|
- le delta final `deltas/0.1.1/rel.001.md` ;
|
||||||
|
- le prompt final `prompts/002-V0_1_2_START_PROMPT.md` ;
|
||||||
|
- la surface Core stabilisée attendue.
|
||||||
|
|
||||||
|
Dans le workflow KSP, cette archive provient directement du tag correspondant et constitue la base stable suffisante pour ouvrir `0.1.2`.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Ouvrir `0.1.2` par la prerelease obligatoire de brainstorming, audit et planification, sans développement fonctionnel Logging.
|
||||||
|
|
||||||
|
Cette tranche :
|
||||||
|
|
||||||
|
- inventorie l'état réel du workspace et confirme l'absence actuelle de `ksp-logging-lib` ;
|
||||||
|
- audite la stack `tracing` officielle actuelle ;
|
||||||
|
- fixe la frontière façade/instrumentation/runtime subscriber ;
|
||||||
|
- retient les macros KSP pour préserver les callsites ;
|
||||||
|
- définit les niveaux et settings runtime candidats ;
|
||||||
|
- borne la sémantique de `target`, `domain` et `component` ;
|
||||||
|
- retient le filtering global + target-prefix via `Targets` ;
|
||||||
|
- retient console + fichier optionnel ;
|
||||||
|
- retient un writer fichier non bloquant non-lossy avec guard possédé explicitement ;
|
||||||
|
- définit le lifecycle d'initialisation/réinitialisation ;
|
||||||
|
- fixe la stratégie d'erreurs Core et de protection des secrets ;
|
||||||
|
- dimensionne `pre.002` à `pre.006` ;
|
||||||
|
- confirme les hors-scope.
|
||||||
|
|
||||||
|
Le détail est consigné dans `docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md`.
|
||||||
|
|
||||||
|
## Version Cargo
|
||||||
|
|
||||||
|
`workspace.package.version` passe de :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.1
|
||||||
|
```
|
||||||
|
|
||||||
|
à :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-pre.1
|
||||||
|
```
|
||||||
|
|
||||||
|
L'identifiant Cargo respecte SemVer sans zéro initial ; l'identifiant de livraison reste `0.1.2-pre.001`.
|
||||||
|
|
||||||
|
Le header de `Cargo.toml` passe de version 25 à 26.
|
||||||
|
|
||||||
|
Aucune dépendance `tracing*` n'est ajoutée par cette tranche de planification : elles seront introduites uniquement lorsque le code/tests de `ksp-logging-lib` les consommeront réellement.
|
||||||
|
|
||||||
|
## Fichiers ajoutés
|
||||||
|
|
||||||
|
- `docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md`
|
||||||
|
- `deltas/0.1.2/pre.001.md`
|
||||||
|
|
||||||
|
## Fichiers modifiés
|
||||||
|
|
||||||
|
- `Cargo.toml`
|
||||||
|
- `ROADMAP.md`
|
||||||
|
- `docs/plans/000-README.md`
|
||||||
|
|
||||||
|
## Fichiers supprimés
|
||||||
|
|
||||||
|
Aucun.
|
||||||
|
|
||||||
|
## Inventaire du workspace
|
||||||
|
|
||||||
|
État de la base stable auditée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace members
|
||||||
|
└── crates/ksp-core-lib
|
||||||
|
```
|
||||||
|
|
||||||
|
`ksp-logging-lib` n'existe pas encore.
|
||||||
|
|
||||||
|
Core fournit déjà les contrats nécessaires à Logging :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp_core_lib::ErrorCode
|
||||||
|
ksp_core_lib::ErrorContext
|
||||||
|
ksp_core_lib::Error
|
||||||
|
ksp_core_lib::Result<T>
|
||||||
|
ksp_core_lib::Pubkey
|
||||||
|
```
|
||||||
|
|
||||||
|
ainsi que les Program IDs fondamentaux et leur registre descriptif.
|
||||||
|
|
||||||
|
La relation retenue reste unidirectionnelle :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-logging-lib -> ksp-core-lib
|
||||||
|
ksp-core-lib -X-> ksp-logging-lib
|
||||||
|
```
|
||||||
|
|
||||||
|
## Audit externe tracing
|
||||||
|
|
||||||
|
Audit effectué le 2026-08-14 sur les publications/docs officielles Tokio `tracing`, docs.rs/crates.io et les manifests publiés.
|
||||||
|
|
||||||
|
Versions observées :
|
||||||
|
|
||||||
|
```text
|
||||||
|
tracing 0.1.44 rustc 1.65+
|
||||||
|
tracing-subscriber 0.3.23 rustc 1.65+
|
||||||
|
tracing-appender 0.2.5 rustc 1.63+
|
||||||
|
```
|
||||||
|
|
||||||
|
Contraintes candidates à revérifier au moment de l'ajout effectif :
|
||||||
|
|
||||||
|
```toml
|
||||||
|
tracing = { version = "^0.1", default-features = false, features = ["std"] }
|
||||||
|
tracing-subscriber = { version = "^0.3", default-features = false, features = ["fmt"] }
|
||||||
|
tracing-appender = { version = "^0.2", default-features = false }
|
||||||
|
```
|
||||||
|
|
||||||
|
Décisions de features :
|
||||||
|
|
||||||
|
- pas de `tracing-attributes`/`attributes` ;
|
||||||
|
- pas de `ansi` ;
|
||||||
|
- pas de `tracing-log` ;
|
||||||
|
- pas d'`env-filter` ;
|
||||||
|
- pas de JSON/Serde ;
|
||||||
|
- pas de chrono/time formatter via `tracing-subscriber` ;
|
||||||
|
- pas de `parking_lot` appender ;
|
||||||
|
- `tracing-appender` tire lui-même `tracing-subscriber` avec `default-features = false`, `fmt` et `std` ainsi que les dépendances internes nécessaires à son fonctionnement.
|
||||||
|
|
||||||
|
Aucune dépendance n'est ajoutée uniquement parce qu'elle figure dans l'architecture candidate.
|
||||||
|
|
||||||
|
## Décisions de planification
|
||||||
|
|
||||||
|
### Façade et callsites
|
||||||
|
|
||||||
|
La surface d'émission KSP sera :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp_logging_lib::error!
|
||||||
|
ksp_logging_lib::warn!
|
||||||
|
ksp_logging_lib::info!
|
||||||
|
ksp_logging_lib::debug!
|
||||||
|
ksp_logging_lib::trace!
|
||||||
|
```
|
||||||
|
|
||||||
|
Les événements ne seront pas émis par de simples fonctions wrappers qui déplaceraient les métadonnées source.
|
||||||
|
|
||||||
|
L'implémentation exacte des macros doit réussir un test d'intégration prouvant que file/module/line et target implicite restent ceux du consommateur.
|
||||||
|
|
||||||
|
### Champs structurés
|
||||||
|
|
||||||
|
- `target` : métadonnée native de routage/filtering, naturelle au callsite ou explicitement overridable ;
|
||||||
|
- `domain` : champ structuré KSP optionnel ;
|
||||||
|
- `component` : champ structuré KSP optionnel ;
|
||||||
|
- autres champs : ouverts selon besoin, sans taxonomie fermée.
|
||||||
|
|
||||||
|
`domain` et `component` ne deviennent pas des filtres dans `0.1.2`.
|
||||||
|
|
||||||
|
### Filtering
|
||||||
|
|
||||||
|
Première surface :
|
||||||
|
|
||||||
|
```text
|
||||||
|
default filter level
|
||||||
|
+ zero or more target-prefix overrides
|
||||||
|
```
|
||||||
|
|
||||||
|
`tracing_subscriber::filter::Targets` est retenu comme mécanisme initial.
|
||||||
|
|
||||||
|
`EnvFilter`, `RUST_LOG`, field-based filtering et hot reload restent hors scope.
|
||||||
|
|
||||||
|
### Settings runtime
|
||||||
|
|
||||||
|
Surface conceptuelle retenue :
|
||||||
|
|
||||||
|
```text
|
||||||
|
LogFilterLevel
|
||||||
|
TargetFilter
|
||||||
|
ConsoleOutput
|
||||||
|
ConsoleSettings
|
||||||
|
FileRotation
|
||||||
|
FileSettings
|
||||||
|
LoggingSettings
|
||||||
|
LoggingGuard
|
||||||
|
initialize(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
Les settings ne lisent ni fichier, ni environnement, ni profil Config et ne contiennent aucun secret.
|
||||||
|
|
||||||
|
### Console
|
||||||
|
|
||||||
|
Sortie console avec choix explicite stdout/stderr.
|
||||||
|
|
||||||
|
Le formatter initial reste humain, sans JSON ni ANSI obligatoire.
|
||||||
|
|
||||||
|
### Fichier
|
||||||
|
|
||||||
|
Sortie fichier optionnelle retenue avec :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Never | Hourly | Daily
|
||||||
|
```
|
||||||
|
|
||||||
|
Le builder fallible du `RollingFileAppender` doit être utilisé afin de remonter les erreurs au lieu de paniquer.
|
||||||
|
|
||||||
|
Le writer fichier utilise `NonBlockingBuilder` en mode :
|
||||||
|
|
||||||
|
```text
|
||||||
|
lossy(false)
|
||||||
|
```
|
||||||
|
|
||||||
|
La saturation applique donc de la backpressure plutôt que de supprimer silencieusement des logs.
|
||||||
|
|
||||||
|
### Lifecycle
|
||||||
|
|
||||||
|
`LoggingGuard` possède le ou les `WorkerGuard` nécessaires au backend non bloquant.
|
||||||
|
|
||||||
|
L'appelant conserve le guard jusqu'à la fin ordonnée du processus.
|
||||||
|
|
||||||
|
Le lifecycle global est volontairement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
uninitialized -> initialized -> process shutdown
|
||||||
|
```
|
||||||
|
|
||||||
|
Une initialisation répétée échoue avec une erreur KSP ; elle ne remplace pas silencieusement un subscriber existant et ne panique pas.
|
||||||
|
|
||||||
|
### Erreurs
|
||||||
|
|
||||||
|
Les erreurs Logging utilisent le contrat Core et restent dans le domaine :
|
||||||
|
|
||||||
|
```text
|
||||||
|
logging
|
||||||
|
```
|
||||||
|
|
||||||
|
Codes conceptuels initiaux :
|
||||||
|
|
||||||
|
```text
|
||||||
|
logging.invalid_settings
|
||||||
|
logging.already_initialized
|
||||||
|
logging.file_output_initialization_failed
|
||||||
|
```
|
||||||
|
|
||||||
|
Les causes externes utiles sont conservées via `Error::with_source(...)` lorsque possible.
|
||||||
|
|
||||||
|
### Secrets
|
||||||
|
|
||||||
|
Sont explicitement interdits dans les logs : clés privées, seeds/mnemonics, passwords/passphrases/PIN, tokens API/bearer/session, cookies/auth headers, secrets de chiffrement/signature, credentials de connexion et futurs `*_SECRET_*`.
|
||||||
|
|
||||||
|
Aucun helper de redaction universel n'est introduit : le caller doit omettre ou redacter explicitement la valeur avant émission.
|
||||||
|
|
||||||
|
Les settings Logging ne contiennent eux-mêmes aucun secret.
|
||||||
|
|
||||||
|
### Surface différée
|
||||||
|
|
||||||
|
Ne pas ajouter dans `0.1.2` sans nouveau besoin validé :
|
||||||
|
|
||||||
|
- spans KSP/`#[instrument]` ;
|
||||||
|
- OpenTelemetry ;
|
||||||
|
- JSON ;
|
||||||
|
- ANSI ;
|
||||||
|
- compatibilité `log` ;
|
||||||
|
- `EnvFilter` ;
|
||||||
|
- reload de filtre ;
|
||||||
|
- filtering par fields/domain ;
|
||||||
|
- rotation minutely/weekly/by-size ;
|
||||||
|
- compression/rétention complexe/latest symlink ;
|
||||||
|
- routes multiples avancées.
|
||||||
|
|
||||||
|
## Référence historique bot3
|
||||||
|
|
||||||
|
L'ancien `ks-logging` de l'archive bot3 fournie a été relu comme référence historique uniquement.
|
||||||
|
|
||||||
|
Éléments conservés comme leçons utiles :
|
||||||
|
|
||||||
|
- objet de lifecycle possédant les `WorkerGuard` ;
|
||||||
|
- console + fichier ;
|
||||||
|
- rotation ;
|
||||||
|
- filtering par targets.
|
||||||
|
|
||||||
|
Éléments non migrés :
|
||||||
|
|
||||||
|
- dépendance Logging -> Config ;
|
||||||
|
- document/schema JSON propre à Logging ;
|
||||||
|
- Serde/JSON pour la configuration ;
|
||||||
|
- routes/formats multiples non nécessaires à la première surface KSP.
|
||||||
|
|
||||||
|
## Prereleases prévues
|
||||||
|
|
||||||
|
```text
|
||||||
|
pre.001 audit + brainstorming + plan
|
||||||
|
pre.002 crate + settings + macros/façade
|
||||||
|
pre.003 subscriber + console + filtering + callsite final
|
||||||
|
pre.004 fichier + non-blocking + lifecycle
|
||||||
|
pre.005 intégration + tests + audits
|
||||||
|
pre.006 validation finale + docs/cleanup + prompt 0.1.3
|
||||||
|
```
|
||||||
|
|
||||||
|
Le découpage reste souple ; une tranche trop large sera scindée plutôt que surchargée.
|
||||||
|
|
||||||
|
## Hors scope confirmé
|
||||||
|
|
||||||
|
- Config/documents/profils ;
|
||||||
|
- Tauri ;
|
||||||
|
- Wallet/signing ;
|
||||||
|
- RPC/WS/providers ;
|
||||||
|
- Program decoding/execution ;
|
||||||
|
- Store/PostgreSQL ;
|
||||||
|
- Materializer ;
|
||||||
|
- workers/jobs/pipelines ;
|
||||||
|
- scenarios ;
|
||||||
|
- trading/ML ;
|
||||||
|
- observabilité distribuée/OpenTelemetry.
|
||||||
|
|
||||||
|
## Validations exécutées
|
||||||
|
|
||||||
|
Dans l'environnement de préparation de ce delta :
|
||||||
|
|
||||||
|
- lecture/audit de l'archive complète `0.1.1` fournie ;
|
||||||
|
- vérification statique de `workspace.package.version = "0.1.1"` ;
|
||||||
|
- vérification de la présence du delta `0.1.1/rel.001` et du prompt final `0.1.2` ;
|
||||||
|
- inventaire des membres workspace et confirmation de l'absence de `ksp-logging-lib` ;
|
||||||
|
- lecture des règles, plans, indexes et documents d'architecture demandés par le prompt ;
|
||||||
|
- lecture de `ksp-core-lib` et de son contrat Error/Result ;
|
||||||
|
- audit de l'ancien `ks-logging` bot3 fourni comme référence historique, sans le traiter comme source de vérité KSP ;
|
||||||
|
- vérification des versions/features/MSRV actuels de `tracing`, `tracing-subscriber` et `tracing-appender` depuis leurs sources de publication officielles ;
|
||||||
|
- audit du manifest publié de `tracing-appender` pour ses dépendances/features ;
|
||||||
|
- audit de `Targets`, `EnvFilter`, du non-blocking, du mode lossy/backpressure, de `WorkerGuard`, du builder fallible et de la rotation ;
|
||||||
|
- parsing TOML statique du manifest modifié ;
|
||||||
|
- contrôle statique des headers `file:` / `version:` des fichiers ajoutés/modifiés ;
|
||||||
|
- contrôle statique des liens Markdown locaux après modification ;
|
||||||
|
- contrôle du contenu de l'archive delta selon `VER-ARCHIVE-004`.
|
||||||
|
|
||||||
|
## Validations non exécutées
|
||||||
|
|
||||||
|
L'environnement de préparation ne contient ni `cargo` ni `rustc`.
|
||||||
|
|
||||||
|
Les commandes suivantes n'ont donc pas pu être exécutées ici :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
cargo check --workspace
|
||||||
|
cargo test --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo tree -p ksp-logging-lib
|
||||||
|
cargo tree -p ksp-logging-lib -d
|
||||||
|
cargo tree -p ksp-logging-lib -e features
|
||||||
|
```
|
||||||
|
|
||||||
|
Les trois commandes `cargo tree -p ksp-logging-lib` ne sont de toute façon applicables qu'après création effective de la crate.
|
||||||
|
|
||||||
|
Aucun succès Cargo n'est déclaré par ce delta.
|
||||||
|
|
||||||
|
## Questions ouvertes
|
||||||
|
|
||||||
|
Aucune question architecturale bloquante ne justifie de poursuivre le développement dans `pre.001`.
|
||||||
|
|
||||||
|
À confirmer par tests dans les tranches suivantes :
|
||||||
|
|
||||||
|
- mécanisme exact de macro KSP préservant le callsite avec la plus petite surface ;
|
||||||
|
- format visuel exact des lignes humaines sans le figer comme protocole ;
|
||||||
|
- nécessité future de capacités volontairement différées comme rétention, ANSI, JSON, `tracing-log`, `EnvFilter`, spans ou reload.
|
||||||
|
|
||||||
|
La prochaine tranche après validation de ce plan est `0.1.2-pre.002`.
|
||||||
125
deltas/0.1.2/pre.002-fix.001.md
Normal file
125
deltas/0.1.2/pre.002-fix.001.md
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<!-- file: deltas/0.1.2/pre.002-fix.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta 0.1.2-pre.002-fix.001
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
Livraison précédente :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-pre.002
|
||||||
|
```
|
||||||
|
|
||||||
|
Ce correctif traite uniquement les résultats de validation remontés après `pre.002`. Il ne modifie pas le périmètre fonctionnel de la prerelease et n'ouvre pas `pre.003`.
|
||||||
|
|
||||||
|
## Résultats de validation à corriger
|
||||||
|
|
||||||
|
Les commandes exécutées sur le workspace de développement ont montré :
|
||||||
|
|
||||||
|
- `cargo fmt --all` : exécuté sans erreur ;
|
||||||
|
- `cargo check --workspace` : réussi ;
|
||||||
|
- `cargo clippy --workspace --all-targets` : terminé avec quatre catégories de warnings à nettoyer dans Logging/tests ;
|
||||||
|
- `cargo test --workspace` : tous les tests Core et les tests unitaires Logging réussissent, mais `async_instrumentation_enters_and_exits_span_during_poll` échoue avec `enters = 2` au lieu de l'attente `1`.
|
||||||
|
|
||||||
|
## Cause du test async
|
||||||
|
|
||||||
|
Le test `pre.002` supposait qu'une future instrumentée n'entrait dans son span que pendant son unique `poll`.
|
||||||
|
|
||||||
|
Le contrat de `tracing::Instrument` est plus précis : la future instrumentée entre dans le span lors de chaque `poll` **et lors de son `Drop`**. Pour `std::future::ready(42_u32)`, le test observe donc :
|
||||||
|
|
||||||
|
```text
|
||||||
|
poll -> enter + exit
|
||||||
|
Drop -> enter + exit
|
||||||
|
```
|
||||||
|
|
||||||
|
Le compteur final `2` est donc conforme au comportement de `tracing`; c'est l'attente du test qui était incorrecte.
|
||||||
|
|
||||||
|
Le test corrigé vérifie séparément :
|
||||||
|
|
||||||
|
1. une paire `enter` / `exit` immédiatement après le `poll` ;
|
||||||
|
2. une deuxième paire après destruction explicite de la future instrumentée ;
|
||||||
|
3. l'équilibre final entre le nombre d'entrées et de sorties.
|
||||||
|
|
||||||
|
Le plan actif documente désormais explicitement cette sémantique afin qu'un futur test async ne réintroduise pas l'hypothèse erronée d'une seule paire `enter` / `exit` sur toute la durée de vie d'une future.
|
||||||
|
|
||||||
|
## Nettoyage Clippy
|
||||||
|
|
||||||
|
### `collapsible_if`
|
||||||
|
|
||||||
|
La validation du préfixe de fichier utilise désormais un `if let` avec condition chaînée compatible Rust 2024 au lieu de deux `if` imbriqués.
|
||||||
|
|
||||||
|
### `double_must_use`
|
||||||
|
|
||||||
|
L'attribut `#[must_use]` explicite de `ksp_logging_lib::instrument(...)` est supprimé : la fonction retourne déjà un type `Future`, lui-même marqué `must_use` par son contrat standard.
|
||||||
|
|
||||||
|
## Documentation des tests d'intégration
|
||||||
|
|
||||||
|
Les crates de tests d'intégration :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-logging-lib/tests/callsite.rs
|
||||||
|
crates/ksp-logging-lib/tests/public_api.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
reçoivent chacune une documentation crate-root `//! ...` afin de satisfaire `missing_docs = "warn"` lorsque les tests sont compilés comme crates séparées.
|
||||||
|
|
||||||
|
## Version Cargo
|
||||||
|
|
||||||
|
La version reste :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-pre.2
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune dépendance et aucun manifest ne sont modifiés.
|
||||||
|
|
||||||
|
L'identifiant de livraison de ce correctif est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-pre.002-fix.001
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers modifiés
|
||||||
|
|
||||||
|
- `crates/ksp-logging-lib/src/settings.rs`
|
||||||
|
- `crates/ksp-logging-lib/src/span.rs`
|
||||||
|
- `crates/ksp-logging-lib/tests/callsite.rs`
|
||||||
|
- `crates/ksp-logging-lib/tests/public_api.rs`
|
||||||
|
- `docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md`
|
||||||
|
|
||||||
|
## Fichier ajouté
|
||||||
|
|
||||||
|
- `deltas/0.1.2/pre.002-fix.001.md`
|
||||||
|
|
||||||
|
## Validations statiques exécutées lors de la préparation
|
||||||
|
|
||||||
|
- contrôle des headers `file:` / `version:` des fichiers du correctif ;
|
||||||
|
- contrôle que `Cargo.toml` n'est pas inclus dans le delta ;
|
||||||
|
- contrôle que la version Cargo de la base reste `0.1.2-pre.2` ;
|
||||||
|
- contrôle de l'absence de nouvelle dépendance ;
|
||||||
|
- contrôle que le correctif ne contient aucun ajout `unwrap`, `expect`, `panic` ou opérateur `?` dans le code production modifié ;
|
||||||
|
- contrôle que l'archive contient uniquement les cinq fichiers modifiés et le nouveau delta.
|
||||||
|
|
||||||
|
## Validations à réexécuter sur le workspace
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
cargo check --workspace
|
||||||
|
cargo test --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
```
|
||||||
|
|
||||||
|
Puis, pour compléter les validations prévues pour `pre.002` si elles ne l'ont pas encore été :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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 par ce delta.
|
||||||
|
|
||||||
|
## Suite
|
||||||
|
|
||||||
|
Une fois ce correctif validé, `0.1.2-pre.002` peut être considérée propre et la session peut passer à `0.1.2-pre.003` pour le subscriber runtime, le takeover, le filtering, la console non bloquante et la fondation du hot reload.
|
||||||
270
deltas/0.1.2/pre.002.md
Normal file
270
deltas/0.1.2/pre.002.md
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
<!-- file: deltas/0.1.2/pre.002.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta 0.1.2-pre.002
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
Livraison précédente validée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-pre.001-fix.001
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette tranche applique le plan corrigé de `pre.001` et ouvre le développement fonctionnel de `ksp-logging-lib` sans encore installer le subscriber runtime.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Créer la première surface fonctionnelle de Logging :
|
||||||
|
|
||||||
|
- créer `crates/ksp-logging-lib` et l'ajouter au workspace ;
|
||||||
|
- dépendre de `ksp-core-lib` pour le contrat commun d'erreur ;
|
||||||
|
- ajouter uniquement `tracing` parmi les dépendances de la stack de logging ;
|
||||||
|
- définir les settings runtime propres à Logging, indépendants de Config ;
|
||||||
|
- exposer les cinq niveaux d'événements par macros KSP avec `target:` explicite ;
|
||||||
|
- exposer les cinq niveaux de spans KSP ;
|
||||||
|
- fournir une abstraction `Span` KSP pour les scopes synchrones ;
|
||||||
|
- fournir `instrument(span, future)` pour l'instrumentation async sans demander au consumer d'utiliser `tracing::Instrument` ;
|
||||||
|
- vérifier par tests la préservation du callsite événement/span et le cycle enter/exit d'une future instrumentée.
|
||||||
|
|
||||||
|
Le subscriber global, le takeover effectif, le filtering runtime, les sorties console/fichier non bloquantes, les guards et le hot reload restent réservés aux prereleases suivantes conformément au plan.
|
||||||
|
|
||||||
|
## Version Cargo
|
||||||
|
|
||||||
|
`workspace.package.version` passe de :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-pre.1
|
||||||
|
```
|
||||||
|
|
||||||
|
à :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-pre.2
|
||||||
|
```
|
||||||
|
|
||||||
|
L'identifiant de livraison reste :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-pre.002
|
||||||
|
```
|
||||||
|
|
||||||
|
Le header du `Cargo.toml` racine passe de version 26 à 27.
|
||||||
|
|
||||||
|
## Dépendances
|
||||||
|
|
||||||
|
`tracing` est ajouté à la racine sous `[workspace.dependencies]` :
|
||||||
|
|
||||||
|
```toml
|
||||||
|
tracing = { version = "^0.1", default-features = false, features = ["std"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
`ksp-logging-lib` le consomme avec :
|
||||||
|
|
||||||
|
```toml
|
||||||
|
tracing.workspace = true
|
||||||
|
```
|
||||||
|
|
||||||
|
L'audit de la publication actuelle retient `tracing 0.1.44`. Les default features ne sont pas activées : `attributes` n'est pas nécessaire à cette tranche, car KSP n'utilise pas `#[instrument]`. La feature `std` suffit à la façade retenue et aux tests de subscriber local.
|
||||||
|
|
||||||
|
`tracing-subscriber` et `tracing-appender` ne sont pas ajoutés dans `pre.002` : ils ne sont pas encore consommés par du code runtime.
|
||||||
|
|
||||||
|
## Settings runtime
|
||||||
|
|
||||||
|
La surface publique introduit :
|
||||||
|
|
||||||
|
```text
|
||||||
|
LogFilterLevel
|
||||||
|
TargetFilter
|
||||||
|
SpanEvents
|
||||||
|
ConsoleOutput
|
||||||
|
ConsoleSettings
|
||||||
|
FileRotation
|
||||||
|
FileSettings
|
||||||
|
LoggingSettings
|
||||||
|
```
|
||||||
|
|
||||||
|
Ces types :
|
||||||
|
|
||||||
|
- appartiennent à `ksp-logging-lib` ;
|
||||||
|
- ne lisent aucun document Config ;
|
||||||
|
- ne consultent aucune variable d'environnement ;
|
||||||
|
- ne dépendent pas de `ksp-config-lib` ;
|
||||||
|
- utilisent des champs privés et une construction/getters explicites.
|
||||||
|
|
||||||
|
Une configuration sans console ni fichier est valide et représente un logging KSP désactivé. Les validations actuelles rejettent uniquement les ambiguïtés propres au contrat déjà fixé, notamment les préfixes de target vides/externes et un préfixe de fichier vide.
|
||||||
|
|
||||||
|
## Façade événements
|
||||||
|
|
||||||
|
Les macros crate-root suivantes sont introduites :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp_logging_lib::error!
|
||||||
|
ksp_logging_lib::warn!
|
||||||
|
ksp_logging_lib::info!
|
||||||
|
ksp_logging_lib::debug!
|
||||||
|
ksp_logging_lib::trace!
|
||||||
|
```
|
||||||
|
|
||||||
|
Leur syntaxe KSP exige `target:` explicitement. Elles délèguent directement aux macros `tracing` au point d'expansion afin que les métadonnées `file`, `module_path` et `line` correspondent au callsite consumer et non à une fonction wrapper dans Logging.
|
||||||
|
|
||||||
|
Un bridge `tracing` public mais caché de la documentation est nécessaire à l'expansion des macros depuis les crates consommatrices. Il est réservé à l'implémentation des macros ; `DEP-LOG-009` interdit son usage direct comme API consumer.
|
||||||
|
|
||||||
|
## Spans synchrones et async
|
||||||
|
|
||||||
|
Les macros suivantes sont introduites :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp_logging_lib::error_span!
|
||||||
|
ksp_logging_lib::warn_span!
|
||||||
|
ksp_logging_lib::info_span!
|
||||||
|
ksp_logging_lib::debug_span!
|
||||||
|
ksp_logging_lib::trace_span!
|
||||||
|
```
|
||||||
|
|
||||||
|
Elles exigent également `target:` explicitement et retournent `ksp_logging_lib::Span`.
|
||||||
|
|
||||||
|
Pour le synchrone :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Span::in_scope(operation)
|
||||||
|
```
|
||||||
|
|
||||||
|
entre dans le span pendant le scope puis en sort à la fin du scope.
|
||||||
|
|
||||||
|
Pour l'async :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp_logging_lib::instrument(span, future)
|
||||||
|
```
|
||||||
|
|
||||||
|
retourne une `Future` opaque instrumentée. Le span est entré pendant chaque poll de la future et quitté lorsque ce poll rend la main ; aucun enter guard KSP n'est destiné à être conservé à travers `.await`.
|
||||||
|
|
||||||
|
Cette surface prépare les diagnostics de durée `NEW/CLOSE`, `busy` et `idle` qui seront activés par le formatter/subscriber dans les tranches runtime suivantes.
|
||||||
|
|
||||||
|
## Erreurs
|
||||||
|
|
||||||
|
`ksp-logging-lib` utilise :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp_core_lib::Result<T>
|
||||||
|
ksp_core_lib::Error
|
||||||
|
ksp_core_lib::ErrorCode
|
||||||
|
```
|
||||||
|
|
||||||
|
Le premier code propre à Logging est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
logging.invalid_settings
|
||||||
|
```
|
||||||
|
|
||||||
|
Core ne reçoit aucune connaissance de Logging et aucune dépendance inverse n'est introduite.
|
||||||
|
|
||||||
|
## Tests ajoutés
|
||||||
|
|
||||||
|
### Unitaires
|
||||||
|
|
||||||
|
- distinction des niveaux ;
|
||||||
|
- construction/getters des target filters ;
|
||||||
|
- console stdout/stderr ;
|
||||||
|
- settings fichier/rotation ;
|
||||||
|
- conservation des settings explicites ;
|
||||||
|
- logging désactivé sans sink ;
|
||||||
|
- rejet des target prefixes vides ou externes ;
|
||||||
|
- rejet du préfixe fichier vide ;
|
||||||
|
- scope synchrone d'un span ;
|
||||||
|
- propagation du résultat d'une future instrumentée.
|
||||||
|
|
||||||
|
### Intégration
|
||||||
|
|
||||||
|
- surface publique des settings sans Config ;
|
||||||
|
- disponibilité des cinq macros événements ;
|
||||||
|
- disponibilité des cinq macros spans ;
|
||||||
|
- usage sync et async sans import consumer de `tracing::Span` ou `tracing::Instrument` ;
|
||||||
|
- préservation de `target`, `file`, `module_path` et `line` au callsite événement ;
|
||||||
|
- préservation de `target`, `file`, `module_path` et `line` au callsite span ;
|
||||||
|
- entrée puis sortie du span lors du poll d'une future instrumentée.
|
||||||
|
|
||||||
|
## Règles ajustées
|
||||||
|
|
||||||
|
`DEP-LOG-009` documente explicitement que le bridge `tracing` caché nécessaire aux macros est un détail d'implémentation de `ksp-logging-lib`, jamais une surface utilisable par une crate consommatrice.
|
||||||
|
|
||||||
|
Le plan `004-V0_1_2_LOGGING_FOUNDATION_PLAN.md` est synchronisé avec l'API effectivement retenue dans `pre.002` et avec la validité d'un logging entièrement désactivé.
|
||||||
|
|
||||||
|
## Fichiers ajoutés
|
||||||
|
|
||||||
|
- `crates/ksp-logging-lib/Cargo.toml`
|
||||||
|
- `crates/ksp-logging-lib/src/error.rs`
|
||||||
|
- `crates/ksp-logging-lib/src/lib.rs`
|
||||||
|
- `crates/ksp-logging-lib/src/macros.rs`
|
||||||
|
- `crates/ksp-logging-lib/src/settings.rs`
|
||||||
|
- `crates/ksp-logging-lib/src/span.rs`
|
||||||
|
- `crates/ksp-logging-lib/unit_tests/settings.rs`
|
||||||
|
- `crates/ksp-logging-lib/unit_tests/span.rs`
|
||||||
|
- `crates/ksp-logging-lib/tests/callsite.rs`
|
||||||
|
- `crates/ksp-logging-lib/tests/public_api.rs`
|
||||||
|
- `deltas/0.1.2/pre.002.md`
|
||||||
|
|
||||||
|
## Fichiers modifiés
|
||||||
|
|
||||||
|
- `Cargo.toml`
|
||||||
|
- `docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md`
|
||||||
|
- `docs/rules/RULES_DEPENDENCIES.md`
|
||||||
|
|
||||||
|
## Fichiers supprimés
|
||||||
|
|
||||||
|
Aucun.
|
||||||
|
|
||||||
|
## Validations exécutées
|
||||||
|
|
||||||
|
Validations statiques exécutées dans l'environnement de préparation :
|
||||||
|
|
||||||
|
- parsing TOML des manifests ;
|
||||||
|
- contrôle des headers `file:` / `version:` des fichiers livrés ;
|
||||||
|
- contrôle de l'absence de `Cargo.lock` dans le delta ;
|
||||||
|
- contrôle de l'absence de `tracing-subscriber` et `tracing-appender` dans les manifests ;
|
||||||
|
- contrôle de la centralisation de `tracing` sous `[workspace.dependencies]` ;
|
||||||
|
- contrôle que les usages directs de `tracing` restent bornés à `ksp-logging-lib` ;
|
||||||
|
- contrôle des patterns Rust interdits par les règles workspace dans le code production ajouté ;
|
||||||
|
- contrôle des liens Markdown locaux du plan modifié ;
|
||||||
|
- contrôle du contenu de l'archive selon `VER-ARCHIVE-004`.
|
||||||
|
|
||||||
|
## Validations non exécutées
|
||||||
|
|
||||||
|
L'environnement de préparation ne fournit pas `cargo`, `rustc` ou `rustfmt`. Les validations suivantes ne sont donc **pas** déclarées réussies :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
cargo check --workspace
|
||||||
|
cargo test --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo tree -p ksp-logging-lib
|
||||||
|
cargo tree -p ksp-logging-lib -d
|
||||||
|
cargo tree -p ksp-logging-lib -e features
|
||||||
|
```
|
||||||
|
|
||||||
|
Elles doivent être exécutées sur le workspace de développement avant validation de la tranche. Toute erreur sera corrigée par le delta suivant conformément au workflow KSP.
|
||||||
|
|
||||||
|
## Décisions prises
|
||||||
|
|
||||||
|
- `tracing` est la seule dépendance de la stack ajoutée en `pre.002` ;
|
||||||
|
- les macros KSP exigent `target:` ;
|
||||||
|
- le callsite est préservé par expansion de macro et testé ;
|
||||||
|
- l'abstraction publique de span est `ksp_logging_lib::Span` ;
|
||||||
|
- le synchrone utilise `Span::in_scope(...)` ;
|
||||||
|
- l'async utilise `instrument(span, future)` ;
|
||||||
|
- une configuration sans sink est valide et représente Logging désactivé ;
|
||||||
|
- aucune initialisation/subscriber global n'est introduit prématurément dans cette tranche.
|
||||||
|
|
||||||
|
## Questions ouvertes
|
||||||
|
|
||||||
|
Aucune question bloquante pour `pre.002`.
|
||||||
|
|
||||||
|
Restent à choisir/tester dans les tranches runtime suivantes :
|
||||||
|
|
||||||
|
- la composition interne reloadable la moins coûteuse ;
|
||||||
|
- l'API exacte d'observation des lignes abandonnées ;
|
||||||
|
- les détails finaux du formatter console/fichier ;
|
||||||
|
- la stratégie de swap des sinks garantissant le maintien de l'ancienne configuration si une reconfiguration échoue.
|
||||||
|
|
||||||
|
Après validation de cette tranche, la prochaine étape est `0.1.2-pre.003` : subscriber, takeover, filtering, console initiale et fondation du hot reload.
|
||||||
144
deltas/0.1.2/pre.003-fix.001.md
Normal file
144
deltas/0.1.2/pre.003-fix.001.md
Normal 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
|
||||||
|
```
|
||||||
305
deltas/0.1.2/pre.003.md
Normal file
305
deltas/0.1.2/pre.003.md
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
<!-- file: deltas/0.1.2/pre.003.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# 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<LoggingGuard>
|
||||||
|
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<Box<dyn Layer<Registry> + 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.
|
||||||
157
deltas/0.1.2/pre.004-fix.001.md
Normal file
157
deltas/0.1.2/pre.004-fix.001.md
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
<!-- file: deltas/0.1.2/pre.004-fix.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta 0.1.2-pre.004-fix.001
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
Livraison précédente :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-pre.004
|
||||||
|
```
|
||||||
|
|
||||||
|
La base porte :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = "0.1.2-pre.4"
|
||||||
|
Cargo.toml header version = 31
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validations remontées
|
||||||
|
|
||||||
|
Les validations utilisateur de `pre.004` sont :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all OK
|
||||||
|
cargo check --workspace OK
|
||||||
|
cargo clippy --workspace --all-targets OK
|
||||||
|
cargo test --workspace ECHEC
|
||||||
|
cargo tree -p ksp-logging-lib OK
|
||||||
|
cargo tree -p ksp-logging-lib -d OK — aucun doublon
|
||||||
|
cargo tree -p ksp-logging-lib -e features inspecté
|
||||||
|
```
|
||||||
|
|
||||||
|
Tous les tests unitaires, de callsite et de façade publique passent. Le seul échec est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
global_runtime_supports_takeover_non_blocking_outputs_hot_reload_and_single_initialization
|
||||||
|
```
|
||||||
|
|
||||||
|
sur :
|
||||||
|
|
||||||
|
```text
|
||||||
|
assertion failed: file_text.contains("file output marker")
|
||||||
|
```
|
||||||
|
|
||||||
|
Le test émet une ligne sur le sink fichier, retire immédiatement ce sink par hot reload, puis lit le fichier. Il constitue donc un test direct du contrat de drain/flush lors d'un reload.
|
||||||
|
|
||||||
|
## Cause de lifecycle
|
||||||
|
|
||||||
|
`pre.004` faisait conceptuellement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
reload_handle.reload(new_layers)
|
||||||
|
retire counters
|
||||||
|
replace outputs
|
||||||
|
drop(old WorkerGuard)
|
||||||
|
```
|
||||||
|
|
||||||
|
Le layer `fmt` retiré possède les clones `NonBlocking` utilisés pour alimenter le worker. KSP ne récupérait cependant pas explicitement l'ancien `Vec` de layers ; l'ordre entre la destruction effective de ces anciens layers et la destruction des `WorkerGuard` n'était donc pas exprimé dans notre lifecycle.
|
||||||
|
|
||||||
|
Pour un sink non bloquant, l'ordre voulu est explicite :
|
||||||
|
|
||||||
|
```text
|
||||||
|
1. préparer complètement le nouveau runtime
|
||||||
|
2. remplacer le Vec actif et récupérer l'ancien Vec
|
||||||
|
3. mémoriser les dropped-line counters
|
||||||
|
4. remplacer les outputs actifs
|
||||||
|
5. détruire les anciens layers / NonBlocking senders
|
||||||
|
6. détruire les anciens WorkerGuard
|
||||||
|
7. retourner du reinitialize()
|
||||||
|
```
|
||||||
|
|
||||||
|
`WorkerGuard` envoie le signal de shutdown au worker et attend son drain/flush de manière bornée. Les anciens senders doivent donc être libérés avant cette étape lorsqu'un sink vient d'être retiré.
|
||||||
|
|
||||||
|
## Correction
|
||||||
|
|
||||||
|
`reinitialize()` n'utilise plus :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Handle::reload(new_layers)
|
||||||
|
```
|
||||||
|
|
||||||
|
pour les changements de runtime.
|
||||||
|
|
||||||
|
Il utilise :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Handle::modify(... mem::replace(active_layers, new_layers) ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
et récupère ainsi l'ancien `RuntimeLayers`.
|
||||||
|
|
||||||
|
Après succès du swap :
|
||||||
|
|
||||||
|
```text
|
||||||
|
drop(retired_layers)
|
||||||
|
drop(retired_outputs)
|
||||||
|
```
|
||||||
|
|
||||||
|
est exécuté dans cet ordre.
|
||||||
|
|
||||||
|
Cette correction :
|
||||||
|
|
||||||
|
- ne change pas l'API publique ;
|
||||||
|
- conserve le subscriber global unique ;
|
||||||
|
- conserve le takeover KSP ;
|
||||||
|
- conserve la préparation transactionnelle des nouveaux sinks avant le swap ;
|
||||||
|
- conserve l'ancienne configuration lorsqu'une validation ou une construction de sink échoue avant le swap ;
|
||||||
|
- rend explicite le lifecycle de retrait des `NonBlocking` writers avant leurs `WorkerGuard` ;
|
||||||
|
- évite d'ajouter un sleep ou un polling temporel au test.
|
||||||
|
|
||||||
|
Le test d'intégration qui a révélé le défaut reste inchangé et sert directement de test de non-régression.
|
||||||
|
|
||||||
|
## Référence backend
|
||||||
|
|
||||||
|
`tracing-appender 0.2.5` documente `WorkerGuard` comme responsable du flush des logs bufferisés à sa destruction. Son implémentation de `Drop` envoie un `Msg::Shutdown` au worker puis attend le signal de fin de drain de manière bornée. KSP doit donc contrôler clairement l'ordre de destruction des senders/layers et du guard au moment d'un hot reload.
|
||||||
|
|
||||||
|
## Version technique
|
||||||
|
|
||||||
|
Ce correctif modifie du Rust. La version workspace devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = "0.1.2-pre.4.fix.1"
|
||||||
|
```
|
||||||
|
|
||||||
|
et l'en-tête du `Cargo.toml` racine devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
# version: 32
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers du delta
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-logging-lib/src/runtime.rs
|
||||||
|
docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md
|
||||||
|
deltas/0.1.2/pre.004-fix.001.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validations à exécuter
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Le graphe Cargo/features de `pre.004` a déjà été remonté sans doublon. Il pourra être réaudité dans `pre.005` avec les validations d'intégration finales.
|
||||||
|
|
||||||
|
Si ces validations sont propres, la tranche suivante reste :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-pre.005 — intégration + concurrence + saturation + audits
|
||||||
|
```
|
||||||
135
deltas/0.1.2/pre.004-fix.002.md
Normal file
135
deltas/0.1.2/pre.004-fix.002.md
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
<!-- file: deltas/0.1.2/pre.004-fix.002.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta 0.1.2-pre.004-fix.002
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
Livraison précédente :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-pre.004-fix.001
|
||||||
|
```
|
||||||
|
|
||||||
|
La base porte :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = "0.1.2-pre.4.fix.1"
|
||||||
|
Cargo.toml header version = 32
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation remontée
|
||||||
|
|
||||||
|
Après application de `pre.004-fix.001`, un rebuild propre a donné :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo clean OK
|
||||||
|
cargo fmt --all OK
|
||||||
|
cargo check --workspace OK
|
||||||
|
cargo clippy --workspace --all-targets OK
|
||||||
|
cargo test --workspace ECHEC
|
||||||
|
```
|
||||||
|
|
||||||
|
Tous les tests sauf le test runtime global passent encore. L'échec reste strictement identique :
|
||||||
|
|
||||||
|
```text
|
||||||
|
assertion failed: file_text.contains("file output marker")
|
||||||
|
```
|
||||||
|
|
||||||
|
La reproduction après `cargo clean` invalide donc l'hypothèse selon laquelle cet échec précis provenait de l'ordre de destruction corrigé par `pre.004-fix.001`. Ce lifecycle explicite est néanmoins conservé.
|
||||||
|
|
||||||
|
## Cause réelle
|
||||||
|
|
||||||
|
`tracing-subscriber 0.3.23` active par défaut la sanitization ANSI des valeurs dans `fmt::Layer`. Cette protection intervient pendant le formatage, donc avant l'appel au `MakeWriter`.
|
||||||
|
|
||||||
|
Le sink fichier KSP était composé comme suit :
|
||||||
|
|
||||||
|
```text
|
||||||
|
value containing ESC
|
||||||
|
-> fmt::Layer ANSI sanitization
|
||||||
|
-> NonBlocking
|
||||||
|
-> StripAnsiWriter
|
||||||
|
-> RollingFileAppender
|
||||||
|
```
|
||||||
|
|
||||||
|
Le `StripAnsiWriter` KSP ne recevait donc plus les octets ESC originaux à supprimer. Le test attend volontairement que :
|
||||||
|
|
||||||
|
```text
|
||||||
|
file ESC[31moutput ESC[0m marker
|
||||||
|
```
|
||||||
|
|
||||||
|
devienne dans le fichier :
|
||||||
|
|
||||||
|
```text
|
||||||
|
file output marker
|
||||||
|
```
|
||||||
|
|
||||||
|
La sanitization native et le stripping KSP sont deux politiques différentes : KSP veut supprimer les contrôles du fichier, pas les transformer avant son propre writer.
|
||||||
|
|
||||||
|
## Correction
|
||||||
|
|
||||||
|
Le runtime distingue désormais la politique du formatter selon le sink :
|
||||||
|
|
||||||
|
```text
|
||||||
|
console
|
||||||
|
fmt::Layer.with_ansi(false)
|
||||||
|
fmt::Layer.with_ansi_sanitization(true)
|
||||||
|
-> NonBlocking console
|
||||||
|
|
||||||
|
file
|
||||||
|
fmt::Layer.with_ansi(false)
|
||||||
|
fmt::Layer.with_ansi_sanitization(false)
|
||||||
|
-> NonBlocking
|
||||||
|
-> StripAnsiWriter
|
||||||
|
-> RollingFileAppender
|
||||||
|
```
|
||||||
|
|
||||||
|
La console conserve donc la protection native de `tracing-subscriber`. Le fichier laisse passer jusqu'au worker les séquences présentes dans les valeurs afin que `StripAnsiWriter` les supprime avant persistence.
|
||||||
|
|
||||||
|
Le stripping reste hors du hot path : il est toujours exécuté derrière la queue non bloquante.
|
||||||
|
|
||||||
|
Le test d'intégration runtime reste inchangé. Il continue à vérifier :
|
||||||
|
|
||||||
|
- l'émission fichier après hot reload ;
|
||||||
|
- le retrait immédiat du sink et son drain ;
|
||||||
|
- la présence du target et du callsite ;
|
||||||
|
- l'absence de séquences ANSI ;
|
||||||
|
- le silence des targets externes.
|
||||||
|
|
||||||
|
## Version technique
|
||||||
|
|
||||||
|
Ce correctif modifie du Rust. La version workspace devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = "0.1.2-pre.4.fix.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
et l'en-tête du `Cargo.toml` racine devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
# version: 33
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers du delta
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-logging-lib/src/runtime.rs
|
||||||
|
docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md
|
||||||
|
deltas/0.1.2/pre.004-fix.002.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validations à exécuter
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Si ces validations sont propres, la tranche suivante reste :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-pre.005 — intégration + concurrence + saturation + audits
|
||||||
|
```
|
||||||
131
deltas/0.1.2/pre.004-fix.003.md
Normal file
131
deltas/0.1.2/pre.004-fix.003.md
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
<!-- file: deltas/0.1.2/pre.004-fix.003.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# 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<Box<dyn Layer<Registry>>>
|
||||||
|
```
|
||||||
|
|
||||||
|
que les formatters.
|
||||||
|
|
||||||
|
Cette composition n'est toutefois pas correcte pour le filtrage global au niveau des callsites. L'implémentation `Layer` de `Vec<L>` 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
|
||||||
|
```
|
||||||
95
deltas/0.1.2/pre.004-fix.004.md
Normal file
95
deltas/0.1.2/pre.004-fix.004.md
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<!-- file: deltas/0.1.2/pre.004-fix.004.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta 0.1.2-pre.004-fix.004
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
Livraison précédente :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-pre.004-fix.003
|
||||||
|
```
|
||||||
|
|
||||||
|
La base porte :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = "0.1.2-pre.4.fix.3"
|
||||||
|
Cargo.toml header version = 34
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation remontée
|
||||||
|
|
||||||
|
Après application de `pre.004-fix.003` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all OK
|
||||||
|
cargo check --workspace OK
|
||||||
|
cargo test --workspace OK
|
||||||
|
cargo clippy --workspace --all-targets WARNING
|
||||||
|
```
|
||||||
|
|
||||||
|
Tous les tests fonctionnels passent désormais, y compris le test runtime global couvrant takeover, sorties non bloquantes, hot reload, fichier, stripping ANSI et initialisation unique.
|
||||||
|
|
||||||
|
Clippy signale uniquement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
clippy::vec_init_then_push
|
||||||
|
```
|
||||||
|
|
||||||
|
sur la construction du `Vec` reloadable après création du composite `Targets -> sinks`.
|
||||||
|
|
||||||
|
## Correction
|
||||||
|
|
||||||
|
La construction :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let mut layers = RuntimeLayers::new();
|
||||||
|
layers.push(takeover_layer);
|
||||||
|
```
|
||||||
|
|
||||||
|
est remplacée par :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let layers = vec![takeover_layer];
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun comportement runtime, test, setting, writer, filtre ou contrat public n'est modifié.
|
||||||
|
|
||||||
|
## Version technique
|
||||||
|
|
||||||
|
Ce correctif modifie du Rust. La version workspace devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = "0.1.2-pre.4.fix.4"
|
||||||
|
```
|
||||||
|
|
||||||
|
et l'en-tête du `Cargo.toml` racine devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
# version: 35
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers du delta
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-logging-lib/src/runtime.rs
|
||||||
|
docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md
|
||||||
|
deltas/0.1.2/pre.004-fix.004.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. Après validation propre, la tranche suivante reste :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-pre.005 — intégration + concurrence + saturation + audits
|
||||||
|
```
|
||||||
301
deltas/0.1.2/pre.004.md
Normal file
301
deltas/0.1.2/pre.004.md
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
<!-- file: deltas/0.1.2/pre.004.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta 0.1.2-pre.004
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
Livraison précédente validée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-pre.003-fix.001
|
||||||
|
```
|
||||||
|
|
||||||
|
La base de développement validée porte :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = "0.1.2-pre.3.fix.1"
|
||||||
|
Cargo.toml header version = 30
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
Compléter le runtime Logging avec les sorties réellement retenues pour `0.1.2` :
|
||||||
|
|
||||||
|
- ajouter `tracing-appender` ;
|
||||||
|
- rendre console et fichier non bloquants pour le caller ;
|
||||||
|
- posséder les `WorkerGuard` jusqu'au reload/shutdown approprié ;
|
||||||
|
- exposer les dropped-line counters ;
|
||||||
|
- activer le fichier `Never/Hourly/Daily` avec construction fallible ;
|
||||||
|
- supprimer les séquences ANSI avant persistence ;
|
||||||
|
- conserver le takeover et le hot reload transactionnel établis par `pre.003-fix.001`.
|
||||||
|
|
||||||
|
## Version Cargo
|
||||||
|
|
||||||
|
`workspace.package.version` passe de :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-pre.3.fix.1
|
||||||
|
```
|
||||||
|
|
||||||
|
à :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-pre.4
|
||||||
|
```
|
||||||
|
|
||||||
|
L'identifiant de livraison est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-pre.004
|
||||||
|
```
|
||||||
|
|
||||||
|
Le header du `Cargo.toml` racine passe de version 30 à 31.
|
||||||
|
|
||||||
|
## Dépendance `tracing-appender`
|
||||||
|
|
||||||
|
L'audit du 2026-08-14 confirme `tracing-appender 0.2.5`, publié le 2026-04-17, dans la génération `^0.2`.
|
||||||
|
|
||||||
|
La dépendance est centralisée sous `[workspace.dependencies]` :
|
||||||
|
|
||||||
|
```toml
|
||||||
|
tracing-appender = { version = "^0.2", default-features = false }
|
||||||
|
```
|
||||||
|
|
||||||
|
`ksp-logging-lib` la consomme avec :
|
||||||
|
|
||||||
|
```toml
|
||||||
|
tracing-appender.workspace = true
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune feature optionnelle n'est activée. Le backend expose `NonBlockingBuilder`, `WorkerGuard`, `ErrorCounter` et `RollingFileAppender` sans feature supplémentaire.
|
||||||
|
|
||||||
|
## Console non bloquante
|
||||||
|
|
||||||
|
La console n'utilise plus directement `stdout`/`stderr` dans le formatter.
|
||||||
|
|
||||||
|
Chaque sink console construit :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Stdout | Stderr
|
||||||
|
-> NonBlockingBuilder(lossy = true)
|
||||||
|
-> fmt layer
|
||||||
|
+ WorkerGuard
|
||||||
|
+ ErrorCounter
|
||||||
|
```
|
||||||
|
|
||||||
|
Le mode lossy est explicite : lorsque la queue est saturée, un log peut être abandonné au lieu de bloquer le thread appelant.
|
||||||
|
|
||||||
|
Le thread worker console est nommé :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-logging-console
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichier et rotation
|
||||||
|
|
||||||
|
`FileSettings` est maintenant réellement consommé par le runtime.
|
||||||
|
|
||||||
|
Le mapping est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
FileRotation::Never -> Rotation::NEVER
|
||||||
|
FileRotation::Hourly -> Rotation::HOURLY
|
||||||
|
FileRotation::Daily -> Rotation::DAILY
|
||||||
|
```
|
||||||
|
|
||||||
|
Le runtime utilise uniquement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
RollingFileAppender::builder()
|
||||||
|
.rotation(...)
|
||||||
|
.filename_prefix(...)
|
||||||
|
.build(directory)
|
||||||
|
```
|
||||||
|
|
||||||
|
La forme builder retourne un `Result`; aucune API de construction qui panique n'est utilisée par KSP.
|
||||||
|
|
||||||
|
Un échec retourne :
|
||||||
|
|
||||||
|
```text
|
||||||
|
logging.file_output_initialization_failed
|
||||||
|
```
|
||||||
|
|
||||||
|
avec le directory, le file-name prefix et l'erreur `InitError` externe conservés dans le contrat Core.
|
||||||
|
|
||||||
|
## Stripping ANSI
|
||||||
|
|
||||||
|
Le fichier est composé comme suit :
|
||||||
|
|
||||||
|
```text
|
||||||
|
fmt layer
|
||||||
|
-> NonBlocking queue
|
||||||
|
-> StripAnsiWriter
|
||||||
|
-> RollingFileAppender
|
||||||
|
```
|
||||||
|
|
||||||
|
Le stripping est donc effectué par le thread logging et non par le caller.
|
||||||
|
|
||||||
|
`StripAnsiWriter` conserve un état entre les appels `Write` afin de retirer correctement une séquence terminal coupée entre plusieurs buffers. La première surface couvre :
|
||||||
|
|
||||||
|
- CSI (`ESC [` ... final byte) ;
|
||||||
|
- OSC terminé par BEL ou ST ;
|
||||||
|
- autres chaînes terminal ESC de type DCS/SOS/PM/APC terminées par ST.
|
||||||
|
|
||||||
|
Ce mécanisme est générique et n'introduit aucune dépendance Tauri.
|
||||||
|
|
||||||
|
## Formatter humain
|
||||||
|
|
||||||
|
Console et fichier partagent le formatter humain KSP avec :
|
||||||
|
|
||||||
|
- timestamp standard `tracing-subscriber` ;
|
||||||
|
- niveau ;
|
||||||
|
- target ;
|
||||||
|
- champs/message ;
|
||||||
|
- source file ;
|
||||||
|
- line number ;
|
||||||
|
- ANSI du formatter désactivé ;
|
||||||
|
- lifecycle de spans selon `SpanEvents`.
|
||||||
|
|
||||||
|
La ponctuation exacte du formatter reste hors contrat public.
|
||||||
|
|
||||||
|
## Ownership et reload
|
||||||
|
|
||||||
|
`LoggingGuard` possède désormais les outputs actifs :
|
||||||
|
|
||||||
|
```text
|
||||||
|
LoggingGuard
|
||||||
|
├── reload handle
|
||||||
|
├── current LoggingSettings
|
||||||
|
├── active console WorkerGuard/ErrorCounter
|
||||||
|
├── active file WorkerGuard/ErrorCounter
|
||||||
|
└── cumulative retired dropped-line counters
|
||||||
|
```
|
||||||
|
|
||||||
|
`reinitialize()` :
|
||||||
|
|
||||||
|
1. valide les nouveaux settings ;
|
||||||
|
2. construit entièrement le nouveau file appender et tous les nouveaux non-blocking writers/guards ;
|
||||||
|
3. construit les nouveaux layers ;
|
||||||
|
4. remplace le `Vec` reloadable ;
|
||||||
|
5. mémorise les dropped lines des anciens sinks ;
|
||||||
|
6. remplace les outputs actifs ;
|
||||||
|
7. détruit les anciens `WorkerGuard`, provoquant leur flush borné par le backend.
|
||||||
|
|
||||||
|
Une erreur avant le swap détruit uniquement les nouveaux outputs préparés et laisse l'ancienne configuration active.
|
||||||
|
|
||||||
|
## Dropped lines
|
||||||
|
|
||||||
|
Nouvelle surface publique :
|
||||||
|
|
||||||
|
```text
|
||||||
|
DroppedLines
|
||||||
|
LoggingGuard::dropped_lines() -> DroppedLines
|
||||||
|
```
|
||||||
|
|
||||||
|
`DroppedLines` expose :
|
||||||
|
|
||||||
|
```text
|
||||||
|
console()
|
||||||
|
file()
|
||||||
|
total()
|
||||||
|
```
|
||||||
|
|
||||||
|
Les valeurs sont cumulées pour toute la durée de vie du `LoggingGuard`, y compris après plusieurs hot reloads. Les `ErrorCounter` de `tracing-appender` ne sont pas exposés directement aux consumers.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
### Unitaires
|
||||||
|
|
||||||
|
- mapping `Never/Hourly/Daily` ;
|
||||||
|
- runtime sans sink ;
|
||||||
|
- console préparée avec filter séparé, non-blocking output et guard ;
|
||||||
|
- addition saturante des dropped-line counters ;
|
||||||
|
- stripping CSI ;
|
||||||
|
- stripping d'une CSI coupée entre deux writes ;
|
||||||
|
- stripping OSC terminé par BEL/ST.
|
||||||
|
|
||||||
|
### Intégration runtime global
|
||||||
|
|
||||||
|
Le test global vérifie désormais :
|
||||||
|
|
||||||
|
1. initialisation silencieuse sans sink ;
|
||||||
|
2. hot reload console non bloquante ;
|
||||||
|
3. takeover KSP et silence `sqlx` ;
|
||||||
|
4. erreur de création d'un file appender sur un chemin invalide ;
|
||||||
|
5. conservation des settings précédents après cet échec ;
|
||||||
|
6. hot reload vers un fichier `Never` ;
|
||||||
|
7. émission d'un message contenant des codes ANSI ;
|
||||||
|
8. retrait du sink fichier par reload, donc drop/flush de son guard ;
|
||||||
|
9. présence du message KSP dans le fichier ;
|
||||||
|
10. absence des codes ANSI persistés ;
|
||||||
|
11. absence du message externe `sqlx` ;
|
||||||
|
12. présence du target et de la source ;
|
||||||
|
13. lecture de la statistique cumulée ;
|
||||||
|
14. refus d'un second `initialize()`.
|
||||||
|
|
||||||
|
La saturation déterministe avec une queue artificiellement petite est reportée à `pre.005`, où un writer de test injecté pourra être utilisé sans rendre la capacité de queue publique dans `LoggingSettings`.
|
||||||
|
|
||||||
|
## Fichiers ajoutés
|
||||||
|
|
||||||
|
- `crates/ksp-logging-lib/src/writer.rs`
|
||||||
|
- `crates/ksp-logging-lib/unit_tests/writer.rs`
|
||||||
|
- `deltas/0.1.2/pre.004.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/src/runtime.rs`
|
||||||
|
- `crates/ksp-logging-lib/unit_tests/runtime.rs`
|
||||||
|
- `crates/ksp-logging-lib/tests/runtime.rs`
|
||||||
|
- `crates/ksp-logging-lib/tests/public_api.rs`
|
||||||
|
- `docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md`
|
||||||
|
|
||||||
|
## Validations exécutées pendant la préparation
|
||||||
|
|
||||||
|
- revérification documentaire officielle de `tracing-appender 0.2.5` ;
|
||||||
|
- vérification de la sémantique lossy de `NonBlockingBuilder` ;
|
||||||
|
- vérification de `WorkerGuard` et `ErrorCounter::dropped_lines()` ;
|
||||||
|
- vérification du builder fallible de `RollingFileAppender` ;
|
||||||
|
- contrôle TOML des manifests ;
|
||||||
|
- contrôle des headers `file:` / `version:` ;
|
||||||
|
- contrôle de la centralisation de `tracing-appender` sous `[workspace.dependencies]` ;
|
||||||
|
- 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 `tracing-appender` restent dans `ksp-logging-lib`.
|
||||||
|
|
||||||
|
## 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.004`, passer à `0.1.2-pre.005` :
|
||||||
|
|
||||||
|
- concurrence/reloads répétés ;
|
||||||
|
- saturation déterministe et dropped lines ;
|
||||||
|
- audits de façade et usages directs de la stack tracing ;
|
||||||
|
- audits Cargo/features/doublons ;
|
||||||
|
- mesure grossière de l'overhead du reload/runtime ;
|
||||||
|
- compléments de tests et documentation de crate avant la tranche finale.
|
||||||
88
deltas/0.1.2/pre.005-fix.001.md
Normal file
88
deltas/0.1.2/pre.005-fix.001.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<!-- file: deltas/0.1.2/pre.005-fix.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta 0.1.2-pre.005-fix.001
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
Livraison précédente :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-pre.005
|
||||||
|
```
|
||||||
|
|
||||||
|
La base porte :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = "0.1.2-pre.5"
|
||||||
|
Cargo.toml header version = 36
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation remontée
|
||||||
|
|
||||||
|
La validation utilisateur de `pre.005` est fonctionnellement propre :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all OK
|
||||||
|
cargo check --workspace OK
|
||||||
|
cargo clippy --workspace --all-targets OK
|
||||||
|
cargo test --workspace OK
|
||||||
|
cargo test -p ksp-logging-lib --test overhead -- --ignored --nocapture OK
|
||||||
|
```
|
||||||
|
|
||||||
|
Le probe d'overhead a également passé son garde-fou grossier sur 200000 itérations.
|
||||||
|
|
||||||
|
Le test runtime concurrent produit toutefois un grand volume de lignes `TRACE` sur stderr au démarrage du stress test. Le runtime est encore sur la configuration précédente `console_enabled` avec un override `Trace` lorsque les producteurs sont libérés ; ils peuvent donc émettre avant le premier reload vers la configuration silencieuse.
|
||||||
|
|
||||||
|
## Correction
|
||||||
|
|
||||||
|
`exercise_concurrent_reload` effectue maintenant un `reinitialize` vers `quiet_console` **avant** de créer/libérer les producteurs concurrents.
|
||||||
|
|
||||||
|
Le stress test conserve ensuite exactement :
|
||||||
|
|
||||||
|
- 4 producteurs ;
|
||||||
|
- les appels continus à `ksp_logging_lib::trace!` ;
|
||||||
|
- 32 hot reloads ;
|
||||||
|
- l'alternance entre console non bloquante présente avec filtre KSP `Off` et runtime sans sink ;
|
||||||
|
- les assertions de succès des reloads et des joins.
|
||||||
|
|
||||||
|
La correction supprime uniquement la fenêtre de course initiale qui laissait la configuration `Trace` précédente produire des lignes. Elle ne modifie aucun comportement de production, aucune API publique et aucune dépendance.
|
||||||
|
|
||||||
|
## Version technique
|
||||||
|
|
||||||
|
Ce correctif modifie un fichier Rust de test. La version workspace devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = "0.1.2-pre.5.fix.1"
|
||||||
|
```
|
||||||
|
|
||||||
|
et l'en-tête du `Cargo.toml` racine devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
# version: 37
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers du delta
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-logging-lib/tests/runtime.rs
|
||||||
|
docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md
|
||||||
|
deltas/0.1.2/pre.005-fix.001.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validations à exécuter
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test --workspace
|
||||||
|
cargo test -p ksp-logging-lib --test overhead -- --ignored --nocapture
|
||||||
|
```
|
||||||
|
|
||||||
|
Si ces validations sont propres et que le test runtime ne pollue plus la console, `pre.005` est clôturée et la tranche suivante reste :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-pre.006 — validation finale, documentation, cleanup et prompt 0.1.3
|
||||||
|
```
|
||||||
173
deltas/0.1.2/pre.005.md
Normal file
173
deltas/0.1.2/pre.005.md
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
<!-- file: deltas/0.1.2/pre.005.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta 0.1.2-pre.005
|
||||||
|
|
||||||
|
## Identité
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-pre.005 — intégration, concurrence, saturation et audits Logging
|
||||||
|
```
|
||||||
|
|
||||||
|
Version Cargo portée par ce delta :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-pre.5
|
||||||
|
```
|
||||||
|
|
||||||
|
## Base
|
||||||
|
|
||||||
|
Base directe : `0.1.2-pre.004-fix.004`, version Cargo `0.1.2-pre.4.fix.4`.
|
||||||
|
|
||||||
|
La validation utilisateur de cette base a exécuté avec succès :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Le test runtime global passe alors avec takeover des targets externes, console/fichier non bloquants, stripping ANSI, lifecycle des `WorkerGuard` et hot reload.
|
||||||
|
|
||||||
|
## Mission du delta
|
||||||
|
|
||||||
|
Cette tranche ne modifie pas la surface fonctionnelle publique de Logging. Elle durcit la fondation existante avant la validation finale :
|
||||||
|
|
||||||
|
- saturation déterministe des queues lossy ;
|
||||||
|
- émissions concurrentes pendant hot reload ;
|
||||||
|
- vérification des temps de lifecycle de spans ;
|
||||||
|
- audit automatique du takeover de dépendances ;
|
||||||
|
- documentation consommateur de la crate ;
|
||||||
|
- probe diagnostic de l'overhead du mécanisme reload.
|
||||||
|
|
||||||
|
## Changements Rust
|
||||||
|
|
||||||
|
### Construction non bloquante testable sans setting supplémentaire
|
||||||
|
|
||||||
|
`runtime.rs` centralise la construction du `NonBlockingBuilder` dans un helper privé utilisé par la production.
|
||||||
|
|
||||||
|
La politique reste :
|
||||||
|
|
||||||
|
```text
|
||||||
|
lossy = true
|
||||||
|
```
|
||||||
|
|
||||||
|
La taille de queue n'entre pas dans `LoggingSettings`. Le test unitaire peut cependant dériver le même builder avec `buffered_lines_limit(1)` afin de provoquer une saturation contrôlée.
|
||||||
|
|
||||||
|
### Saturation déterministe
|
||||||
|
|
||||||
|
Le test unitaire runtime introduit un writer qui bloque volontairement son worker sur la première écriture.
|
||||||
|
|
||||||
|
Une fois le worker bloqué :
|
||||||
|
|
||||||
|
1. la queue est limitée à une ligne ;
|
||||||
|
2. un producteur émet 1024 lignes supplémentaires ;
|
||||||
|
3. le producteur doit terminer sous timeout alors que le writer reste bloqué ;
|
||||||
|
4. le compteur `ErrorCounter::dropped_lines()` doit être strictement positif ;
|
||||||
|
5. le writer est ensuite libéré et le `WorkerGuard` peut terminer proprement.
|
||||||
|
|
||||||
|
Ce test distingue directement la politique lossy retenue d'une régression vers une queue exerçant de la backpressure.
|
||||||
|
|
||||||
|
### Concurrence pendant hot reload
|
||||||
|
|
||||||
|
Le test runtime global lance quatre threads qui émettent continuellement via `ksp_logging_lib::trace!` pendant que le thread possédant `LoggingGuard` effectue 32 `reinitialize()` successifs.
|
||||||
|
|
||||||
|
Les settings alternent entre :
|
||||||
|
|
||||||
|
- runtime sans sink ;
|
||||||
|
- console non bloquante présente avec niveau KSP `Off`.
|
||||||
|
|
||||||
|
Le test exerce donc le reload, la création/retrait de worker guards et la lecture concurrente du subscriber sans inonder stdout/stderr.
|
||||||
|
|
||||||
|
### Lifecycle des spans
|
||||||
|
|
||||||
|
Un nouveau test avec subscriber local vérifie que `FmtSpan::NEW | FmtSpan::CLOSE` produit pour un span KSP :
|
||||||
|
|
||||||
|
- l'identité du span ;
|
||||||
|
- l'événement `new` ;
|
||||||
|
- l'événement `close` ;
|
||||||
|
- `time.busy` ;
|
||||||
|
- `time.idle`.
|
||||||
|
|
||||||
|
### Audit de takeover
|
||||||
|
|
||||||
|
`tests/ownership.rs` parcourt les crates du workspace autres que `ksp-logging-lib` et rejette :
|
||||||
|
|
||||||
|
- une dépendance Cargo directe `tracing` ;
|
||||||
|
- une dépendance directe `tracing-subscriber` ;
|
||||||
|
- une dépendance directe `tracing-appender` ;
|
||||||
|
- les usages Rust directs `tracing::`, `tracing_subscriber::` ou `tracing_appender::`.
|
||||||
|
|
||||||
|
La crate Logging elle-même est explicitement exclue de cet audit car elle possède légitimement la stack.
|
||||||
|
|
||||||
|
## Documentation de crate
|
||||||
|
|
||||||
|
Ajouts :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-logging-lib/README.md
|
||||||
|
crates/ksp-logging-lib/USAGE.md
|
||||||
|
crates/ksp-logging-lib/TODO.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Ils documentent notamment :
|
||||||
|
|
||||||
|
- target = nom Cargo de la crate propriétaire ;
|
||||||
|
- champs structurés additionnels ;
|
||||||
|
- `initialize` puis `reinitialize` ;
|
||||||
|
- hot reload transactionnel ;
|
||||||
|
- spans sync/async ;
|
||||||
|
- dropped lines ;
|
||||||
|
- capacités explicitement différées.
|
||||||
|
|
||||||
|
## Probe d'overhead
|
||||||
|
|
||||||
|
`tests/overhead.rs` est ignoré par défaut car il s'agit d'un probe temporel diagnostic, pas d'un benchmark de précision.
|
||||||
|
|
||||||
|
Commande explicite :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test -p ksp-logging-lib --test overhead -- --ignored --nocapture
|
||||||
|
```
|
||||||
|
|
||||||
|
Le probe compare un filtre local fixe au même filtre derrière `tracing_subscriber::reload::Layer` sur 200000 événements et n'échoue que si le coût reload dépasse un garde-fou volontairement très large. Le résultat sert à repérer une régression grossière ; il ne constitue pas une mesure HFT ni un engagement de performance absolue.
|
||||||
|
|
||||||
|
## Audit des dépendances
|
||||||
|
|
||||||
|
Aucune dépendance n'est ajoutée par `pre.005`.
|
||||||
|
|
||||||
|
Lors de `pre.004`, les commandes utilisateur ont observé :
|
||||||
|
|
||||||
|
```text
|
||||||
|
tracing 0.1.44
|
||||||
|
tracing-subscriber 0.3.23
|
||||||
|
tracing-appender 0.2.5
|
||||||
|
```
|
||||||
|
|
||||||
|
`cargo tree -p ksp-logging-lib -d` ne signalait aucun doublon. Le graphe de features n'activait pas via KSP `tracing-attributes`, `tracing-log`, `env-filter`, JSON/Serde ou le formatter ANSI. Ces commandes doivent être réexécutées sur `pre.005` avant validation finale de la tranche car une validation d'une base précédente ne vaut pas validation du delta courant.
|
||||||
|
|
||||||
|
## Validations à exécuter
|
||||||
|
|
||||||
|
Non exécutées dans l'environnement de génération :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
cargo check --workspace
|
||||||
|
cargo test --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-logging-lib --test overhead -- --ignored --nocapture
|
||||||
|
cargo tree -p ksp-logging-lib
|
||||||
|
cargo tree -p ksp-logging-lib -d
|
||||||
|
cargo tree -p ksp-logging-lib -e features
|
||||||
|
```
|
||||||
|
|
||||||
|
La base stable ne contient pas de répertoire `scripts/`; aucun script inexistant n'est déclaré réussi.
|
||||||
|
|
||||||
|
## Suite
|
||||||
|
|
||||||
|
Après validation de `pre.005`, la tranche prévue est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-pre.006 — validation finale, documentation, cleanup et prompt 0.1.3
|
||||||
|
```
|
||||||
69
deltas/0.1.2/pre.006-fix.001.md
Normal file
69
deltas/0.1.2/pre.006-fix.001.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<!-- file: deltas/0.1.2/pre.006-fix.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta 0.1.2-pre.006-fix.001
|
||||||
|
|
||||||
|
## Nature
|
||||||
|
|
||||||
|
Correctif **documentaire uniquement** appliqué après validation complète de `0.1.2-pre.006`.
|
||||||
|
|
||||||
|
La version Cargo reste :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = "0.1.2-pre.6"
|
||||||
|
Cargo.toml header version = 38
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun fichier Rust, manifest Cargo, dépendance ou comportement runtime n'est modifié.
|
||||||
|
|
||||||
|
## Base validée
|
||||||
|
|
||||||
|
La validation utilisateur de `pre.006` est propre :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all OK
|
||||||
|
cargo check --workspace OK
|
||||||
|
cargo build -p ksp-logging-lib OK
|
||||||
|
cargo clippy --workspace --all-targets OK
|
||||||
|
cargo test --workspace OK
|
||||||
|
cargo test -p ksp-logging-lib --test overhead -- --ignored --nocapture OK
|
||||||
|
cargo tree -p ksp-logging-lib OK
|
||||||
|
cargo tree -p ksp-logging-lib -d OK (aucun doublon)
|
||||||
|
cargo tree -p ksp-logging-lib -e features OK
|
||||||
|
cargo tree -p ksp-logging-lib -e normal OK (Tokio absent)
|
||||||
|
cargo tree -p ksp-logging-lib -e dev OK (Tokio seul dev-dependency)
|
||||||
|
```
|
||||||
|
|
||||||
|
Les deux tests Tokio réels passent et le build normal de `ksp-logging-lib` confirme que Tokio reste hors du graphe runtime normal.
|
||||||
|
|
||||||
|
## Corrections du prompt `0.1.3`
|
||||||
|
|
||||||
|
Le prompt Config est corrigé avant `rel.001` afin de ne pas démarrer la session suivante avec d'anciens contrats khadhroony-bot3 devenus incorrects.
|
||||||
|
|
||||||
|
Décisions enregistrées :
|
||||||
|
|
||||||
|
1. les vrais fichiers de configuration runtime sont sous `config/` ;
|
||||||
|
2. les schemas sont sous `config/schemas/` ;
|
||||||
|
3. les exemples sont sous `config/examples/`, séparés des fichiers réels ;
|
||||||
|
4. la conception doit reprendre un système de **documents unitaires** spécialisés et de **fichiers composites** qui assemblent ces documents pour un exécutable/application et peuvent sélectionner/remplacer les profils ;
|
||||||
|
5. `ksp-config-lib` est le propriétaire unique de la lecture, résolution, validation et mutation des fichiers Config ainsi que des variables d'environnement applicatives ; les autres crates/apps passent par ses APIs ;
|
||||||
|
6. les namespaces d'environnement deviennent `KSP_*`, `KSP_PUBLIC_*`, `KSP_SECRET_*` pour KSP et `KSPB_*`, `KSPB_PUBLIC_*`, `KSPB_SECRET_*` pour la branche bot ;
|
||||||
|
7. les secrets ne suivent plus une règle absolue « jamais exposés » : ils restent protégés contre toute exposition implicite, mais les composants légitimes et les applications de management Config doivent pouvoir les consulter/modifier via des contrats explicitement autorisés ;
|
||||||
|
8. `ksp-app-config-desk` est cité comme premier consommateur probable d'une telle surface privilégiée, avant une éventuelle application générale disposant d'une section Config.
|
||||||
|
|
||||||
|
Le détail des formats, contrats d'autorisation et découpage fonctionnel reste volontairement à décider lors du brainstorming obligatoire de `0.1.3-pre.001`.
|
||||||
|
|
||||||
|
## Fichiers
|
||||||
|
|
||||||
|
Modifiés :
|
||||||
|
|
||||||
|
- `prompts/003-V0_1_3_START_PROMPT.md` ;
|
||||||
|
- `docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md`.
|
||||||
|
|
||||||
|
Ajouté :
|
||||||
|
|
||||||
|
- `deltas/0.1.2/pre.006-fix.001.md`.
|
||||||
|
|
||||||
|
## Suite
|
||||||
|
|
||||||
|
Après application de ce correctif documentaire, `0.1.2-pre.006` reste la dernière prerelease fonctionnelle validée. La prochaine livraison est `0.1.2-rel.001` avec passage à la version stable `0.1.2` et validations finales de publication.
|
||||||
213
deltas/0.1.2/pre.006.md
Normal file
213
deltas/0.1.2/pre.006.md
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
<!-- file: deltas/0.1.2/pre.006.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta 0.1.2-pre.006
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
Livraison précédente validée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-pre.005-fix.001
|
||||||
|
```
|
||||||
|
|
||||||
|
La base porte :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = "0.1.2-pre.5.fix.1"
|
||||||
|
Cargo.toml header version = 37
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation de la base
|
||||||
|
|
||||||
|
La validation utilisateur de `pre.005-fix.001` est propre :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all OK
|
||||||
|
cargo check --workspace OK
|
||||||
|
cargo clippy --workspace --all-targets OK
|
||||||
|
cargo test --workspace OK
|
||||||
|
cargo test -p ksp-logging-lib --test overhead -- --ignored --nocapture OK
|
||||||
|
```
|
||||||
|
|
||||||
|
Le stress test concurrent ne produit plus le flux TRACE parasite corrigé par `pre.005-fix.001`.
|
||||||
|
|
||||||
|
Le probe diagnostic d'overhead a passé son garde-fou sur 200000 itérations :
|
||||||
|
|
||||||
|
```text
|
||||||
|
baseline = 19.658553 ms
|
||||||
|
reload = 28.807958 ms
|
||||||
|
```
|
||||||
|
|
||||||
|
Ces nombres restent des observations diagnostiques et non un benchmark contractuel.
|
||||||
|
|
||||||
|
## Objet de pre.006
|
||||||
|
|
||||||
|
`pre.006` est la prerelease finale prévue de `0.1.2`.
|
||||||
|
|
||||||
|
Elle :
|
||||||
|
|
||||||
|
- ajoute une validation des spans async sur un executor Tokio réel ;
|
||||||
|
- garde Tokio hors des dépendances runtime de `ksp-logging-lib` ;
|
||||||
|
- consolide la documentation de la crate ;
|
||||||
|
- ferme les TODO de prerelease ;
|
||||||
|
- prépare le prompt final de `0.1.3 — ksp-config-lib` ;
|
||||||
|
- prépare les validations finales précédant `rel.001`.
|
||||||
|
|
||||||
|
## Tokio uniquement pour les tests
|
||||||
|
|
||||||
|
Version actuelle vérifiée le 2026-08-14 :
|
||||||
|
|
||||||
|
```text
|
||||||
|
tokio 1.53.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Le workspace centralise une contrainte de génération :
|
||||||
|
|
||||||
|
```toml
|
||||||
|
tokio = { version = "^1.53", default-features = false, features = ["rt", "rt-multi-thread", "macros"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
`ksp-logging-lib` le consomme uniquement comme dev-dependency :
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio.workspace = true
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun source de production de Logging n'importe Tokio. L'API `instrument(span, future)` reste fondée sur `std::future::Future` et reste indépendante de l'executor choisi par le consumer.
|
||||||
|
|
||||||
|
## Tests Tokio réels
|
||||||
|
|
||||||
|
Nouveau fichier :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-logging-lib/tests/tokio_span.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Current-thread
|
||||||
|
|
||||||
|
Le premier test utilise :
|
||||||
|
|
||||||
|
```text
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
```
|
||||||
|
|
||||||
|
Il instrumente une future contenant plusieurs `tokio::task::yield_now().await` et utilise un subscriber de test associé au span pour compter les `enter`/`exit`.
|
||||||
|
|
||||||
|
Le test exige plusieurs ré-entrées du span après suspension et un nombre final d'entrées/sorties identique.
|
||||||
|
|
||||||
|
### Multi-thread
|
||||||
|
|
||||||
|
Le second test utilise :
|
||||||
|
|
||||||
|
```text
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
```
|
||||||
|
|
||||||
|
Deux futures instrumentées sont lancées avec `tokio::spawn`, effectuent des suspensions répétées puis doivent terminer normalement. Le test vérifie également l'équilibre des `enter`/`exit`.
|
||||||
|
|
||||||
|
Ce test démontre l'utilisation correcte de la surface KSP sous un runtime Tokio multi-thread ; il ne prétend pas imposer ni mesurer une migration déterministe d'une même future entre worker threads.
|
||||||
|
|
||||||
|
## Documentation finale
|
||||||
|
|
||||||
|
Mises à jour :
|
||||||
|
|
||||||
|
- `crates/ksp-logging-lib/README.md` : indépendance de l'executor en production et statut test-only de Tokio ;
|
||||||
|
- `crates/ksp-logging-lib/USAGE.md` : exemple async et frontière executor ;
|
||||||
|
- `crates/ksp-logging-lib/TODO.md` : seules restent les validations finales et la future livraison stable ;
|
||||||
|
- `docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md` : statut validé de `pre.005-fix.001`, contenu `pre.006`, validations finales et absence de question architecturale bloquante ;
|
||||||
|
- `prompts/000-README.md` : ajout du prompt Config ;
|
||||||
|
- `prompts/003-V0_1_3_START_PROMPT.md` : prompt final pour ouvrir `0.1.3` après `v0.1.2`.
|
||||||
|
|
||||||
|
`ROADMAP.md` reste volontairement inchangé : `0.1.2` demeure en cours tant que `rel.001` et le tag `v0.1.2` ne sont pas validés.
|
||||||
|
|
||||||
|
Aucun changelog général n'existe actuellement dans le dépôt ; `pre.006` n'en crée pas artificiellement un.
|
||||||
|
|
||||||
|
## Version technique
|
||||||
|
|
||||||
|
La prerelease devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = "0.1.2-pre.6"
|
||||||
|
```
|
||||||
|
|
||||||
|
L'en-tête du manifest racine devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
# version: 38
|
||||||
|
```
|
||||||
|
|
||||||
|
Le manifest de `ksp-logging-lib` devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
# version: 4
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers du delta
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-logging-lib/Cargo.toml
|
||||||
|
crates/ksp-logging-lib/README.md
|
||||||
|
crates/ksp-logging-lib/TODO.md
|
||||||
|
crates/ksp-logging-lib/USAGE.md
|
||||||
|
crates/ksp-logging-lib/tests/tokio_span.rs
|
||||||
|
docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md
|
||||||
|
prompts/000-README.md
|
||||||
|
prompts/003-V0_1_3_START_PROMPT.md
|
||||||
|
deltas/0.1.2/pre.006.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validations finales à exécuter
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
cargo check --workspace
|
||||||
|
cargo build -p ksp-logging-lib
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test --workspace
|
||||||
|
cargo test -p ksp-logging-lib --test overhead -- --ignored --nocapture
|
||||||
|
cargo tree -p ksp-logging-lib
|
||||||
|
cargo tree -p ksp-logging-lib -d
|
||||||
|
cargo tree -p ksp-logging-lib -e features
|
||||||
|
cargo tree -p ksp-logging-lib -e normal
|
||||||
|
cargo tree -p ksp-logging-lib -e dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Contrôles attendus en particulier :
|
||||||
|
|
||||||
|
- les deux tests de `tests/tokio_span.rs` passent ;
|
||||||
|
- `cargo build -p ksp-logging-lib` reste un build normal sans Tokio comme dépendance runtime ;
|
||||||
|
- le graphe `-e normal` n'inclut pas Tokio ;
|
||||||
|
- Tokio est visible uniquement via l'usage dev attendu ;
|
||||||
|
- aucune seconde version évitable n'apparaît ;
|
||||||
|
- l'audit ownership continue à interdire les contournements de la façade tracing.
|
||||||
|
|
||||||
|
Aucune validation Cargo n'est déclarée réussie dans ce delta avant exécution dans l'environnement de développement.
|
||||||
|
|
||||||
|
## Suite après validation
|
||||||
|
|
||||||
|
Si `pre.006` est propre, la prochaine livraison est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-rel.001
|
||||||
|
```
|
||||||
|
|
||||||
|
Elle publiera :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = "0.1.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
puis, après validation utilisateur, le commit final recevra :
|
||||||
|
|
||||||
|
```text
|
||||||
|
v0.1.2
|
||||||
|
```
|
||||||
|
|
||||||
|
La session fonctionnelle suivante pourra alors démarrer avec :
|
||||||
|
|
||||||
|
```text
|
||||||
|
prompts/003-V0_1_3_START_PROMPT.md
|
||||||
|
```
|
||||||
193
deltas/0.1.2/rel.001.md
Normal file
193
deltas/0.1.2/rel.001.md
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
<!-- file: deltas/0.1.2/rel.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.1.2-rel.001` — publication stable Logging
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
`0.1.2-pre.006` avec le correctif documentaire `0.1.2-pre.006-fix.001`, au sens des commits de livraison correspondants, avec :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = "0.1.2-pre.6"
|
||||||
|
```
|
||||||
|
|
||||||
|
Le correctif `pre.006-fix.001` ne modifie pas la version Cargo.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Publier la release stable `0.1.2`, clôturer `Logging foundation` et préparer l'ouverture de `0.1.3 — Configuration foundation` sans modifier la surface fonctionnelle de `ksp-logging-lib`.
|
||||||
|
|
||||||
|
## Version Cargo
|
||||||
|
|
||||||
|
`workspace.package.version` passe de :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2-pre.6
|
||||||
|
```
|
||||||
|
|
||||||
|
à :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.2
|
||||||
|
```
|
||||||
|
|
||||||
|
Le header de `Cargo.toml` passe de version 38 à 39.
|
||||||
|
|
||||||
|
Les contraintes de dépendances restent inchangées :
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[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"] }
|
||||||
|
tracing-appender = { version = "^0.2", default-features = false }
|
||||||
|
tokio = { version = "^1.53", default-features = false, features = ["rt", "rt-multi-thread", "macros"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
Tokio reste uniquement une dev-dependency de `ksp-logging-lib` et n'appartient pas à son graphe normal.
|
||||||
|
|
||||||
|
## Validations finales exécutées par le user
|
||||||
|
|
||||||
|
Commandes exécutées avec succès le 2026-08-14 sur `0.1.2-pre.6` :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
cargo check --workspace
|
||||||
|
cargo build -p ksp-logging-lib
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test --workspace
|
||||||
|
cargo test -p ksp-logging-lib --test overhead -- --ignored --nocapture
|
||||||
|
cargo tree -p ksp-logging-lib
|
||||||
|
cargo tree -p ksp-logging-lib -d
|
||||||
|
cargo tree -p ksp-logging-lib -e features
|
||||||
|
cargo tree -p ksp-logging-lib -e normal
|
||||||
|
cargo tree -p ksp-logging-lib -e dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Résultats communiqués :
|
||||||
|
|
||||||
|
- `cargo check --workspace` : succès ;
|
||||||
|
- `cargo build -p ksp-logging-lib` : succès sur le graphe normal ;
|
||||||
|
- `cargo clippy --workspace --all-targets` : succès sans warning communiqué ;
|
||||||
|
- `cargo test --workspace` : tous les tests exécutés réussissent, dont les tests de takeover, saturation non bloquante, hot reload concurrent, lifecycle span et les deux tests Tokio réels ;
|
||||||
|
- probe d'overhead explicite : succès sur 200000 itérations, `baseline=16.959425ms`, `reload=24.942444ms` ;
|
||||||
|
- `cargo tree -p ksp-logging-lib -d` : aucun doublon ;
|
||||||
|
- `cargo tree -p ksp-logging-lib -e normal` : Tokio absent ;
|
||||||
|
- `cargo tree -p ksp-logging-lib -e dev` : Tokio présent comme seule dev-dependency directe ;
|
||||||
|
- features Tokio observées : `macros`, `rt`, `rt-multi-thread`, sans feature `full`.
|
||||||
|
|
||||||
|
Le correctif documentaire `pre.006-fix.001` appliqué après ces validations ne modifie ni Rust, ni manifest, ni runtime.
|
||||||
|
|
||||||
|
## Surface stable publiée
|
||||||
|
|
||||||
|
`0.1.2` stabilise notamment :
|
||||||
|
|
||||||
|
- `ksp-logging-lib` comme façade runtime KSP unique de logging/tracing ;
|
||||||
|
- les macros `error!`, `warn!`, `info!`, `debug!`, `trace!` avec target KSP explicite et callsite consommateur préservé ;
|
||||||
|
- les spans KSP synchrones et `instrument(span, future)` pour l'async sans dépendance `tracing` directe chez les consumers ;
|
||||||
|
- `LoggingSettings`, `LogFilterLevel`, `TargetFilter`, `SpanEvents`, `ConsoleSettings`, `FileSettings` et `FileRotation` ;
|
||||||
|
- `initialize()` unique, `reinitialize()` à chaud et `LoggingGuard` ;
|
||||||
|
- takeover KSP avec silence externe par défaut et overrides par préfixe `ksp-*` ;
|
||||||
|
- console et fichier non bloquants avec `WorkerGuard` possédés par Logging ;
|
||||||
|
- mode lossy sans backpressure sur le hot path et observation cumulée des lignes abandonnées via `DroppedLines` ;
|
||||||
|
- rotation fichier `Never`, `Hourly`, `Daily` ;
|
||||||
|
- suppression des séquences ANSI avant persistence fichier ;
|
||||||
|
- reconfiguration transactionnelle conservant l'ancienne configuration si la nouvelle préparation échoue ;
|
||||||
|
- lifecycle spans `Off`, `NewAndClose`, `Full`, avec `busy`/`idle` lorsque demandé ;
|
||||||
|
- tests de concurrence/reload, saturation, ownership de la stack tracing, callsites et instrumentation Tokio current-thread/multi-thread ;
|
||||||
|
- ownership exclusif de `tracing`, `tracing-subscriber` et `tracing-appender` par `ksp-logging-lib` dans le workspace KSP.
|
||||||
|
|
||||||
|
Aucune nouvelle primitive ou API n'est ajoutée par le présent delta de publication.
|
||||||
|
|
||||||
|
## Documentation de clôture
|
||||||
|
|
||||||
|
Le présent delta :
|
||||||
|
|
||||||
|
- marque `0.1.2` réalisée dans `ROADMAP.md` ;
|
||||||
|
- conserve `004-V0_1_2_LOGGING_FOUNDATION_PLAN.md` comme plan historique clôturé ;
|
||||||
|
- ajoute ce plan aux index de documentation/plans ;
|
||||||
|
- remplace le périmètre candidat Logging dans la séquence fonctionnelle par la surface réellement stabilisée ;
|
||||||
|
- réaligne la section Config de la séquence fonctionnelle sur les décisions de `pre.006-fix.001` : `KSP_*`/`KSPB_*`, `config/examples/`, documents unitaires + composites, ownership exclusif de Config et accès explicite aux secrets pour les surfaces autorisées ;
|
||||||
|
- conserve `prompts/003-V0_1_3_START_PROMPT.md` comme prompt final d'ouverture de `0.1.3`.
|
||||||
|
|
||||||
|
Aucun changelog général n'existe dans la base actuelle ; aucun changelog artificiel n'est créé.
|
||||||
|
|
||||||
|
## Fichiers ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
deltas/0.1.2/rel.001.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
ROADMAP.md
|
||||||
|
docs/000-README.md
|
||||||
|
docs/plans/000-README.md
|
||||||
|
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
|
||||||
|
docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers supprimés
|
||||||
|
|
||||||
|
Aucun.
|
||||||
|
|
||||||
|
## Décisions
|
||||||
|
|
||||||
|
Aucune nouvelle décision fonctionnelle concernant Logging.
|
||||||
|
|
||||||
|
La publication stable confirme les décisions, contrats et corrections stabilisés pendant les prereleases `0.1.2` et leurs fixes.
|
||||||
|
|
||||||
|
La synchronisation de la documentation Config en clôture ne remplace pas le brainstorming `0.1.3-pre.001`; elle ne fait qu'enregistrer les décisions déjà prises dans `pre.006-fix.001`.
|
||||||
|
|
||||||
|
## Validations non exécutées dans cette livraison
|
||||||
|
|
||||||
|
L'environnement de génération du delta ne dispose pas de Cargo/Rust. Les commandes Cargo ne sont donc pas réexécutées ici sur la version finale `0.1.2`.
|
||||||
|
|
||||||
|
Après application du delta, le user doit exécuter au minimum :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Le build normal et les graphes Cargo peuvent également être rejoués pour confirmer une dernière fois l'absence de Tokio dans le graphe runtime.
|
||||||
|
|
||||||
|
## Publication Git
|
||||||
|
|
||||||
|
Après application et validation de ce delta :
|
||||||
|
|
||||||
|
1. vérifier que le working tree ne contient que les modifications attendues ;
|
||||||
|
2. exécuter les validations finales sur `workspace.package.version = "0.1.2"` ;
|
||||||
|
3. créer le commit de release :
|
||||||
|
|
||||||
|
```text
|
||||||
|
v0.1.2-rel.001
|
||||||
|
```
|
||||||
|
|
||||||
|
4. marquer ce commit comme release stable avec le tag :
|
||||||
|
|
||||||
|
```text
|
||||||
|
v0.1.2
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun tag supplémentaire n'est requis pour les prereleases/fixes historiques.
|
||||||
|
|
||||||
|
## Suite
|
||||||
|
|
||||||
|
Après le tag stable `v0.1.2`, ouvrir :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.3-pre.001
|
||||||
|
```
|
||||||
|
|
||||||
|
avec :
|
||||||
|
|
||||||
|
```text
|
||||||
|
prompts/003-V0_1_3_START_PROMPT.md
|
||||||
|
```
|
||||||
|
|
||||||
|
La première prerelease de `0.1.3` reste une phase de brainstorming, audit et planification avant développement fonctionnel de `ksp-config-lib`.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/000-README.md -->
|
<!-- file: docs/000-README.md -->
|
||||||
<!-- version: 10 -->
|
<!-- version: 11 -->
|
||||||
|
|
||||||
# Documentation KSP
|
# Documentation KSP
|
||||||
|
|
||||||
@@ -35,7 +35,8 @@ docs/
|
|||||||
│ ├── 000-README.md
|
│ ├── 000-README.md
|
||||||
│ ├── 001-V0_0_3_PLAN.md
|
│ ├── 001-V0_0_3_PLAN.md
|
||||||
│ ├── 002-FUNCTIONAL_RELEASE_SEQUENCE.md
|
│ ├── 002-FUNCTIONAL_RELEASE_SEQUENCE.md
|
||||||
│ └── 003-V0_1_1_CORE_FOUNDATION_PLAN.md
|
│ ├── 003-V0_1_1_CORE_FOUNDATION_PLAN.md
|
||||||
|
│ └── 004-V0_1_2_LOGGING_FOUNDATION_PLAN.md
|
||||||
└── rules/
|
└── rules/
|
||||||
├── FILE_CONTRACTS.md
|
├── FILE_CONTRACTS.md
|
||||||
├── PROMPT_STRUCTURE.md
|
├── PROMPT_STRUCTURE.md
|
||||||
@@ -52,7 +53,7 @@ D'autres sous-répertoires seront ajoutés uniquement lorsque leur rôle aura é
|
|||||||
|
|
||||||
## Documents de planification
|
## Documents de planification
|
||||||
|
|
||||||
Le plan historique de la phase fondatrice clôturée est conservé dans [`plans/001-V0_0_3_PLAN.md`](plans/001-V0_0_3_PLAN.md). La séquence active des premières releases fonctionnelles est définie dans [`plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md`](plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md). Le plan détaillé de la release stable `0.1.1` est conservé comme historique clôturé dans [`plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md`](plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md).
|
Le plan historique de la phase fondatrice clôturée est conservé dans [`plans/001-V0_0_3_PLAN.md`](plans/001-V0_0_3_PLAN.md). La séquence active des premières releases fonctionnelles est définie dans [`plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md`](plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md). Le plan détaillé de la release stable `0.1.1` est conservé comme historique clôturé dans [`plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md`](plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md). Le plan détaillé de la release stable `0.1.2` est conservé comme historique clôturé dans [`plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md`](plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md).
|
||||||
|
|
||||||
`IDEAS.md` conserve les pistes et questions qui ne sont pas encore des engagements du roadmap ni des décisions architecturales.
|
`IDEAS.md` conserve les pistes et questions qui ne sont pas encore des engagements du roadmap ni des décisions architecturales.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/architecture/003-COMPONENT_CONTRACTS.md -->
|
<!-- file: docs/architecture/003-COMPONENT_CONTRACTS.md -->
|
||||||
<!-- version: 10 -->
|
<!-- version: 11 -->
|
||||||
|
|
||||||
# Contrats initiaux des composants KSP
|
# Contrats initiaux des composants KSP
|
||||||
|
|
||||||
@@ -80,11 +80,17 @@ Le retry d'un appel réseau identique reste une responsabilité transport, disti
|
|||||||
|
|
||||||
`ksp-logging-lib` est la façade KSP unique pour le logging/tracing runtime. Elle importe/initialise directement `tracing`, `tracing-appender` et `tracing-subscriber` et peut dépendre de `ksp-core-lib` pour `Error` / `Result`.
|
`ksp-logging-lib` est la façade KSP unique pour le logging/tracing runtime. Elle importe/initialise directement `tracing`, `tracing-appender` et `tracing-subscriber` et peut dépendre de `ksp-core-lib` pour `Error` / `Result`.
|
||||||
|
|
||||||
Les crates comportementales KSP peuvent dépendre directement de `ksp-logging-lib` et utilisent sa façade pour `error`, `warn`, `info`, `debug` et `trace` avec target/domain/champs structurés selon l'API finale.
|
Les crates comportementales KSP utilisent sa façade pour leurs événements `error`, `warn`, `info`, `debug`, `trace` et pour leurs spans sync/async. Elles n'émettent pas leurs propres logs via une dépendance directe à la stack tracing.
|
||||||
|
|
||||||
|
Chaque émission KSP indique un target correspondant au nom Cargo de la crate propriétaire ; `domain`, `component` et les autres champs structurés décrivent les subdivisions fonctionnelles sans multiplier les targets.
|
||||||
|
|
||||||
|
Le subscriber KSP rend les targets tiers silencieux par défaut. Lorsqu'une information provenant d'une dépendance externe est nécessaire, la crate KSP qui possède l'opération la réémet explicitement sous son propre target ; Logging ne renomme pas les événements tiers.
|
||||||
|
|
||||||
|
Logging possède ses `LoggingSettings`, ses writers/guards et son lifecycle. Le subscriber global est installé une fois, puis la configuration peut être rechargée à chaud via la façade KSP sans dépendance vers Config.
|
||||||
|
|
||||||
`ksp-core-lib` n'a pas de dépendance logging requise. Les crates `*-api` purement déclaratives restent sans logging par défaut.
|
`ksp-core-lib` n'a pas de dépendance logging requise. Les crates `*-api` purement déclaratives restent sans logging par défaut.
|
||||||
|
|
||||||
Une application Tauri peut exceptionnellement avoir une dépendance/framework tracing imposée par un plugin, sans définir une politique parallèle à `ksp-logging-lib`.
|
Une application Tauri peut exceptionnellement avoir une dépendance/framework tracing imposée par un plugin, sans définir une politique parallèle à `ksp-logging-lib`; les événements KSP restent émis via la façade KSP.
|
||||||
|
|
||||||
## Frontière materializer / store
|
## Frontière materializer / store
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/architecture/005-DEPENDENCY_GRAPH.md -->
|
<!-- file: docs/architecture/005-DEPENDENCY_GRAPH.md -->
|
||||||
<!-- version: 7 -->
|
<!-- version: 8 -->
|
||||||
|
|
||||||
# Graphe de dépendances KSP
|
# Graphe de dépendances KSP
|
||||||
|
|
||||||
@@ -100,9 +100,11 @@ runtime crate
|
|||||||
-> ksp-logging-lib
|
-> ksp-logging-lib
|
||||||
```
|
```
|
||||||
|
|
||||||
`ksp-logging-lib` est la seule façade KSP propriétaire de l'initialisation/configuration tracing. Une crate runtime ne dépend normalement pas directement de `tracing`.
|
`ksp-logging-lib` est la seule façade KSP propriétaire de l'initialisation/configuration tracing. Une crate runtime KSP n'utilise pas directement `tracing`, `tracing-subscriber` ou `tracing-appender` pour émettre ses propres événements/spans.
|
||||||
|
|
||||||
Exception technique possible : une application/framework, notamment Tauri, peut devoir intégrer un plugin tracing. Cette adaptation ne crée pas une seconde politique de logging parallèle.
|
Le subscriber global KSP est installé une seule fois puis sa configuration peut être rechargée à chaud par `ksp-logging-lib`. Les targets externes sont silencieux par défaut ; les informations tierces utiles sont réémises par le composant KSP propriétaire sous son propre target.
|
||||||
|
|
||||||
|
Exception technique possible : une application/framework, notamment Tauri, peut devoir intégrer un plugin tracing. Cette adaptation ne crée pas une seconde politique de logging parallèle et les événements KSP restent émis via la façade KSP.
|
||||||
|
|
||||||
`ksp-core-lib` et les crates `*-api` purement déclaratives n'ont pas de dépendance logging obligatoire.
|
`ksp-core-lib` et les crates `*-api` purement déclaratives n'ont pas de dépendance logging obligatoire.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/000-README.md -->
|
<!-- file: docs/plans/000-README.md -->
|
||||||
<!-- version: 6 -->
|
<!-- version: 7 -->
|
||||||
|
|
||||||
# Plans KSP
|
# Plans KSP
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@ Un plan décrit le périmètre, les décisions déjà acquises, les questions ou
|
|||||||
- [`001-V0_0_3_PLAN.md`](001-V0_0_3_PLAN.md) — plan historique de la phase fondatrice `0.0.3`, clôturée ;
|
- [`001-V0_0_3_PLAN.md`](001-V0_0_3_PLAN.md) — plan historique de la phase fondatrice `0.0.3`, clôturée ;
|
||||||
- [`002-FUNCTIONAL_RELEASE_SEQUENCE.md`](002-FUNCTIONAL_RELEASE_SEQUENCE.md) — séquence active de référence des premières releases fonctionnelles ;
|
- [`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`.
|
- [`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`.
|
||||||
|
|
||||||
Le `pre.001` de chaque release fonctionnelle peut introduire son propre plan détaillé lorsque la release s'ouvre.
|
Le `pre.001` de chaque release fonctionnelle peut introduire son propre plan détaillé lorsque la release s'ouvre.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md -->
|
<!-- file: docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md -->
|
||||||
<!-- version: 4 -->
|
<!-- version: 5 -->
|
||||||
|
|
||||||
# Séquence des releases fonctionnelles KSP
|
# Séquence des releases fonctionnelles KSP
|
||||||
|
|
||||||
@@ -100,7 +100,26 @@ rel.001 publication stable validée de 0.1.1
|
|||||||
|
|
||||||
## `0.1.2` — Logging foundation
|
## `0.1.2` — Logging foundation
|
||||||
|
|
||||||
### Dépendances
|
### Mission
|
||||||
|
|
||||||
|
Faire de `ksp-logging-lib` la façade KSP unique de logging/tracing pour les composants runtime.
|
||||||
|
|
||||||
|
### Surface stabilisée
|
||||||
|
|
||||||
|
`0.1.2` stabilise :
|
||||||
|
|
||||||
|
- les macros KSP `error!`, `warn!`, `info!`, `debug!`, `trace!` avec `target:` KSP explicite et callsite consommateur préservé ;
|
||||||
|
- les spans KSP synchrones et l'instrumentation de futures async sans exposer `tracing` aux consumers ;
|
||||||
|
- `LoggingSettings`, niveaux, overrides par préfixe de target et lifecycle de spans ;
|
||||||
|
- `initialize()` unique et `reinitialize()` à chaud avec `LoggingGuard` ;
|
||||||
|
- le takeover des targets : targets externes silencieux par défaut, targets `ksp-*` gouvernés par la politique KSP ;
|
||||||
|
- console et fichier non bloquants, rotation, ownership des `WorkerGuard` et compteurs cumulés de lignes abandonnées ;
|
||||||
|
- stripping ANSI avant persistence fichier ;
|
||||||
|
- reconfiguration transactionnelle conservant l'ancien runtime en cas d'échec ;
|
||||||
|
- tests de saturation, concurrence/reload, lifecycle spans et instrumentation Tokio réelle ;
|
||||||
|
- audit d'ownership empêchant les autres crates workspace de dépendre directement de la stack `tracing*`.
|
||||||
|
|
||||||
|
### Dépendances runtime
|
||||||
|
|
||||||
```text
|
```text
|
||||||
ksp-logging-lib
|
ksp-logging-lib
|
||||||
@@ -110,26 +129,29 @@ ksp-logging-lib
|
|||||||
-> tracing-subscriber
|
-> tracing-subscriber
|
||||||
```
|
```
|
||||||
|
|
||||||
### Mission
|
Tokio est uniquement une dev-dependency de `ksp-logging-lib` pour les tests async réels et n'appartient pas à son graphe normal.
|
||||||
|
|
||||||
Faire de `ksp-logging-lib` la façade KSP unique de logging/tracing pour les composants runtime.
|
`ksp-logging-lib` ne dépend pas de `ksp-config-lib`. Config pourra convertir ses documents résolus en `LoggingSettings` puis utiliser le lifecycle public de Logging.
|
||||||
|
|
||||||
### Périmètre candidat
|
### Lifecycle de la release
|
||||||
|
|
||||||
- initialisation ;
|
Trajectoire réellement suivie :
|
||||||
- settings runtime propres au logging ;
|
|
||||||
- `error`, `warn`, `info`, `debug`, `trace` ;
|
|
||||||
- target/domain/component/champs structurés selon API validée ;
|
|
||||||
- fonctions, macros ou combinaison permettant de préserver correctement les callsites ;
|
|
||||||
- console/fichiers ;
|
|
||||||
- filtering ;
|
|
||||||
- appender/rotation selon besoin concret ;
|
|
||||||
- tests ;
|
|
||||||
- protection contre logging de secrets.
|
|
||||||
|
|
||||||
`ksp-logging-lib` ne dépend pas de `ksp-config-lib`.
|
```text
|
||||||
|
pre.001 brainstorming + audit + plan détaillé
|
||||||
Config pourra plus tard convertir ses documents résolus en settings de Logging.
|
pre.001-fix.001 corrections de cadrage takeover/reload/spans
|
||||||
|
pre.002 crate + settings + façade événements/spans
|
||||||
|
pre.002-fix.001 corrections tests/lints
|
||||||
|
pre.003 subscriber + takeover + console + reload
|
||||||
|
pre.003-fix.001 correction du montage reload/filter
|
||||||
|
pre.004 console/fichier non bloquants + guards + ANSI
|
||||||
|
pre.004-fix.001..004 corrections lifecycle, ANSI, takeover et Clippy
|
||||||
|
pre.005 robustesse, concurrence, saturation, audits
|
||||||
|
pre.005-fix.001 suppression du bruit console du stress test
|
||||||
|
pre.006 validation finale, Tokio dev-only, docs, prompt 0.1.3
|
||||||
|
pre.006-fix.001 correction documentaire du prompt Config
|
||||||
|
rel.001 publication stable validée de 0.1.2
|
||||||
|
```
|
||||||
|
|
||||||
## `0.1.3` — Configuration foundation
|
## `0.1.3` — Configuration foundation
|
||||||
|
|
||||||
@@ -154,11 +176,13 @@ Périmètre candidat à revalider dans son `pre.001` :
|
|||||||
- résolution ;
|
- résolution ;
|
||||||
- validation ;
|
- validation ;
|
||||||
- modification/sauvegarde ;
|
- modification/sauvegarde ;
|
||||||
- variables d'environnement `KS_*` / `KB_*` ;
|
- variables d'environnement `KSP_*` / `KSPB_*` ;
|
||||||
- secret/public/debug exposure policy ;
|
- secret/public/debug exposure policy ;
|
||||||
- `logging.config.json` séparé ;
|
- `logging.config.json` séparé ;
|
||||||
- schemas sous `config/schemas/` ;
|
- vrais fichiers runtime sous `config/`, schemas sous `config/schemas/` et exemples sous `config/examples/` ;
|
||||||
- examples sous `config/`.
|
- documents unitaires spécialisés + fichiers composites par application/exécutable ;
|
||||||
|
- ownership exclusif de `ksp-config-lib` sur lecture/résolution/validation/mutation des fichiers Config et variables d'environnement ;
|
||||||
|
- accès explicite aux secrets pour les surfaces de management autorisées.
|
||||||
|
|
||||||
### Règle de scission
|
### Règle de scission
|
||||||
|
|
||||||
|
|||||||
1130
docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md
Normal file
1130
docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/rules/RULES_DEPENDENCIES.md -->
|
<!-- file: docs/rules/RULES_DEPENDENCIES.md -->
|
||||||
<!-- version: 8 -->
|
<!-- version: 10 -->
|
||||||
|
|
||||||
# Règles des dépendances KSP
|
# Règles des dépendances KSP
|
||||||
|
|
||||||
@@ -43,12 +43,15 @@ Elles complètent les règles Rust générales et le graphe de `docs/architectur
|
|||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
|
|
||||||
- **DEP-LOG-001** — `ksp-logging-lib` est le propriétaire KSP direct de `tracing`, `tracing-appender`, `tracing-subscriber` et de l'initialisation/configuration logging.
|
- **DEP-LOG-001** — `ksp-logging-lib` est le propriétaire KSP direct de `tracing`, `tracing-appender`, `tracing-subscriber` et de l'initialisation/configuration runtime logging.
|
||||||
- **DEP-LOG-002** — `ksp-logging-lib` peut dépendre de `ksp-core-lib` pour le contrat commun `Error` / `Result`.
|
- **DEP-LOG-002** — `ksp-logging-lib` peut dépendre de `ksp-core-lib` pour le contrat commun `Error` / `Result`.
|
||||||
- **DEP-LOG-003** — `ksp-core-lib` ne dépend pas de `ksp-logging-lib` dans l'architecture actuelle.
|
- **DEP-LOG-003** — `ksp-core-lib` ne dépend pas de `ksp-logging-lib` dans l'architecture actuelle.
|
||||||
- **DEP-LOG-004** — Les crates KSP comportant du runtime peuvent dépendre directement de `ksp-logging-lib` et ne dépendent normalement pas directement de `tracing`.
|
- **DEP-LOG-004** — Les crates KSP comportant du runtime dépendent de `ksp-logging-lib` lorsqu'elles instrumentent leur comportement et n'utilisent pas directement `tracing`, `tracing-subscriber` ou `tracing-appender` pour émettre leurs propres événements/spans.
|
||||||
- **DEP-LOG-005** — Les crates `*-api` purement déclaratives n'ajoutent pas une dépendance logging sans comportement réel à instrumenter.
|
- **DEP-LOG-005** — Les crates `*-api` purement déclaratives n'ajoutent pas une dépendance logging sans comportement réel à instrumenter.
|
||||||
- **DEP-LOG-006** — Une application/framework peut exceptionnellement intégrer directement un plugin/dépendance tracing imposé par son framework, notamment Tauri, sans créer une seconde politique de logging parallèle à `ksp-logging-lib`.
|
- **DEP-LOG-006** — Le subscriber KSP rend silencieux par défaut les targets externes ; une information tierce utile est réémise explicitement par la crate KSP propriétaire sous son propre target au lieu de renommer/réécrire l'événement tiers.
|
||||||
|
- **DEP-LOG-007** — Une application/framework peut exceptionnellement intégrer directement un plugin/dépendance tracing imposé par son framework, notamment Tauri, sans créer une seconde politique de logging parallèle à `ksp-logging-lib`; les événements KSP restent émis via la façade KSP.
|
||||||
|
- **DEP-LOG-008** — `ksp-logging-lib` possède ses settings runtime et son hot reload ; `ksp-config-lib` peut plus tard construire ces settings et demander une reconfiguration sans créer de dépendance inverse Logging -> Config.
|
||||||
|
- **DEP-LOG-009** — Tout bridge `tracing` public mais caché de la documentation rendu techniquement nécessaire par l’expansion des macros de `ksp-logging-lib` est un détail d’implémentation réservé à ces macros ; une crate consommatrice ne l’utilise jamais directement et reste limitée à la façade KSP documentée.
|
||||||
|
|
||||||
## Program / Execution
|
## Program / Execution
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: prompts/000-README.md -->
|
<!-- file: prompts/000-README.md -->
|
||||||
<!-- version: 4 -->
|
<!-- version: 5 -->
|
||||||
|
|
||||||
# Prompts KSP
|
# Prompts KSP
|
||||||
|
|
||||||
@@ -23,3 +23,4 @@ Le prompt générique `0.1.x` a été affiné pendant `0.0.3` puis remplacé par
|
|||||||
|
|
||||||
- [`001-V0_1_1_START_PROMPT.md`](001-V0_1_1_START_PROMPT.md) — prompt historique ouvrant la première release fonctionnelle `0.1.1` après publication stable de `0.0.3` ;
|
- [`001-V0_1_1_START_PROMPT.md`](001-V0_1_1_START_PROMPT.md) — prompt historique ouvrant la première release fonctionnelle `0.1.1` après publication stable de `0.0.3` ;
|
||||||
- [`002-V0_1_2_START_PROMPT.md`](002-V0_1_2_START_PROMPT.md) — prompt final destiné à ouvrir `0.1.2 — Logging foundation` après publication stable de `0.1.1`.
|
- [`002-V0_1_2_START_PROMPT.md`](002-V0_1_2_START_PROMPT.md) — prompt final destiné à ouvrir `0.1.2 — Logging foundation` après publication stable de `0.1.1`.
|
||||||
|
- [`003-V0_1_3_START_PROMPT.md`](003-V0_1_3_START_PROMPT.md) — prompt final destiné à ouvrir `0.1.3 — Configuration foundation` après publication stable de `0.1.2`.
|
||||||
|
|||||||
370
prompts/003-V0_1_3_START_PROMPT.md
Normal file
370
prompts/003-V0_1_3_START_PROMPT.md
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
<!-- file: prompts/003-V0_1_3_START_PROMPT.md -->
|
||||||
|
<!-- version: 2 -->
|
||||||
|
|
||||||
|
# Prompt de démarrage KSP 0.1.3
|
||||||
|
|
||||||
|
## 1. Contexte de reprise
|
||||||
|
|
||||||
|
Projet : `khadhroony-solana-project` (KSP).
|
||||||
|
|
||||||
|
Base requise avant ouverture de cette session :
|
||||||
|
|
||||||
|
```text
|
||||||
|
v0.1.2 stable
|
||||||
|
```
|
||||||
|
|
||||||
|
La release `0.1.2` a introduit `ksp-logging-lib` comme façade KSP unique de logging/tracing runtime. `0.1.3` ne doit pas rouvrir cette architecture ; Config doit consommer ses contrats publics.
|
||||||
|
|
||||||
|
Release à développer :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.3 — Configuration foundation
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Mission
|
||||||
|
|
||||||
|
Introduire `ksp-config-lib` comme propriétaire unique KSP de la configuration applicative : documents de configuration unitaires et composites, résolution des profils, variables d'environnement, validation, lecture et modifications/persistences explicitement autorisées.
|
||||||
|
|
||||||
|
Les autres crates et applications KSP ne doivent pas lire, résoudre ou modifier directement les fichiers de configuration ni les variables d'environnement. Elles consomment les contrats de `ksp-config-lib`. Une application dédiée au management de configuration utilise donc Config comme unique frontière, y compris lorsqu'elle doit afficher ou modifier des valeurs sensibles explicitement autorisées.
|
||||||
|
|
||||||
|
La première prerelease est obligatoirement une tranche de brainstorming, audit et planification. Ne pas commencer directement par une implémentation large de Config.
|
||||||
|
|
||||||
|
## 3. Base architecturale à préserver
|
||||||
|
|
||||||
|
Dépendances candidates de la nouvelle crate :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-config-lib
|
||||||
|
-> ksp-core-lib
|
||||||
|
-> ksp-logging-lib
|
||||||
|
```
|
||||||
|
|
||||||
|
Relations interdites :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-core-lib -X-> ksp-config-lib
|
||||||
|
ksp-logging-lib -X-> ksp-config-lib
|
||||||
|
```
|
||||||
|
|
||||||
|
`ksp-logging-lib` possède toujours ses propres `LoggingSettings` et son lifecycle `initialize` / `reinitialize`. Config lit/résout ses documents puis construit explicitement les settings publics Logging ; Logging ne lit aucun document Config et ne connaît aucun profil.
|
||||||
|
|
||||||
|
## 4. Première prerelease obligatoire : `0.1.3-pre.001`
|
||||||
|
|
||||||
|
Cette tranche doit produire un plan détaillé avant développement fonctionnel.
|
||||||
|
|
||||||
|
Elle doit notamment :
|
||||||
|
|
||||||
|
1. auditer l'état réel du workspace stable `0.1.2` ;
|
||||||
|
2. réauditer les règles Config déjà présentes dans le dépôt et les archives historiques pertinentes sans les copier aveuglément ;
|
||||||
|
3. inventorier les documents de configuration unitaires nécessaires à court terme et les fichiers composites qui les assemblent pour un exécutable/application ;
|
||||||
|
4. distinguer valeurs globales, valeurs profilées, sélection du profil, documents unitaires réutilisables et composition propre aux exécutables ;
|
||||||
|
5. fixer la politique de résolution document unitaire -> composition -> profil -> env override -> valeur effective ;
|
||||||
|
6. fixer la validation, les diagnostics et les erreurs Core nécessaires ;
|
||||||
|
7. fixer les opérations de modification/sauvegarde autorisées et leurs garanties d'atomicité ;
|
||||||
|
8. fixer la frontière secrets/public/debug et les droits explicites permettant à une application de management Config d'accéder aux secrets lorsqu'elle doit les consulter ou les modifier ;
|
||||||
|
9. définir la relation exacte avec `LoggingSettings` et le hot reload Logging ;
|
||||||
|
10. décider si le périmètre Config tient proprement dans une seule release `0.1.3` ou doit être scindé ;
|
||||||
|
11. produire `docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md` et le plan souple des prereleases suivantes.
|
||||||
|
|
||||||
|
Le `pre.001` reste une tranche de planification : ne pas ajouter de dépendance fonctionnelle inutilisée et ne pas créer une large surface de code avant validation du plan.
|
||||||
|
|
||||||
|
## 5. Documents spécialisés
|
||||||
|
|
||||||
|
La configuration KSP doit être pensée comme plusieurs documents spécialisés plutôt qu'un unique fichier global monolithique lorsque les responsabilités sont distinctes.
|
||||||
|
|
||||||
|
Le point déjà fixé est notamment :
|
||||||
|
|
||||||
|
```text
|
||||||
|
logging.config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
séparé de la configuration applicative générale.
|
||||||
|
|
||||||
|
Le `pre.001` doit inventorier les autres documents réellement nécessaires au premier cycle et éviter de créer prématurément des fichiers pour des composants non développés.
|
||||||
|
|
||||||
|
La structure doit conserver la séparation déjà utile dans khadhroony-bot3 entre **documents unitaires** et **fichiers composites** :
|
||||||
|
|
||||||
|
- un document unitaire possède une responsabilité spécialisée et reste réutilisable indépendamment des exécutables ;
|
||||||
|
- un fichier composite assemble les documents requis par un exécutable/application et peut sélectionner ou remplacer les profils nécessaires sans dupliquer les documents spécialisés.
|
||||||
|
|
||||||
|
Les vrais fichiers de configuration runtime appartiennent sous :
|
||||||
|
|
||||||
|
```text
|
||||||
|
config/
|
||||||
|
```
|
||||||
|
|
||||||
|
Les schemas appartiennent sous :
|
||||||
|
|
||||||
|
```text
|
||||||
|
config/schemas/
|
||||||
|
```
|
||||||
|
|
||||||
|
Les exemples ne doivent pas être mélangés aux fichiers runtime réels et appartiennent sous :
|
||||||
|
|
||||||
|
```text
|
||||||
|
config/examples/
|
||||||
|
```
|
||||||
|
|
||||||
|
Les noms exacts, la nomenclature des fichiers composites et leur format restent à valider dans le plan Config `pre.001`.
|
||||||
|
|
||||||
|
## 6. Valeurs globales et profils
|
||||||
|
|
||||||
|
Une valeur qui ne varie pas selon le profil reste hors profils.
|
||||||
|
|
||||||
|
Exemples historiques déjà retenus comme principe :
|
||||||
|
|
||||||
|
```text
|
||||||
|
logging.logs_directory
|
||||||
|
wallets_directory
|
||||||
|
```
|
||||||
|
|
||||||
|
Le `default_profile` est autonome : il sélectionne un profil par défaut mais ne doit pas être artificiellement imbriqué dans chacun des profils.
|
||||||
|
|
||||||
|
Les fichiers composites propres à un binaire/application sélectionnent les documents unitaires utilisés et peuvent remplacer les profils choisis lorsqu'un besoin concret l'exige. Cette composition doit rester possédée et résolue par `ksp-config-lib`, pas par chaque exécutable séparément.
|
||||||
|
|
||||||
|
## 7. Variables d'environnement
|
||||||
|
|
||||||
|
Toutes les variables d'environnement applicatives doivent être namespacées selon leur propriétaire fonctionnel.
|
||||||
|
|
||||||
|
Pour les composants génériques KSP / `ksp-*` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
KSP_*
|
||||||
|
KSP_PUBLIC_*
|
||||||
|
KSP_SECRET_*
|
||||||
|
```
|
||||||
|
|
||||||
|
Pour les futures applications/composants bot du projet :
|
||||||
|
|
||||||
|
```text
|
||||||
|
KSPB_*
|
||||||
|
KSPB_PUBLIC_*
|
||||||
|
KSPB_SECRET_*
|
||||||
|
```
|
||||||
|
|
||||||
|
`ksp-config-lib` est le propriétaire unique de la lecture, de la résolution, de la validation et de l'écriture éventuelle des variables d'environnement KSP. Les autres crates/applications ne doivent pas contourner cette frontière par des lectures directes de l'environnement applicatif.
|
||||||
|
|
||||||
|
Le `pre.001` doit formaliser précisément la correspondance entre document, clé de configuration et override d'environnement.
|
||||||
|
|
||||||
|
Aucune variable applicative sans préfixe propriétaire ne doit être introduite.
|
||||||
|
|
||||||
|
## 8. Secrets, public et debug
|
||||||
|
|
||||||
|
La classification Config doit distinguer les valeurs publiques, ordinaires et secrètes, mais **secret ne signifie pas interdiction absolue de lecture**.
|
||||||
|
|
||||||
|
Direction déjà retenue :
|
||||||
|
|
||||||
|
- `KSP_SECRET_*` / `KSPB_SECRET_*` : valeurs sensibles, jamais exposées accidentellement dans les logs, diagnostics ordinaires ou surfaces publiques génériques ;
|
||||||
|
- `KSP_PUBLIC_*` / `KSPB_PUBLIC_*` : valeurs explicitement exposables ;
|
||||||
|
- autres valeurs : politique d'exposition à fixer selon le contrat applicatif et le contexte debug ;
|
||||||
|
- les composants runtime qui ont légitimement besoin d'un secret doivent pouvoir l'obtenir via un contrat Config explicite ;
|
||||||
|
- une application possédant une fonctionnalité de **management de configuration** doit pouvoir, via une surface Config explicitement prévue et contrôlée, consulter et modifier les secrets nécessaires. Cela couvre notamment la future `ksp-app-config-desk` et, plus tard, la partie Config d'une éventuelle application générale.
|
||||||
|
|
||||||
|
La surface Config doit donc formaliser une **politique d'accès** aux secrets, et non une règle simpliste « jamais exposé ». Le `pre.001` doit décider les contrats distincts de lecture runtime, consultation de management, mutation et exposition Tauri/DTO afin d'éviter toute fuite implicite tout en permettant l'administration légitime.
|
||||||
|
|
||||||
|
Cela ne change pas la responsabilité de Logging concernant le contenu des messages : `ksp-logging-lib` ne scanne ni ne redacte automatiquement les secrets fournis par ses callers.
|
||||||
|
|
||||||
|
## 9. Relation Config -> Logging
|
||||||
|
|
||||||
|
Config peut produire un `ksp_logging_lib::LoggingSettings` à partir de sa configuration effective puis appeler le lifecycle Logging au niveau d'orchestration approprié.
|
||||||
|
|
||||||
|
Séquence conceptuelle :
|
||||||
|
|
||||||
|
```text
|
||||||
|
documents Config
|
||||||
|
-> résolution/profil/env
|
||||||
|
-> configuration Logging effective
|
||||||
|
-> LoggingSettings
|
||||||
|
-> ksp_logging_lib::initialize(...) ou reinitialize(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
Le mécanisme qui détecte un changement de fichier, s'il est introduit plus tard, appartient à Config/application/orchestration ; Logging fournit uniquement sa reconfiguration runtime.
|
||||||
|
|
||||||
|
`0.1.3-pre.001` doit préciser qui possède le `LoggingGuard` dans les premières compositions concrètes sans créer de singleton global Config inutile.
|
||||||
|
|
||||||
|
## 10. Validation et erreurs
|
||||||
|
|
||||||
|
`ksp-config-lib` doit réutiliser :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp_core_lib::Error
|
||||||
|
ksp_core_lib::ErrorCode
|
||||||
|
ksp_core_lib::ErrorContext
|
||||||
|
ksp_core_lib::Result<T>
|
||||||
|
```
|
||||||
|
|
||||||
|
Les codes propres à Config appartiennent au domaine Config et ne doivent pas être ajoutés comme connaissance métier à Core.
|
||||||
|
|
||||||
|
Les diagnostics doivent distinguer autant que nécessaire :
|
||||||
|
|
||||||
|
- document absent lorsque obligatoire ;
|
||||||
|
- syntaxe invalide ;
|
||||||
|
- schema/contrainte invalide ;
|
||||||
|
- profil demandé absent ;
|
||||||
|
- valeur effective invalide ;
|
||||||
|
- override d'environnement invalide ;
|
||||||
|
- opération de sauvegarde/modification impossible.
|
||||||
|
|
||||||
|
La nomenclature exacte des ErrorCode est décidée dans le plan puis ajoutée seulement quand chaque erreur devient nécessaire.
|
||||||
|
|
||||||
|
## 11. Mutation et persistence
|
||||||
|
|
||||||
|
La configuration n'est pas uniquement un lecteur statique : le périmètre candidat de `0.1.3` comprend les modifications/sauvegardes explicitement autorisées.
|
||||||
|
|
||||||
|
Le `pre.001` doit décider :
|
||||||
|
|
||||||
|
- quels documents peuvent être modifiés par API ;
|
||||||
|
- quelles valeurs sont read-only ;
|
||||||
|
- comment préserver format/version/schema ;
|
||||||
|
- comment éviter un fichier partiellement écrit ;
|
||||||
|
- comment représenter une modification rejetée ;
|
||||||
|
- comment distinguer configuration souhaitée et configuration effective lorsqu'un composant runtime ne peut pas appliquer immédiatement une valeur.
|
||||||
|
|
||||||
|
Si cette surface rend la release trop large, elle doit être scindée plutôt que comprimée artificiellement.
|
||||||
|
|
||||||
|
## 12. Frontière application/Tauri
|
||||||
|
|
||||||
|
`0.1.3` reste une release de bibliothèque Config.
|
||||||
|
|
||||||
|
La validation desktop complète est prévue ensuite, par défaut dans :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.4 — ksp-app-config-desk
|
||||||
|
```
|
||||||
|
|
||||||
|
Les DTO/bindings Tauri n'appartiennent donc pas automatiquement à `ksp-config-lib`. TS-RS reste principalement une frontière des applications Tauri et ne doit être dérivé dans une crate générique que pour un contrat externe réellement générique et indépendant de Tauri. Une application Config desktop pourra toutefois exposer, par des commandes/DTO applicatifs dédiés, les opérations privilégiées de consultation/modification de secrets que `ksp-config-lib` autorise explicitement ; elle ne doit jamais contourner Config en lisant les fichiers ou l'environnement directement.
|
||||||
|
|
||||||
|
## 13. Règles Rust et Cargo à conserver
|
||||||
|
|
||||||
|
Conserver les règles normatives du dépôt, notamment :
|
||||||
|
|
||||||
|
- Rust 2024 ;
|
||||||
|
- `unsafe` interdit ;
|
||||||
|
- pas de `unwrap`, `expect`, `panic` dans le code production ;
|
||||||
|
- pas d'opérateur `?` ;
|
||||||
|
- retours explicites selon les règles Clippy du workspace ;
|
||||||
|
- imports de traits seulement lorsque nécessaire, chemins pleinement qualifiés sinon ;
|
||||||
|
- pas de `mod.rs` ;
|
||||||
|
- pas de `pub(super)` / `pub(in ...)` ;
|
||||||
|
- code et Rustdoc en anglais ;
|
||||||
|
- documentation Markdown en français ;
|
||||||
|
- tests unitaires hors `src` selon la convention du dépôt ;
|
||||||
|
- dépendances externes communes déclarées uniquement sous `[workspace.dependencies]` puis consommées avec `.workspace = true` ;
|
||||||
|
- versions externes sous contraintes caret de génération compatibles, après vérification de la version actuelle au moment de l'ajout ;
|
||||||
|
- ne pas versionner `Cargo.lock`.
|
||||||
|
|
||||||
|
## 14. Logging dans Config
|
||||||
|
|
||||||
|
`ksp-config-lib` contient du comportement runtime et doit normalement dépendre de `ksp-logging-lib` pour ses propres événements utiles.
|
||||||
|
|
||||||
|
Elle ne doit jamais importer directement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
tracing
|
||||||
|
tracing-subscriber
|
||||||
|
tracing-appender
|
||||||
|
```
|
||||||
|
|
||||||
|
Ses targets KSP explicites utilisent le nom Cargo :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-config-lib
|
||||||
|
```
|
||||||
|
|
||||||
|
Les détails d'une bibliothèque tierce utilisés par Config sont silencieux par défaut ; Config réémet sous son propre target les informations réellement utiles au diagnostic KSP.
|
||||||
|
|
||||||
|
## 15. Hors scope initial
|
||||||
|
|
||||||
|
Sauf décision explicite du `pre.001`, ne pas ouvrir dans `0.1.3` :
|
||||||
|
|
||||||
|
- application desktop Config ;
|
||||||
|
- Tauri comme dépendance de `ksp-config-lib` ;
|
||||||
|
- Wallet ;
|
||||||
|
- Store/PostgreSQL ;
|
||||||
|
- RPC/WS/provider ;
|
||||||
|
- Program/decoder/execution ;
|
||||||
|
- workers/jobs/pipelines ;
|
||||||
|
- trading/ML ;
|
||||||
|
- watcher générique de tous les fichiers du projet ;
|
||||||
|
- service distribué de configuration ;
|
||||||
|
- secrets manager distant ;
|
||||||
|
- configuration spécifique à des composants qui n'existent pas encore.
|
||||||
|
|
||||||
|
## 16. Git, versions et deltas
|
||||||
|
|
||||||
|
Chaque livraison est un delta commité :
|
||||||
|
|
||||||
|
```text
|
||||||
|
pre.NNN
|
||||||
|
pre.NNN-fix.NNN
|
||||||
|
rel.NNN
|
||||||
|
```
|
||||||
|
|
||||||
|
Cargo utilise les identifiants SemVer sans zéros de tête :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.1.3-pre.1
|
||||||
|
0.1.3-pre.1.fix.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Les noms de deltas/documents peuvent conserver la représentation `pre.001` / `fix.001`.
|
||||||
|
|
||||||
|
Une correction ultérieure ne réécrit pas un delta déjà livré.
|
||||||
|
|
||||||
|
Seul le commit final stable reçoit :
|
||||||
|
|
||||||
|
```text
|
||||||
|
v0.1.3
|
||||||
|
```
|
||||||
|
|
||||||
|
## 17. Première action de la session
|
||||||
|
|
||||||
|
Commencer par `0.1.3-pre.001` : audit, brainstorming et plan de travail.
|
||||||
|
|
||||||
|
Ne pas traiter ce `pre.001` comme un simple audit passif. Il doit produire les décisions nécessaires au développement des tranches suivantes et une matrice claire des responsabilités Config.
|
||||||
|
|
||||||
|
## 18. Dernière prerelease
|
||||||
|
|
||||||
|
La dernière prerelease de `0.1.3` devra :
|
||||||
|
|
||||||
|
- exécuter les validations finales ;
|
||||||
|
- consolider la documentation durable ;
|
||||||
|
- nettoyer/archiver uniquement ce que les règles exigent ;
|
||||||
|
- préparer le prompt de la release suivante ;
|
||||||
|
- vérifier le graphe de dépendances/features ;
|
||||||
|
- fermer les TODO de release ou les reporter explicitement ;
|
||||||
|
- préparer la livraison `rel.001` et le tag stable après validation utilisateur.
|
||||||
|
|
||||||
|
## 19. Validations minimales attendues
|
||||||
|
|
||||||
|
Lorsque les commandes sont applicables :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test --workspace
|
||||||
|
cargo tree -p ksp-config-lib
|
||||||
|
cargo tree -p ksp-config-lib -d
|
||||||
|
cargo tree -p ksp-config-lib -e features
|
||||||
|
```
|
||||||
|
|
||||||
|
Exécuter également tout script d'audit réellement présent dans le dépôt au moment de la validation.
|
||||||
|
|
||||||
|
Aucune commande non exécutée ne doit être déclarée réussie.
|
||||||
|
|
||||||
|
## 20. Critère de succès du `pre.001`
|
||||||
|
|
||||||
|
Le `pre.001` est terminé lorsque nous savons précisément :
|
||||||
|
|
||||||
|
- quels documents existent dans la première surface Config ;
|
||||||
|
- ce qui est global et ce qui est profilé ;
|
||||||
|
- comment fonctionne `default_profile` ;
|
||||||
|
- comment se résolvent les overrides `KSP_*`/`KSPB_*` ;
|
||||||
|
- comment documents unitaires et fichiers composites sont séparés puis résolus ;
|
||||||
|
- comment les secrets/public/debug sont classifiés et quels contrats autorisent leur lecture/mutation ;
|
||||||
|
- quels contrats de lecture/résolution/validation/mutation sont publics ou privilégiés, et comment `ksp-config-lib` reste l'unique manager des fichiers Config et variables d'environnement ;
|
||||||
|
- comment Config construit `LoggingSettings` sans dépendance inverse ;
|
||||||
|
- quelles dépendances externes sont réellement nécessaires ;
|
||||||
|
- si `0.1.3` reste une seule release ou doit être scindée ;
|
||||||
|
- quel est le découpage des prereleases de développement.
|
||||||
Reference in New Issue
Block a user