v0.1.2-pre.005

This commit is contained in:
2026-08-14 19:54:58 +02:00
parent 20c5643701
commit ea87a58e32
13 changed files with 746 additions and 27 deletions

View File

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

View File

@@ -0,0 +1,33 @@
<!-- file: crates/ksp-logging-lib/README.md -->
<!-- version: 1 -->
# 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.
## 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: 1 -->
# TODO ksp-logging-lib
## À fermer avant la stable 0.1.2
- exécuter les validations Cargo complètes de `pre.005` puis de la tranche finale ;
- exécuter explicitement le probe d'overhead ignoré et conserver son résultat dans le delta de validation approprié ;
- vérifier une dernière fois le graphe de dépendances/features et l'absence de contournement de la façade KSP ;
- synchroniser la documentation finale et le prompt `0.1.3 — ksp-config-lib`.
## 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,118 @@
<!-- file: crates/ksp-logging-lib/USAGE.md -->
<!-- version: 1 -->
# 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"
);
```

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-logging-lib/src/lib.rs
// version: 3
// version: 4
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
@@ -7,8 +7,9 @@
//! 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.004` 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.
//! `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;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-logging-lib/src/runtime.rs
// version: 7
// version: 8
use tracing_subscriber::Layer; // rust-rules: trait-import
use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import
@@ -224,12 +224,16 @@ fn build_non_blocking_output<W>(writer: W, thread_name: &str, settings: &crate::
where
W: std::io::Write + std::marker::Send + 'static,
{
let (non_blocking, worker_guard) = tracing_appender::non_blocking::NonBlockingBuilder::default().lossy(true).thread_name(thread_name).finish(writer);
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)

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

@@ -1,5 +1,5 @@
// file: crates/ksp-logging-lib/tests/runtime.rs
// version: 2
// version: 3
//! Integration tests for global initialization, takeover filtering, non-blocking outputs and hot reload.
@@ -62,6 +62,50 @@ fn read_directory_text(path: &std::path::Path) -> std::string::String {
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 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();
@@ -117,6 +161,7 @@ fn global_runtime_supports_takeover_non_blocking_outputs_hot_reload_and_single_i
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,

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

@@ -1,5 +1,5 @@
// file: crates/ksp-logging-lib/unit_tests/runtime.rs
// version: 4
// version: 5
#[test]
fn level_mapping_covers_all_ksp_levels() {
@@ -88,3 +88,107 @@ fn dropped_line_snapshots_add_saturating_by_sink() {
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);
}

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

@@ -1,13 +1,13 @@
<!-- file: docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md -->
<!-- version: 11 -->
<!-- version: 12 -->
# Plan KSP 0.1.2 — Logging foundation
## Statut
Plan actif de `0.1.2`, établi par `0.1.2-pre.001`, corrigé par `0.1.2-pre.001-fix.001`, concrétisé par la façade de `0.1.2-pre.002`, étendu au runtime subscriber par `0.1.2-pre.003`/`pre.003-fix.001`, puis complété par les sorties non bloquantes de `0.1.2-pre.004`. `pre.004-fix.001` a rendu explicite l'ordre de retrait des layers et des `WorkerGuard`; `pre.004-fix.002` corrige ensuite l'interaction entre la sanitization ANSI native de `tracing-subscriber 0.3.23` et le stripping fichier KSP.
Plan actif de `0.1.2`, établi par `0.1.2-pre.001`, corrigé par `0.1.2-pre.001-fix.001`, concrétisé par la façade de `0.1.2-pre.002`, étendu au runtime subscriber par `0.1.2-pre.003`/`pre.003-fix.001`, puis complété et corrigé par `pre.004`/`pre.004-fix.001..004`. `pre.005` ouvre la phase d'intégration/robustesse : saturation déterministe, concurrence pendant hot reload, audit de takeover, validation des spans de timing, documentation de crate et probe d'overhead grossier.
`pre.003-fix.001` a été validé dans l'environnement de développement avec `cargo fmt --all`, `cargo check --workspace`, `cargo clippy --workspace --all-targets` et `cargo test --workspace` propres sur la version Cargo `0.1.2-pre.3.fix.1`. `pre.004` ajoute `tracing-appender`, remplace la console synchrone par un writer non bloquant, introduit le fichier `Never/Hourly/Daily`, possède les `WorkerGuard`, expose des compteurs cumulés de dropped lines, déporte le stripping ANSI côté worker fichier et conserve le hot reload transactionnel des sinks. `pre.004-fix.001` a rendu explicite le retrait des anciens layers avant leurs `WorkerGuard`, mais la validation utilisateur après `cargo clean` a reproduit exactement le même échec du marqueur fichier. Cette seconde validation a invalidé l'hypothèse selon laquelle ce défaut précis provenait du drain. La cause réelle est la sanitization ANSI native de `tracing-subscriber 0.3.23`, activée par défaut dans `fmt::Layer` : elle transforme les séquences de contrôle présentes dans les valeurs avant qu'elles n'atteignent le writer. `pre.004-fix.002` la conserve pour la console mais la désactive sur le formatter fichier afin que le `StripAnsiWriter` KSP, placé côté worker, reçoive les séquences ANSI originales et les supprime réellement. Le lifecycle explicite de `fix.001` est conservé car il reste correct pour le retrait des sinks non bloquants. La validation de `pre.004-fix.002` confirme ensuite que l'écriture fichier et le stripping ANSI fonctionnent, mais révèle que le target externe `sqlx` est encore persisté. La cause est la composition du filtre global comme enfant frère des formatters dans un `Vec<Layer>` : le `Vec` agrège l'intérêt de callsite en conservant l'intérêt le plus élevé de ses enfants, de sorte qu'un formatter intéressé peut masquer le `Interest::never()` du `Targets`. `pre.004-fix.003` compose donc `Targets` devant le `Vec` des sinks avec `Layer::and_then`; le filtre redevient global pour tout le groupe de sorties sans utiliser `with_filter`/`Filtered`. La validation de `pre.004-fix.003` confirme que tous les tests workspace passent, y compris le takeover externe et le fichier non bloquant, mais `cargo clippy --workspace --all-targets` signale encore `vec_init_then_push` dans la construction du composite reloadable. `pre.004-fix.004` applique uniquement cette correction de forme sans modifier le comportement runtime.
`pre.003-fix.001` a été validé dans l'environnement de développement avec `cargo fmt --all`, `cargo check --workspace`, `cargo clippy --workspace --all-targets` et `cargo test --workspace` propres sur la version Cargo `0.1.2-pre.3.fix.1`. `pre.004` ajoute `tracing-appender`, remplace la console synchrone par un writer non bloquant, introduit le fichier `Never/Hourly/Daily`, possède les `WorkerGuard`, expose des compteurs cumulés de dropped lines, déporte le stripping ANSI côté worker fichier et conserve le hot reload transactionnel des sinks. `pre.004-fix.001` a rendu explicite le retrait des anciens layers avant leurs `WorkerGuard`, mais la validation utilisateur après `cargo clean` a reproduit exactement le même échec du marqueur fichier. Cette seconde validation a invalidé l'hypothèse selon laquelle ce défaut précis provenait du drain. La cause réelle est la sanitization ANSI native de `tracing-subscriber 0.3.23`, activée par défaut dans `fmt::Layer` : elle transforme les séquences de contrôle présentes dans les valeurs avant qu'elles n'atteignent le writer. `pre.004-fix.002` la conserve pour la console mais la désactive sur le formatter fichier afin que le `StripAnsiWriter` KSP, placé côté worker, reçoive les séquences ANSI originales et les supprime réellement. Le lifecycle explicite de `fix.001` est conservé car il reste correct pour le retrait des sinks non bloquants. La validation de `pre.004-fix.002` confirme ensuite que l'écriture fichier et le stripping ANSI fonctionnent, mais révèle que le target externe `sqlx` est encore persisté. La cause est la composition du filtre global comme enfant frère des formatters dans un `Vec<Layer>` : le `Vec` agrège l'intérêt de callsite en conservant l'intérêt le plus élevé de ses enfants, de sorte qu'un formatter intéressé peut masquer le `Interest::never()` du `Targets`. `pre.004-fix.003` compose donc `Targets` devant le `Vec` des sinks avec `Layer::and_then`; le filtre redevient global pour tout le groupe de sorties sans utiliser `with_filter`/`Filtered`. La validation de `pre.004-fix.003` confirme que tous les tests workspace passent, y compris le takeover externe et le fichier non bloquant, mais `cargo clippy --workspace --all-targets` signale encore `vec_init_then_push` dans la construction du composite reloadable. `pre.004-fix.004` applique uniquement cette correction de forme sans modifier le comportement runtime. La validation utilisateur finale de `pre.004-fix.004` sur `0.1.2-pre.4.fix.4` confirme `cargo fmt --all`, `cargo check --workspace`, `cargo clippy --workspace --all-targets` et `cargo test --workspace` propres ; le test runtime global passe avec takeover, fichier non bloquant, stripping ANSI et hot reload. Le graphe Cargo exécuté durant `pre.004` ne présentait aucun doublon et aucune activation KSP de `tracing-attributes`, `tracing-log`, `env-filter`, JSON/Serde ou ANSI formatter ; les dépendances n'ayant pas changé depuis, `pre.005` doit néanmoins refaire ces commandes avant validation de tranche.
## Base auditée
@@ -964,7 +964,7 @@ Le runtime reloadable reste fondé sur un `Vec<Box<dyn Layer<Registry>>>` placé
### `0.1.2-pre.004` — non-blocking console/fichier + guards + ANSI + reload sinks
Statut : implémenté, validations Cargo à exécuter.
Statut : validé après `pre.004-fix.004`. Les validations utilisateur `cargo fmt --all`, `cargo check --workspace`, `cargo clippy --workspace --all-targets` et `cargo test --workspace` sont propres sur `0.1.2-pre.4.fix.4`.
Réalisé :
@@ -986,15 +986,33 @@ La saturation déterministe des queues, les reloads concurrents et l'audit compl
### `0.1.2-pre.005` — intégration + concurrence + tests + audits
Objectifs :
Statut : implémenté dans le delta `pre.005`, validations Cargo à exécuter.
- compléter les tests settings/filtering/takeover/spans/runtime/reload ;
- tester les reloads concurrents et l'absence de deadlock/panic ;
- vérifier que les crates de fixture n'utilisent pas directement `tracing` ;
- auditer les réexports crate-root ;
- auditer la documentation de crate (`README.md`, `TODO.md`, `USAGE.md`) ;
- exécuter les audits Cargo/features/doublons et scripts réellement présents ;
- mesurer l'overhead grossier de la stratégie reload afin d'éviter une architecture inutilement coûteuse.
Réalisé :
- la construction `NonBlockingBuilder` de production est centralisée afin que le test unitaire puisse réutiliser exactement `lossy(true)` sans exposer la taille de queue dans `LoggingSettings` ;
- un test de saturation déterministe utilise une queue de capacité 1 et un writer worker volontairement bloqué : le producer doit terminer sous timeout et `ErrorCounter::dropped_lines()` doit devenir non nul, ce qui détecterait une régression vers la backpressure ;
- le test runtime global lance quatre producteurs façade KSP pendant 32 reloads alternant runtime silencieux et console présente avec niveau `Off`, afin d'exercer concurrence, création/retrait de workers et reload sans produire de bruit console ;
- un test d'intégration de lifecycle span capture le formatter `New | Close` et vérifie la présence de `time.busy` et `time.idle` ;
- un test d'ownership parcourt les crates workspace autres que Logging et refuse les dépendances directes `tracing`, `tracing-subscriber`, `tracing-appender` ainsi que leurs usages Rust directs ;
- `README.md`, `USAGE.md` et `TODO.md` sont introduits dans la crate avec la responsabilité, les frontières, l'initialisation/reload, les spans sync/async et les capacités différées ;
- un test diagnostic ignoré compare un subscriber local fixe à la même couche placée derrière `reload::Layer`; il utilise un garde-fou volontairement très large contre une régression grossière et doit être exécuté explicitement avec `cargo test -p ksp-logging-lib --test overhead -- --ignored --nocapture` ;
- aucun nouveau backend, setting public, format ou dépendance n'est introduit dans cette tranche.
Validations attendues avant clôture de `pre.005` :
```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
```
Aucun répertoire `scripts/` n'était présent dans la base stable `0.1.1`; aucun audit script historique n'est donc déclaré exécuté par anticipation. Si un script existe dans le dépôt de développement au moment de la validation, il doit être exécuté selon les règles générales.
### `0.1.2-pre.006` — validation finale + docs + cleanup + prompt 0.1.3
@@ -1057,7 +1075,7 @@ La release peut être stabilisée lorsque :
- les validations workspace et audits présents sont propres ;
- la documentation finale et le prompt `0.1.3` sont prêts.
## Questions ouvertes après `pre.004`
## Questions ouvertes après `pre.005`
Les deux questions d'API propres à `pre.002` sont résolues :
@@ -1066,10 +1084,10 @@ Les deux questions d'API propres à `pre.002` sont résolues :
La composition de reload est désormais fixée pour cette release à un `Vec` de layers boxed derrière une `reload::Layer`, ce qui autorise l'activation/désactivation des sinks et le remplacement de leurs paramètres sans second subscriber global. Le takeover `Targets` est composé avec `Layer::and_then` devant le `Vec` des sinks afin que son `Interest::never()` gouverne réellement tout le groupe de sorties ; les layers de sortie reloadables ne sont pas des `Filtered` remplacés directement. Pour les changements de sinks, `pre.004-fix.001` utilise `Handle::modify` afin de récupérer le `Vec` retiré : les anciens layers sont détruits avant les `WorkerGuard` correspondants, ce qui fixe explicitement le lifecycle de drain des writers non bloquants. `pre.004-fix.002` distingue en outre la sanitization des valeurs : activée côté console, désactivée côté formatter fichier car le `StripAnsiWriter` KSP possède la responsabilité de suppression avant persistence. `pre.004-fix.003` ferme enfin la fuite des targets externes révélée par le test d'intégration fichier.
Restent à confirmer par les prereleases suivantes sans remettre en cause ce contrat :
`pre.005` ferme les deux points de robustesse restants de `pre.004` : la saturation est testée avec le builder de production et une queue bornée injectée uniquement dans le test ; la concurrence/reload est exercée par quatre producteurs pendant des reconfigurations répétées. Le probe d'overhead existe désormais comme test diagnostic explicitement ignoré par défaut.
1. le détail visuel exact du formatter humain, sans transformer sa ponctuation en contrat public ;
2. la stratégie de test déterministe de saturation des queues avec writer injecté, sans exposer la taille de queue dans `LoggingSettings` ;
3. les mesures d'overhead et tests de concurrence/reload prévus pour `pre.005`.
Reste à confirmer pendant la validation finale sans remettre en cause le contrat :
La prochaine action après validation de `pre.004` est `0.1.2-pre.005` : intégration, concurrence, saturation déterministe, audits de façade/dépendances/features et mesure grossière de l'overhead de reload.
1. le détail visuel exact du formatter humain, sans transformer sa ponctuation en contrat public.
La prochaine action après validation de `pre.005` est `0.1.2-pre.006` : validation finale, documentation/cleanup, synchronisation des points d'entrée et préparation du prompt `0.1.3 — ksp-config-lib`.