v0.1.3-pre.006

This commit is contained in:
2026-08-15 20:40:38 +02:00
parent 3a479e4f43
commit b7323fe961
15 changed files with 629 additions and 106 deletions

View File

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

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-logging-lib/README.md -->
<!-- version: 4 -->
<!-- version: 5 -->
# ksp-logging-lib
@@ -14,8 +14,8 @@ La crate possède :
- `LoggingSettings`, la console explicite et les settings fichier indépendants de Config ;
- les formats runtime `Human/Compact/Pretty/Json` ;
- zéro, un ou plusieurs outputs fichier actifs simultanément, identifiés par `output_id` unique ;
- le routing par output sur niveau et target KSP ;
- le contrat de filtre `domain`, dont l'activation runtime structurée est volontairement séparée dans la tranche suivante ;
- le routing par output sur niveau, target KSP et champ structuré `domain` ;
- l'héritage du `domain` effectif à travers les spans, avec possibilité pour un event ou un span enfant de le remplacer explicitement ;
- 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 ;
@@ -28,7 +28,7 @@ L'API async de production reste indépendante de tout executor. Tokio est utilis
## Routing `domain`
`OutputFilter` représente déjà :
`OutputFilter` applique désormais les trois dimensions :
```text
level
@@ -36,9 +36,17 @@ targets[]
domains[]
```
`0.1.3-pre.005` active réellement `level` et `targets[]`. Un output actif dont `domains[]` est différent de `[*]` est refusé explicitement par `initialize/reinitialize` jusqu'à la tranche dédiée au routing structuré par champ `domain`.
Le `domain` reste un champ structuré distinct du target. Sa résolution runtime suit ces règles :
Ce refus évite d'assimiler artificiellement `domain` au target ou d'ignorer silencieusement la dimension demandée.
- le `domain` porté directement par un event est prioritaire ;
- sinon l'event hérite du `domain` effectif de son span ;
- un span qui porte son propre `domain` remplace celui de son parent ;
- un span sans `domain` hérite de celui de son parent au moment de sa création ;
- les événements de lifecycle de span utilisent le `domain` effectif du span concerné ;
- un selector `domains = ["*"]` accepte aussi les événements sans `domain` ;
- un selector nommé correspond par préfixe et ne sélectionne pas un événement sans `domain`.
Le routing `domain` est appliqué en plus du niveau et du target de l'output. Il ne modifie ni le target propriétaire KSP ni les champs formatés.
## Frontières

View File

