16 Commits

Author SHA1 Message Date
3a7219fa59 v0.1.2-rel.001 2026-08-14 20:41:23 +02:00
77f6eaf487 v0.1.2-pre.006-fix.001 2026-08-14 20:34:54 +02:00
8a0c119878 v0.1.2-pre.006 2026-08-14 20:29:48 +02:00
d96f41fb8e v0.1.2-pre.005-fix.001 2026-08-14 20:08:18 +02:00
ea87a58e32 v0.1.2-pre.005 2026-08-14 19:54:58 +02:00
20c5643701 v0.1.2-pre.004-fix.004 2026-08-14 19:39:42 +02:00
d55903b13e v0.1.2-pre.004-fix.003 2026-08-14 19:37:37 +02:00
37f53f3080 v0.1.2-pre.004-fix.002 2026-08-14 19:33:59 +02:00
9f23eb950c v0.1.2-pre.004-fix.001 2026-08-14 19:22:56 +02:00
151590422d v0.1.2-pre.004 2026-08-14 18:43:54 +02:00
488d8ae0a8 v0.1.2-pre.003-fix.001 2026-08-14 18:29:56 +02:00
6e06802e38 v0.1.2-pre.003 2026-08-14 18:24:28 +02:00
697527675a v0.1.2-pre.002-fix 2026-08-14 18:10:38 +02:00
7b4444fb07 v0.1.2-pre.002 2026-08-14 18:03:20 +02:00
bd8401c0d0 v0.1.2-pre.001-fix.001 2026-08-14 17:52:34 +02:00
18ae0e4873 v0.1.2-pre.001 2026-08-14 16:43:34 +02:00
49 changed files with 6654 additions and 41 deletions

View File

@@ -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"

View File

@@ -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.

View 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

View 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.

View 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.

View 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.

View 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");

View 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;

View 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)+))
}};
}

View 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;

View 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;

View 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;

View 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;

View 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));
}

View 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:?}"
);
}

View 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());
}
}
}

View 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);
}

View 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());
}

View 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"));
}

View 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);
}

View 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);
}

View 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());
}

View 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));
}

View 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");
}

View 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
View 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`.

View 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
View 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.

View File

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

305
deltas/0.1.2/pre.003.md Normal file
View 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.

View 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
```

View 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
```

View 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
```

View 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
View 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.

View 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
View 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
```

View 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
View 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
View 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`.

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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 lexpansion des macros de `ksp-logging-lib` est un détail dimplémentation réservé à ces macros ; une crate consommatrice ne lutilise jamais directement et reste limitée à la façade KSP documentée.
## Program / Execution ## Program / Execution

View File

@@ -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`.

View 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.