@@ -1,20 +1,17 @@
<!-- file: crates/ksp-logging-lib/TODO.md -->
<!-- version: 4 -->
<!-- version: 5 -->
# TODO ksp-logging-lib
## À fermer pendant `0.1.3`
`0.1.3-pre.005` active le runtime multi-sink pour les dimensions disponibles directement dans les métadonnées `tracing` : niveau et target.
Les trois tranches Logging nécessaires à la fondation Config sont maintenant couvertes fonctionnellement :
La tranche Logging suivante doit encore :
- `pre.004` : contrats/settings multi-output ;
- `pre.005` : runtime multi-sink, formats et routing level/target ;
- `pre.006` : routing structuré `domain`, héritage de spans et lifecycle.
- appliquer réellement `OutputFilter.domains[]` à partir du champ structuré `domain` ;
- définir l'héritage du domain pour les events émis dans un span lorsqu'ils ne portent pas eux-mêmes le champ ;
- conserver une sémantique cohérente pour les événements de lifecycle de span ;
- couvrir ce routing par des tests dédiés avant de figer `std.logging.schema.json`.
Tant que cette tranche n'est pas livrée, un output actif avec `domains[] != ["*"]` est refusé explicitement par `initialize/reinitialize`.
Avant le gel de `std.logging.schema.json`, il reste uniquement à faire valider `pre.006` par les commandes workspace usuelles. Les travaux suivants de `0.1.3` reviennent ensuite à `ksp-config-lib`.
## Capacités désormais actives
@@ -22,12 +19,13 @@ Le runtime supporte maintenant :
- zéro, un ou plusieurs outputs fichier simultanés ;
- une console indépendante ;
- routing par niveau et target pour chaque output ;
- routing par niveau, target et `domain` structuré pour chaque output ;
- `Human`, `Compact`, `Pretty` et `Json` ;
- ANSI console configurable et fichiers persistants sans ANSI ;
- guards non bloquants indépendants ;
- compteurs agrégés et compteurs cumulatifs par `output_id` fichier ;
- hot reload transactionnel du groupe de sinks.
- hot reload transactionnel du groupe de sinks ;
- héritage du `domain` effectif pour les events/spans et lifecycle de spans.
## Capacités différées

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-logging-lib/USAGE.md -->
<!-- version: 4 -->
<!-- version: 5 -->
# Utilisation de ksp-logging-lib
@@ -46,18 +46,20 @@ Une sortie fichier possède un `output_id` stable et unique dans `LoggingSetting
Les fichiers persistants interdisent `ansi = true`.
## Runtime multi-output de `pre.005`
## Runtime multi-output et routing `domain`
`0.1.3-pre.005` active réellement :
`0.1.3-pre.005` active le multi-sink, les formats et le routing niveau/target ; `0.1.3-pre.006` complète le routing structuré `domain`. Le runtime supporte donc réellement :
- plusieurs fichiers simultanés ;
- les formats `Human`, `Compact`, `Pretty` et `Json` ;
- l'ANSI console ;
- le routing par niveau et target pour chaque output ;
- le routing par niveau, target et `domain` pour chaque output ;
- le comptage cumulatif des lignes abandonnées par `output_id` fichier ;
- le hot reload de ces sorties sans réinstaller le subscriber global.
Le routing `domain` est volontairement séparé. `OutputFilter` continue de représenter `domains[]`, mais un output actif dont cette dimension est différente de `["*"]` est refusé par `initialize/reinitialize` jusqu'à la tranche suivante. La configuration n'est donc jamais acceptée puis partiellement ignorée.
Le `domain` n'est jamais transformé en target. Un event portant directement `domain` utilise cette valeur ; sinon il hérite du `domain` effectif de son span. Un span explicite remplace le `domain` de son parent, tandis qu'un span sans `domain` l'hérite. Les lifecycle events d'un span utilisent le même `domain` effectif.
Pour `domains[]`, `["*"]` accepte tous les événements, y compris ceux sans `domain`. Un selector nommé correspond par préfixe et ne sélectionne pas une entrée sans `domain`.
Les helpers `ConsoleSettings::stdout()` et `ConsoleSettings::stderr()` restent des raccourcis `Human`, sans ANSI et sans restriction supplémentaire par output.
@@ -92,7 +94,7 @@ Une configuration sans output actif est valide et installe une infrastructure in
## Construction d'un runtime multi-output
Le runtime peut maintenant activer plusieurs sorties ayant des formats et filtres metadata distincts :
Le runtime peut activer plusieurs sorties ayant des formats et filtres niveau/target/domain distincts :
```rust
let console = ksp_logging_lib::ConsoleSettings::new(
@@ -131,7 +133,19 @@ let settings = ksp_logging_lib::LoggingSettings::new(
let validation = settings.validate();
```
`validate()` vérifie le contrat structurel. `initialize/reinitialize` appliquent le niveau et les targets par output ; les domains spécifiques restent refusés jusqu'à la tranche de routing structuré.
`validate()` vérifie le contrat structurel. `initialize/reinitialize` appliquent ensuite conjointement niveau, target et `domain` par output.
Par exemple, un fichier réservé au domaine Store peut utiliser :
```rust
ksp_logging_lib::OutputFilter::new(
ksp_logging_lib::LogFilterLevel::Debug,
std::vec!["ksp-store-lib".to_string()],
std::vec!["store".to_string()],
)
```
Un event `domain = "store.postgres"` correspond au selector `store`; un event sans `domain` n'y correspond pas.
## Hot reload

View File

@@ -0,0 +1,181 @@
// file: crates/ksp-logging-lib/src/domain.rs
// version: 1
std::thread_local! {
static CURRENT_DOMAIN: std::cell::RefCell<std::option::Option<std::string::String>> = const { std::cell::RefCell::new(std::option::Option::None) };
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
struct SpanDomain {
value: std::option::Option<std::string::String>,
}
#[derive(Default)]
struct DomainVisitor {
value: std::option::Option<std::string::String>,
}
impl tracing::field::Visit for DomainVisitor {
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
if field.name() == "domain" {
self.value = std::option::Option::Some(value.to_string());
}
}
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
if field.name() == "domain" {
self.value = std::option::Option::Some(format!("{value:?}"));
}
}
}
pub(crate) struct DomainContextLayer;
impl DomainContextLayer {
pub(crate) const fn new() -> Self {
return Self;
}
}
impl<S> tracing_subscriber::Layer<S> for DomainContextLayer
where
S: tracing::Subscriber + for<'lookup> tracing_subscriber::registry::LookupSpan<'lookup>,
{
fn on_new_span(&self, attrs: &tracing::span::Attributes<'_>, id: &tracing::span::Id, ctx: tracing_subscriber::layer::Context<'_, S>) {
let mut visitor = DomainVisitor::default();
attrs.record(&mut visitor);
let effective_domain = match visitor.value {
std::option::Option::Some(domain) => std::option::Option::Some(domain),
std::option::Option::None => span_parent_domain(id, &ctx),
};
if let std::option::Option::Some(span) = ctx.span(id) {
span.extensions_mut().insert(SpanDomain { value: effective_domain.clone() });
}
set_current_domain(effective_domain.as_deref());
}
fn on_record(&self, id: &tracing::span::Id, values: &tracing::span::Record<'_>, ctx: tracing_subscriber::layer::Context<'_, S>) {
let mut visitor = DomainVisitor::default();
values.record(&mut visitor);
if let std::option::Option::Some(domain) = visitor.value {
if let std::option::Option::Some(span) = ctx.span(id) {
let mut extensions = span.extensions_mut();
if let std::option::Option::Some(stored) = extensions.get_mut::<SpanDomain>() {
stored.value = std::option::Option::Some(domain.clone());
} else {
extensions.insert(SpanDomain { value: std::option::Option::Some(domain.clone()) });
}
}
set_current_domain(std::option::Option::Some(domain.as_str()));
return;
}
let domain = span_domain(id, &ctx);
set_current_domain(domain.as_deref());
}
fn on_event(&self, event: &tracing::Event<'_>, ctx: tracing_subscriber::layer::Context<'_, S>) {
let mut visitor = DomainVisitor::default();
event.record(&mut visitor);
let effective_domain = match visitor.value {
std::option::Option::Some(domain) => std::option::Option::Some(domain),
std::option::Option::None => event_parent_domain(event, &ctx),
};
set_current_domain(effective_domain.as_deref());
}
fn on_enter(&self, id: &tracing::span::Id, ctx: tracing_subscriber::layer::Context<'_, S>) {
let domain = span_domain(id, &ctx);
set_current_domain(domain.as_deref());
}
fn on_exit(&self, id: &tracing::span::Id, ctx: tracing_subscriber::layer::Context<'_, S>) {
let domain = span_domain(id, &ctx);
set_current_domain(domain.as_deref());
}
fn on_close(&self, id: tracing::span::Id, ctx: tracing_subscriber::layer::Context<'_, S>) {
let domain = span_domain(&id, &ctx);
set_current_domain(domain.as_deref());
}
}
pub(crate) fn current_domain_matches(selectors: &[std::string::String]) -> bool {
if let [selector] = selectors
&& selector == "*"
{
return true;
}
return CURRENT_DOMAIN.with(|current| -> bool {
let borrow_result = current.try_borrow();
let current = match borrow_result {
std::result::Result::Ok(current) => current,
std::result::Result::Err(_) => return false,
};
let domain = match current.as_ref() {
std::option::Option::Some(domain) => domain,
std::option::Option::None => return false,
};
return selectors.iter().any(|selector| -> bool {
return domain.starts_with(selector.as_str());
});
});
}
fn set_current_domain(domain: std::option::Option<&str>) {
CURRENT_DOMAIN.with(|current| {
let borrow_result = current.try_borrow_mut();
if let std::result::Result::Ok(mut current) = borrow_result {
*current = domain.map(std::borrow::ToOwned::to_owned);
}
});
}
fn span_parent_domain<S>(id: &tracing::span::Id, ctx: &tracing_subscriber::layer::Context<'_, S>) -> std::option::Option<std::string::String>
where
S: tracing::Subscriber + for<'lookup> tracing_subscriber::registry::LookupSpan<'lookup>,
{
let span = match ctx.span(id) {
std::option::Option::Some(span) => span,
std::option::Option::None => return std::option::Option::None,
};
let parent = match span.parent() {
std::option::Option::Some(parent) => parent,
std::option::Option::None => return std::option::Option::None,
};
let extensions = parent.extensions();
return extensions.get::<SpanDomain>().and_then(|domain| -> std::option::Option<std::string::String> {
return domain.value.clone();
});
}
fn span_domain<S>(id: &tracing::span::Id, ctx: &tracing_subscriber::layer::Context<'_, S>) -> std::option::Option<std::string::String>
where
S: tracing::Subscriber + for<'lookup> tracing_subscriber::registry::LookupSpan<'lookup>,
{
let span = match ctx.span(id) {
std::option::Option::Some(span) => span,
std::option::Option::None => return std::option::Option::None,
};
let extensions = span.extensions();
return extensions.get::<SpanDomain>().and_then(|domain| -> std::option::Option<std::string::String> {
return domain.value.clone();
});
}
fn event_parent_domain<S>(event: &tracing::Event<'_>, ctx: &tracing_subscriber::layer::Context<'_, S>) -> std::option::Option<std::string::String>
where
S: tracing::Subscriber + for<'lookup> tracing_subscriber::registry::LookupSpan<'lookup>,
{
let parent = match ctx.event_span(event) {
std::option::Option::Some(parent) => parent,
std::option::Option::None => return std::option::Option::None,
};
let extensions = parent.extensions();
return extensions.get::<SpanDomain>().and_then(|domain| -> std::option::Option<std::string::String> {
return domain.value.clone();
});
}
#[cfg(test)]
#[path = "../unit_tests/domain.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-logging-lib/src/lib.rs
// version: 6
// version: 7
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
@@ -7,10 +7,11 @@
//! 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. The crate owns the single global subscriber, KSP takeover filtering, hot reload and non-blocking outputs. `0.1.3-pre.005` activates
//! multiple simultaneous file outputs, per-output level/target routing, selectable formats, console ANSI and per-file dropped-line accounting. Structured
//! `domain` routing remains explicitly rejected until its dedicated runtime tranche so field-based routing is never approximated or ignored silently.
//! `tracing` stack. The crate owns the single global subscriber, KSP takeover filtering, hot reload and non-blocking outputs. `0.1.3-pre.006` supports
//! multiple simultaneous outputs with per-output level/target/domain routing, selectable formats, console ANSI and per-file dropped-line accounting. Structured
//! `domain` routing remains distinct from targets and follows explicit event domains or inherited span domains.
mod domain;
mod error;
mod macros;
mod runtime;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-logging-lib/src/runtime.rs
// version: 10
// version: 11
use tracing_subscriber::Layer; // rust-rules: trait-import
use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import
@@ -219,10 +219,6 @@ fn prepare_runtime(settings: &crate::LoggingSettings) -> ksp_core_lib::Result<Pr
if let std::option::Option::Some(error) = validation_error {
return std::result::Result::Err(error);
}
let runtime_validation = validate_current_runtime_capabilities(settings);
if let std::result::Result::Err(error) = runtime_validation {
return std::result::Result::Err(error);
}
let enabled_console = settings.console().filter(|console| -> bool {
return console.enabled();
});
@@ -248,38 +244,11 @@ fn prepare_runtime(settings: &crate::LoggingSettings) -> ksp_core_lib::Result<Pr
if output_layers.is_empty() {
return std::result::Result::Ok(PreparedRuntime { layers: RuntimeLayers::new(), outputs });
}
output_layers.insert(0, crate::domain::DomainContextLayer::new().boxed());
let takeover_layer = build_target_filter(settings).and_then(output_layers).boxed();
return std::result::Result::Ok(PreparedRuntime { layers: vec![takeover_layer], outputs });
}
fn validate_current_runtime_capabilities(settings: &crate::LoggingSettings) -> ksp_core_lib::Result<()> {
if let std::option::Option::Some(console) = settings.console()
&& console.enabled()
&& !domains_are_unrestricted(console.filter())
{
return runtime_capability_error("domain routing requires the dedicated structured-domain runtime tranche");
}
for file in settings.files() {
if file.enabled() && !domains_are_unrestricted(file.filter()) {
return runtime_capability_error("domain routing requires the dedicated structured-domain runtime tranche");
}
}
return std::result::Result::Ok(());
}
fn domains_are_unrestricted(filter: &crate::OutputFilter) -> bool {
return match filter.domains() {
[selector] => selector == "*",
_ => false,
};
}
fn runtime_capability_error(message: &str) -> ksp_core_lib::Result<()> {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, message).with_context("runtime_contract", "metadata-routing-before-domain-routing"),
);
}
fn build_console_output(console: &crate::ConsoleSettings, settings: &crate::LoggingSettings) -> PreparedOutput {
return match console.output() {
crate::ConsoleOutput::Stdout => build_non_blocking_output(

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-logging-lib/src/writer.rs
// version: 2
// version: 3
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum StripAnsiState {
@@ -158,9 +158,12 @@ fn metadata_matches_filter(metadata: &tracing::Metadata<'_>, filter: &crate::Out
if !level_is_enabled(metadata.level(), filter.level()) {
return false;
}
return filter.targets().iter().any(|selector| -> bool {
if !filter.targets().iter().any(|selector| -> bool {
return selector == "*" || metadata.target().starts_with(selector.as_str());
});
}) {
return false;
}
return crate::domain::current_domain_matches(filter.domains());
}
fn level_is_enabled(level: &tracing::Level, filter: crate::LogFilterLevel) -> bool {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-logging-lib/tests/runtime.rs
// version: 7
// version: 8
//! Integration tests for global initialization, takeover filtering, non-blocking outputs and hot reload.
@@ -173,21 +173,6 @@ fn global_runtime_supports_takeover_non_blocking_outputs_hot_reload_and_single_i
assert!(logging_trace_enabled());
assert!(!external_error_enabled());
exercise_concurrent_reload(&mut guard, &disabled);
let domain_routed = ksp_logging_lib::LoggingSettings::new(
ksp_logging_lib::LogFilterLevel::Info,
ksp_logging_lib::SpanEvents::Off,
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::new(
true,
ksp_logging_lib::ConsoleOutput::Stderr,
false,
ksp_logging_lib::LogFormat::Human,
ksp_logging_lib::OutputFilter::new(ksp_logging_lib::LogFilterLevel::Info, std::vec!["*".to_string()], std::vec!["logging".to_string()]),
)),
std::vec::Vec::new(),
);
let domain_reload = ksp_logging_lib::reinitialize(&mut guard, &domain_routed);
assert!(domain_reload.is_err());
assert_ne!(guard.settings(), &domain_routed);
let human_directory = root.join("human");
let compact_directory = root.join("compact");
let pretty_directory = root.join("pretty");
@@ -272,6 +257,119 @@ fn global_runtime_supports_takeover_non_blocking_outputs_hot_reload_and_single_i
assert_eq!(guard.dropped_file_lines("file.pretty.error"), std::option::Option::Some(0));
assert_eq!(guard.dropped_file_lines("file.json.logging"), std::option::Option::Some(0));
assert_eq!(guard.dropped_file_lines("file.unknown"), std::option::Option::None);
let domain_logging_directory = root.join("domain-logging");
let domain_store_directory = root.join("domain-store");
let domain_any_directory = root.join("domain-any");
let domain_settings = ksp_logging_lib::LoggingSettings::new(
ksp_logging_lib::LogFilterLevel::Trace,
ksp_logging_lib::SpanEvents::Full,
std::option::Option::None,
std::vec![
ksp_logging_lib::FileSettings::new(
"file.domain.logging",
true,
domain_logging_directory.as_path(),
"logging.log",
ksp_logging_lib::FileRotation::Never,
ksp_logging_lib::LogFormat::Human,
ksp_logging_lib::OutputFilter::new(
ksp_logging_lib::LogFilterLevel::Trace,
std::vec![LOGGING_TARGET.to_string()],
std::vec!["logging".to_string()],
),
),
ksp_logging_lib::FileSettings::new(
"file.domain.store",
true,
domain_store_directory.as_path(),
"store.log",
ksp_logging_lib::FileRotation::Never,
ksp_logging_lib::LogFormat::Human,
ksp_logging_lib::OutputFilter::new(
ksp_logging_lib::LogFilterLevel::Trace,
std::vec![LOGGING_TARGET.to_string()],
std::vec!["store".to_string()],
),
),
ksp_logging_lib::FileSettings::new(
"file.domain.any",
true,
domain_any_directory.as_path(),
"any.log",
ksp_logging_lib::FileRotation::Never,
ksp_logging_lib::LogFormat::Human,
ksp_logging_lib::OutputFilter::new(ksp_logging_lib::LogFilterLevel::Trace, std::vec![LOGGING_TARGET.to_string()], std::vec!["*".to_string()],),
),
],
);
let domain_reload = ksp_logging_lib::reinitialize(&mut guard, &domain_settings);
assert!(domain_reload.is_ok());
ksp_logging_lib::info!(target: LOGGING_TARGET, domain = "logging.runtime", "direct logging domain marker");
ksp_logging_lib::info!(target: LOGGING_TARGET, domain = "store", "direct store domain marker");
ksp_logging_lib::info!(target: LOGGING_TARGET, "undomained marker");
let logging_lifecycle = ksp_logging_lib::info_span!(target: LOGGING_TARGET, "logging_lifecycle_span", domain = "logging");
logging_lifecycle.in_scope(|| {
return;
});
let store_lifecycle = ksp_logging_lib::info_span!(target: LOGGING_TARGET, "store_lifecycle_span", domain = "store");
store_lifecycle.in_scope(|| {
return;
});
let logging_parent = ksp_logging_lib::info_span!(target: LOGGING_TARGET, "logging_parent_span", domain = "logging");
logging_parent.in_scope(|| {
ksp_logging_lib::info!(target: LOGGING_TARGET, "inherited logging marker");
ksp_logging_lib::info!(target: LOGGING_TARGET, domain = "store", "event override store marker");
let inherited_child = ksp_logging_lib::info_span!(target: LOGGING_TARGET, "inherited_child_span");
inherited_child.in_scope(|| {
ksp_logging_lib::info!(target: LOGGING_TARGET, "child inherited logging marker");
});
let store_child = ksp_logging_lib::info_span!(target: LOGGING_TARGET, "store_child_span", domain = "store");
store_child.in_scope(|| {
ksp_logging_lib::info!(target: LOGGING_TARGET, "child explicit store marker");
});
});
let store_parent = ksp_logging_lib::info_span!(target: LOGGING_TARGET, "store_parent_span", domain = "store");
store_parent.in_scope(|| {
ksp_logging_lib::info!(target: LOGGING_TARGET, "inherited store marker");
});
let disable_after_domain = ksp_logging_lib::reinitialize(&mut guard, &disabled);
assert!(disable_after_domain.is_ok());
let domain_logging_text = read_directory_text(domain_logging_directory.as_path());
let domain_store_text = read_directory_text(domain_store_directory.as_path());
let domain_any_text = read_directory_text(domain_any_directory.as_path());
assert!(domain_logging_text.contains("direct logging domain marker"));
assert!(domain_logging_text.contains("inherited logging marker"));
assert!(domain_logging_text.contains("child inherited logging marker"));
assert!(domain_logging_text.contains("logging_lifecycle_span"));
assert!(!domain_logging_text.contains("direct store domain marker"));
assert!(!domain_logging_text.contains("event override store marker"));
assert!(!domain_logging_text.contains("child explicit store marker"));
assert!(!domain_logging_text.contains("inherited store marker"));
assert!(!domain_logging_text.contains("store_lifecycle_span"));
assert!(!domain_logging_text.contains("undomained marker"));
assert!(domain_store_text.contains("direct store domain marker"));
assert!(domain_store_text.contains("event override store marker"));
assert!(domain_store_text.contains("child explicit store marker"));
assert!(domain_store_text.contains("inherited store marker"));
assert!(domain_store_text.contains("store_lifecycle_span"));
assert!(!domain_store_text.contains("direct logging domain marker"));
assert!(!domain_store_text.contains("inherited logging marker"));
assert!(!domain_store_text.contains("child inherited logging marker"));
assert!(!domain_store_text.contains("logging_lifecycle_span"));
assert!(!domain_store_text.contains("undomained marker"));
assert!(domain_any_text.contains("direct logging domain marker"));
assert!(domain_any_text.contains("direct store domain marker"));
assert!(domain_any_text.contains("undomained marker"));
assert!(domain_any_text.contains("inherited logging marker"));
assert!(domain_any_text.contains("event override store marker"));
assert!(domain_any_text.contains("child inherited logging marker"));
assert!(domain_any_text.contains("child explicit store marker"));
assert!(domain_any_text.contains("inherited store marker"));
assert!(domain_any_text.contains("logging_lifecycle_span"));
assert!(domain_any_text.contains("store_lifecycle_span"));
assert_eq!(guard.dropped_file_lines("file.domain.logging"), std::option::Option::Some(0));
assert_eq!(guard.dropped_file_lines("file.domain.store"), std::option::Option::Some(0));
assert_eq!(guard.dropped_file_lines("file.domain.any"), std::option::Option::Some(0));
let dropped = guard.dropped_lines();
assert_eq!(dropped.total(), dropped.console().saturating_add(dropped.file()));
let second_initialize = ksp_logging_lib::initialize(&disabled);

View File

@@ -0,0 +1,20 @@
// file: crates/ksp-logging-lib/unit_tests/domain.rs
// version: 1
#[test]
fn wildcard_domain_matches_with_or_without_current_domain() {
super::set_current_domain(std::option::Option::None);
assert!(super::current_domain_matches(&["*".to_string()]));
super::set_current_domain(std::option::Option::Some("logging"));
assert!(super::current_domain_matches(&["*".to_string()]));
}
#[test]
fn named_domain_requires_current_matching_prefix() {
super::set_current_domain(std::option::Option::None);
assert!(!super::current_domain_matches(&["logging".to_string()]));
super::set_current_domain(std::option::Option::Some("logging.runtime"));
assert!(super::current_domain_matches(&["logging".to_string()]));
assert!(super::current_domain_matches(&["store".to_string(), "logging.runtime".to_string()]));
assert!(!super::current_domain_matches(&["store".to_string()]));
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-logging-lib/unit_tests/runtime.rs
// version: 8
// version: 9
#[test]
fn level_mapping_covers_all_ksp_levels() {
@@ -154,7 +154,7 @@ fn multi_sink_runtime_accepts_metadata_routing_formats_and_console_ansi() {
}
#[test]
fn domain_routing_is_explicitly_deferred_instead_of_ignored() {
fn domain_routing_is_accepted_by_runtime_preparation() {
let console = crate::ConsoleSettings::new(
true,
crate::ConsoleOutput::Stdout,
@@ -165,12 +165,7 @@ fn domain_routing_is_explicitly_deferred_instead_of_ignored() {
let settings = crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::Some(console), std::vec::Vec::new());
assert!(settings.validate().is_ok());
let result = super::prepare_runtime(&settings);
assert!(result.is_err());
let error = match result {
std::result::Result::Ok(_) => return,
std::result::Result::Err(error) => error,
};
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_SETTINGS);
assert!(result.is_ok());
}
struct BlockingWriter {

226
deltas/0.1.3/pre.006.md Normal file
View File

@@ -0,0 +1,226 @@
<!-- file: deltas/0.1.3/pre.006.md -->
<!-- version: 1 -->
# Delta 0.1.3-pre.006
## Base requise
Livraison précédente validée :
```text
0.1.3-pre.005-fix.001
```
Version technique de cette base :
```text
workspace.package.version = "0.1.3-pre.5.fix.1"
Cargo.toml header version = 46
```
Validations utilisateur exécutées le 2026-08-15 :
```text
cargo fmt --all OK
cargo check --workspace OK
cargo clippy --workspace --all-targets OK
cargo test --workspace OK
cargo tree -p ksp-logging-lib OK
cargo tree -p ksp-logging-lib -d OK — aucune duplication
cargo tree -p ksp-logging-lib -e features OK
```
Le fix JSON de `pre.005` est donc clos avant ouverture de cette tranche.
## Objet de pre.006
Fermer la troisième dimension de routing déjà représentée par `OutputFilter` :
```text
level
target
domain
```
`domain` reste un champ structuré indépendant du target. Cette tranche ne change pas les contrats Config et ne crée encore aucun document JSON Config.
## Domain effectif
Le runtime applique la sémantique suivante :
1. un event portant directement `domain` utilise cette valeur ;
2. sinon l'event hérite du domain effectif de son span ;
3. un span portant directement `domain` définit son domain effectif ;
4. un span sans `domain` hérite du domain effectif de son parent au moment de sa création ;
5. un span enfant explicite peut donc remplacer le domain hérité ;
6. les lifecycle events `NEW`, `ENTER`, `EXIT` et `CLOSE` utilisent le domain effectif du span concerné.
Un domain direct d'event ne modifie pas le domain mémorisé du span.
## Routing par output
Le writer routé applique maintenant conjointement :
```text
OutputFilter.level
OutputFilter.targets[]
OutputFilter.domains[]
```
Pour `domains[]` :
- `["*"]` signifie aucune restriction et accepte aussi un event/span sans domain ;
- un selector nommé correspond par préfixe ;
- un selector nommé ne correspond jamais à une entrée sans domain.
La politique globale existante reste distincte :
```text
default_filter + TargetFilter[]
DomainContextLayer + groupe de sinks
OutputFilter level/target/domain de chaque sink
```
Aucune convention de pseudo-target domain n'est introduite.
## Couche interne de contexte
`DomainContextLayer` est une couche privée de `ksp-logging-lib` installée avant les formatters lorsque le runtime possède au moins un output actif.
Elle :
- capture le champ `domain` des events et spans ;
- conserve le domain effectif d'un span dans ses extensions runtime ;
- expose uniquement au routing interne le domain effectif correspondant au callback en cours ;
- conserve les champs originaux pour les formatters Human/Compact/Pretty/Json ;
- n'ajoute aucun type public et ne change pas la façade consommateur.
Le contexte courant utilisé par le routing reste interne au thread de dispatch Logging et n'est pas une variable globale Config/applicative.
## Runtime préservé
Cette tranche conserve sans changement de contrat :
- subscriber global unique ;
- takeover des targets externes ;
- multi-sink ;
- formats Human/Compact/Pretty/Json ;
- ANSI console et stripping fichier ;
- writers non bloquants ;
- guards et compteurs par `output_id` ;
- hot reload transactionnel ;
- comportement de saturation.
Le refus transitoire `domains != ["*"]` introduit en `pre.005` est supprimé puisque la dimension domain est maintenant exécutée réellement.
## Tests
Les tests unitaires couvrent :
- wildcard domain avec ou sans domain courant ;
- selector nommé absent sans domain ;
- correspondance par préfixe ;
- acceptation runtime d'un filtre domain spécifique.
Le test d'intégration global ajoute trois sinks simultanés :
```text
file.domain.logging -> domains = ["logging"]
file.domain.store -> domains = ["store"]
file.domain.any -> domains = ["*"]
```
Il couvre :
- domain direct d'event ;
- event sans domain ;
- héritage depuis un span parent ;
- override direct d'event ;
- enfant sans domain ;
- enfant avec domain explicite ;
- lifecycle de spans Logging/Store ;
- maintien des compteurs à zéro dans le scénario nominal.
## Dépendances
Aucune nouvelle dépendance et aucune nouvelle feature Cargo ne sont ajoutées.
`ksp-logging-lib` reste le seul propriétaire direct de la stack `tracing*` et ne dépend toujours pas de `ksp-config-lib`.
## Plan
Les trois tranches Logging nécessaires avant le premier schema Config sont désormais fonctionnellement couvertes :
```text
pre.004 contrats/settings multi-output
pre.005 runtime multi-sink + level/target/formats
pre.006 routing structuré domain
```
Après validation utilisateur de `pre.006`, la prochaine tranche est :
```text
pre.007 JSON/JSON Schema + std.logging.json
```
## Version technique
La prerelease devient :
```text
workspace.package.version = "0.1.3-pre.6"
Cargo.toml header version = 47
```
## Fichiers ajoutés
```text
crates/ksp-logging-lib/src/domain.rs
crates/ksp-logging-lib/unit_tests/domain.rs
deltas/0.1.3/pre.006.md
```
## Fichiers modifiés
```text
Cargo.toml
crates/ksp-logging-lib/README.md
crates/ksp-logging-lib/TODO.md
crates/ksp-logging-lib/USAGE.md
crates/ksp-logging-lib/src/lib.rs
crates/ksp-logging-lib/src/runtime.rs
crates/ksp-logging-lib/src/writer.rs
crates/ksp-logging-lib/tests/runtime.rs
crates/ksp-logging-lib/unit_tests/runtime.rs
docs/plans/000-README.md
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md
```
## Hors scope
Cette tranche ne modifie pas :
- `ksp-config-lib` ;
- le registre `file_id` ;
- JSON/JSON Schema ;
- `std.logging.json` ;
- profils/composites ;
- `.env` ou les variables d'environnement ;
- persistence Config.
## Validations à exécuter
```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 n'est déclarée réussie tant qu'elle n'a pas été exécutée sur le workspace utilisateur.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/000-README.md -->
<!-- version: 12 -->
<!-- version: 13 -->
# Plans KSP
@@ -13,7 +13,7 @@ Un plan décrit le périmètre, les décisions déjà acquises, les questions ou
- [`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`.
- [`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`.
- [`005-V0_1_3_CONFIG_FOUNDATION_PLAN.md`](005-V0_1_3_CONFIG_FOUNDATION_PLAN.md) — plan actif de `0.1.3 — Configuration foundation`, établi par `0.1.3-pre.001`, corrigé par `pre.001-fix.001`, complété par `pre.001-fix.002` pour le registre `file_id`/bootstrap/non-régression Logging, regranularisé par `pre.001-fix.003`, puis rescindé pendant `pre.005` pour traiter séparément le routing structuré `domain` avant le premier schema Logging.
- [`005-V0_1_3_CONFIG_FOUNDATION_PLAN.md`](005-V0_1_3_CONFIG_FOUNDATION_PLAN.md) — plan actif de `0.1.3 — Configuration foundation`, établi par `0.1.3-pre.001`, corrigé par `pre.001-fix.001`, complété par `pre.001-fix.002` pour le registre `file_id`/bootstrap/non-régression Logging, regranularisé par `pre.001-fix.003`, puis rescindé pendant `pre.005`; `pre.006` ferme maintenant le routing structuré `domain` avant le premier schema Logging de `pre.007`.
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 -->
<!-- version: 10 -->
<!-- version: 11 -->
# Séquence des releases fonctionnelles KSP
@@ -219,6 +219,8 @@ pre.015 clôture
Cette prévision n'est pas un plafond : chaque prerelease doit rester une petite tranche, avec scission explicite si l'objectif dépasse environ 1520 minutes de travail effectif.
Après validation de `pre.005-fix.001`, `pre.006` ferme le routing Logging structuré `domain`; `pre.007` redevient donc la prochaine tranche Config avec JSON/JSON Schema et le premier document `std.logging.json`.
## `0.1.4` — Config desktop par défaut
La décision `0.1.3-pre.001` conserve cette release comme étape suivante par défaut.

View File

@@ -1,11 +1,11 @@
<!-- file: docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md -->
<!-- version: 8 -->
<!-- version: 9 -->
# Plan `0.1.3` — Configuration foundation
## 1. Statut et objectif
Ce plan a été établi par `0.1.3-pre.001`, corrigé par `0.1.3-pre.001-fix.001/.002/.003`, puis exécuté par petites tranches. `pre.002` a livré le bootstrap Config, `pre.003` le registre `file_id`, `pre.004` les contrats publics multi-output de Logging, et `pre.005` active le runtime multi-sink sur niveau/target/formats. Le routing structuré `domain` est scindé en `pre.006` avant le gel du premier schema Logging.
Ce plan a été établi par `0.1.3-pre.001`, corrigé par `0.1.3-pre.001-fix.001/.002/.003`, puis exécuté par petites tranches. `pre.002` a livré le bootstrap Config, `pre.003` le registre `file_id`, `pre.004` les contrats publics multi-output de Logging, `pre.005` le runtime multi-sink sur niveau/target/formats et `pre.006` ferme le routing structuré `domain`. Le premier schema Logging peut donc commencer en `pre.007` après validation utilisateur de cette tranche.
La base auditée reste la release stable `v0.1.2`.
@@ -1677,23 +1677,31 @@ Implémentation candidate de `pre.005` :
- JSON + ANSI console est refusé comme combinaison incohérente ;
- aucune dépendance Config n'entre dans Logging.
La validation utilisateur de `pre.005` est requise avant l'ouverture de `pre.006`.
`pre.005` a d'abord révélé une assertion d'intégration JSON incorrecte : le message de test contenait lui-même une séquence ANSI que le formatter JSON sérialisait comme donnée échappée. `pre.005-fix.001` a séparé le test JSON du test de stripping ANSI sans modifier le runtime. L'utilisateur a ensuite validé `cargo fmt --all`, `cargo check --workspace`, `cargo clippy --workspace --all-targets`, `cargo test --workspace` et les trois vues `cargo tree -p ksp-logging-lib` sans warning ni échec.
### `0.1.3-pre.006` — `ksp-logging-lib` : routing structuré `domain`
Objectif unique : fermer proprement la dimension `OutputFilter.domains[]` avant que Config ne fige le schema Logging.
- extraire le champ structuré `domain` des events/spans ;
- définir la sémantique d'un event portant son propre `domain` ;
- définir l'héritage depuis le span courant lorsqu'un event ne porte pas de `domain` ;
- définir le comportement des spans sans domain et des spans imbriqués ;
- appliquer les selectors domain par output sans casser level/target ;
- conserver les événements lifecycle `SpanEvents` cohérents avec le domain du span ;
- préserver les champs formatés requis par les différents formatters ;
- conserver hot reload, takeover et non-blocking ;
- tests unitaires/intégration sur events, spans, héritage et plusieurs sinks.
Implémentation candidate livrée par `pre.006` :
Une solution qui réduirait `domain` à une convention de target est interdite : il s'agit d'une dimension structurée distincte.
- `domain` reste un champ structuré distinct du target et n'est jamais converti en pseudo-target ;
- une couche interne `DomainContextLayer` capture le `domain` effectif avant les formatters de sortie ;
- un event portant directement `domain` remplace le domain hérité pour cet event ;
- un event sans `domain` hérite du domain effectif de son span ;
- un span portant `domain` définit son domain effectif ;
- un span sans `domain` hérite du domain effectif de son parent au moment de sa création ;
- les lifecycle events `NEW/ENTER/EXIT/CLOSE` utilisent le domain effectif du span concerné ;
- `domains = ["*"]` reste l'absence de restriction et accepte aussi les events/spans sans domain ;
- un selector domain nommé correspond par préfixe et ne sélectionne pas une entrée sans domain ;
- le writer applique conjointement `level`, `target` et le domain effectif ;
- la politique globale de takeover KSP, le subscriber unique, le hot reload transactionnel et les writers non bloquants restent inchangés ;
- le contexte domain est thread-local interne au dispatch Logging et n'entre pas dans les contrats publics ;
- les tests couvrent domain direct, absence de domain, héritage parent/enfant, override d'event, override de span enfant, lifecycle et trois sinks domain distincts.
Aucune dépendance externe ou feature Cargo supplémentaire n'est nécessaire pour cette tranche. `pre.007` peut donc construire `std.logging.schema.json` sur une surface Logging qui représente et exécute réellement les trois dimensions de routing retenues : level, target et domain.
La validation utilisateur de `pre.006` reste requise avant l'ouverture de `pre.007`.
### `0.1.3-pre.007` — JSON/JSON Schema + premier document Logging