v0.1.3-pre.006
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 46
|
# version: 47
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = ["crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib"]
|
members = ["crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.3-pre.5.fix.1"
|
version = "0.1.3-pre.6"
|
||||||
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"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: crates/ksp-logging-lib/README.md -->
|
<!-- file: crates/ksp-logging-lib/README.md -->
|
||||||
<!-- version: 4 -->
|
<!-- version: 5 -->
|
||||||
|
|
||||||
# ksp-logging-lib
|
# ksp-logging-lib
|
||||||
|
|
||||||
@@ -14,8 +14,8 @@ La crate possède :
|
|||||||
- `LoggingSettings`, la console explicite et les settings fichier indépendants de Config ;
|
- `LoggingSettings`, la console explicite et les settings fichier indépendants de Config ;
|
||||||
- les formats runtime `Human/Compact/Pretty/Json` ;
|
- les formats runtime `Human/Compact/Pretty/Json` ;
|
||||||
- zéro, un ou plusieurs outputs fichier actifs simultanément, identifiés par `output_id` unique ;
|
- 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 routing par output sur niveau, target KSP et champ structuré `domain` ;
|
||||||
- le contrat de filtre `domain`, dont l'activation runtime structurée est volontairement séparée dans la tranche suivante ;
|
- 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 ;
|
- l'installation unique du subscriber global ;
|
||||||
- le hot reload via `reinitialize` sans second subscriber global ;
|
- le hot reload via `reinitialize` sans second subscriber global ;
|
||||||
- le takeover des logs : les targets externes sont silencieux par défaut ;
|
- 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`
|
## Routing `domain`
|
||||||
|
|
||||||
`OutputFilter` représente déjà :
|
`OutputFilter` applique désormais les trois dimensions :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
level
|
level
|
||||||
@@ -36,9 +36,17 @@ targets[]
|
|||||||
domains[]
|
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
|
## Frontières
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
<!-- file: crates/ksp-logging-lib/TODO.md -->
|
<!-- file: crates/ksp-logging-lib/TODO.md -->
|
||||||
<!-- version: 4 -->
|
<!-- version: 5 -->
|
||||||
|
|
||||||
# TODO ksp-logging-lib
|
# TODO ksp-logging-lib
|
||||||
|
|
||||||
## À fermer pendant `0.1.3`
|
## À 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` ;
|
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`.
|
||||||
- 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`.
|
|
||||||
|
|
||||||
## Capacités désormais actives
|
## Capacités désormais actives
|
||||||
|
|
||||||
@@ -22,12 +19,13 @@ Le runtime supporte maintenant :
|
|||||||
|
|
||||||
- zéro, un ou plusieurs outputs fichier simultanés ;
|
- zéro, un ou plusieurs outputs fichier simultanés ;
|
||||||
- une console indépendante ;
|
- 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` ;
|
- `Human`, `Compact`, `Pretty` et `Json` ;
|
||||||
- ANSI console configurable et fichiers persistants sans ANSI ;
|
- ANSI console configurable et fichiers persistants sans ANSI ;
|
||||||
- guards non bloquants indépendants ;
|
- guards non bloquants indépendants ;
|
||||||
- compteurs agrégés et compteurs cumulatifs par `output_id` fichier ;
|
- 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
|
## Capacités différées
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: crates/ksp-logging-lib/USAGE.md -->
|
<!-- file: crates/ksp-logging-lib/USAGE.md -->
|
||||||
<!-- version: 4 -->
|
<!-- version: 5 -->
|
||||||
|
|
||||||
# Utilisation de ksp-logging-lib
|
# 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`.
|
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 ;
|
- plusieurs fichiers simultanés ;
|
||||||
- les formats `Human`, `Compact`, `Pretty` et `Json` ;
|
- les formats `Human`, `Compact`, `Pretty` et `Json` ;
|
||||||
- l'ANSI console ;
|
- 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 comptage cumulatif des lignes abandonnées par `output_id` fichier ;
|
||||||
- le hot reload de ces sorties sans réinstaller le subscriber global.
|
- 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.
|
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
|
## 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
|
```rust
|
||||||
let console = ksp_logging_lib::ConsoleSettings::new(
|
let console = ksp_logging_lib::ConsoleSettings::new(
|
||||||
@@ -131,7 +133,19 @@ let settings = ksp_logging_lib::LoggingSettings::new(
|
|||||||
let validation = settings.validate();
|
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
|
## Hot reload
|
||||||
|
|
||||||
|
|||||||
181
crates/ksp-logging-lib/src/domain.rs
Normal file
181
crates/ksp-logging-lib/src/domain.rs
Normal 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;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-logging-lib/src/lib.rs
|
// file: crates/ksp-logging-lib/src/lib.rs
|
||||||
// version: 6
|
// version: 7
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
@@ -7,10 +7,11 @@
|
|||||||
//! KSP-owned logging and tracing facade.
|
//! 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
|
//! 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
|
//! `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 file outputs, per-output level/target routing, selectable formats, console ANSI and per-file dropped-line accounting. Structured
|
//! multiple simultaneous outputs with per-output level/target/domain 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.
|
//! `domain` routing remains distinct from targets and follows explicit event domains or inherited span domains.
|
||||||
|
|
||||||
|
mod domain;
|
||||||
mod error;
|
mod error;
|
||||||
mod macros;
|
mod macros;
|
||||||
mod runtime;
|
mod runtime;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-logging-lib/src/runtime.rs
|
// file: crates/ksp-logging-lib/src/runtime.rs
|
||||||
// version: 10
|
// version: 11
|
||||||
|
|
||||||
use tracing_subscriber::Layer; // rust-rules: trait-import
|
use tracing_subscriber::Layer; // rust-rules: trait-import
|
||||||
use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import
|
use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import
|
||||||
@@ -219,10 +219,6 @@ fn prepare_runtime(settings: &crate::LoggingSettings) -> ksp_core_lib::Result<Pr
|
|||||||
if let std::option::Option::Some(error) = validation_error {
|
if let std::option::Option::Some(error) = validation_error {
|
||||||
return std::result::Result::Err(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 {
|
let enabled_console = settings.console().filter(|console| -> bool {
|
||||||
return console.enabled();
|
return console.enabled();
|
||||||
});
|
});
|
||||||
@@ -248,38 +244,11 @@ fn prepare_runtime(settings: &crate::LoggingSettings) -> ksp_core_lib::Result<Pr
|
|||||||
if output_layers.is_empty() {
|
if output_layers.is_empty() {
|
||||||
return std::result::Result::Ok(PreparedRuntime { layers: RuntimeLayers::new(), outputs });
|
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();
|
let takeover_layer = build_target_filter(settings).and_then(output_layers).boxed();
|
||||||
return std::result::Result::Ok(PreparedRuntime { layers: vec![takeover_layer], outputs });
|
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 {
|
fn build_console_output(console: &crate::ConsoleSettings, settings: &crate::LoggingSettings) -> PreparedOutput {
|
||||||
return match console.output() {
|
return match console.output() {
|
||||||
crate::ConsoleOutput::Stdout => build_non_blocking_output(
|
crate::ConsoleOutput::Stdout => build_non_blocking_output(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-logging-lib/src/writer.rs
|
// file: crates/ksp-logging-lib/src/writer.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
enum StripAnsiState {
|
enum StripAnsiState {
|
||||||
@@ -158,9 +158,12 @@ fn metadata_matches_filter(metadata: &tracing::Metadata<'_>, filter: &crate::Out
|
|||||||
if !level_is_enabled(metadata.level(), filter.level()) {
|
if !level_is_enabled(metadata.level(), filter.level()) {
|
||||||
return false;
|
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 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 {
|
fn level_is_enabled(level: &tracing::Level, filter: crate::LogFilterLevel) -> bool {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-logging-lib/tests/runtime.rs
|
// 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.
|
//! 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!(logging_trace_enabled());
|
||||||
assert!(!external_error_enabled());
|
assert!(!external_error_enabled());
|
||||||
exercise_concurrent_reload(&mut guard, &disabled);
|
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 human_directory = root.join("human");
|
||||||
let compact_directory = root.join("compact");
|
let compact_directory = root.join("compact");
|
||||||
let pretty_directory = root.join("pretty");
|
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.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.json.logging"), std::option::Option::Some(0));
|
||||||
assert_eq!(guard.dropped_file_lines("file.unknown"), std::option::Option::None);
|
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();
|
let dropped = guard.dropped_lines();
|
||||||
assert_eq!(dropped.total(), dropped.console().saturating_add(dropped.file()));
|
assert_eq!(dropped.total(), dropped.console().saturating_add(dropped.file()));
|
||||||
let second_initialize = ksp_logging_lib::initialize(&disabled);
|
let second_initialize = ksp_logging_lib::initialize(&disabled);
|
||||||
|
|||||||
20
crates/ksp-logging-lib/unit_tests/domain.rs
Normal file
20
crates/ksp-logging-lib/unit_tests/domain.rs
Normal 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()]));
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-logging-lib/unit_tests/runtime.rs
|
// file: crates/ksp-logging-lib/unit_tests/runtime.rs
|
||||||
// version: 8
|
// version: 9
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn level_mapping_covers_all_ksp_levels() {
|
fn level_mapping_covers_all_ksp_levels() {
|
||||||
@@ -154,7 +154,7 @@ fn multi_sink_runtime_accepts_metadata_routing_formats_and_console_ansi() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn domain_routing_is_explicitly_deferred_instead_of_ignored() {
|
fn domain_routing_is_accepted_by_runtime_preparation() {
|
||||||
let console = crate::ConsoleSettings::new(
|
let console = crate::ConsoleSettings::new(
|
||||||
true,
|
true,
|
||||||
crate::ConsoleOutput::Stdout,
|
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());
|
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());
|
assert!(settings.validate().is_ok());
|
||||||
let result = super::prepare_runtime(&settings);
|
let result = super::prepare_runtime(&settings);
|
||||||
assert!(result.is_err());
|
assert!(result.is_ok());
|
||||||
let error = match result {
|
|
||||||
std::result::Result::Ok(_) => return,
|
|
||||||
std::result::Result::Err(error) => error,
|
|
||||||
};
|
|
||||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_SETTINGS);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct BlockingWriter {
|
struct BlockingWriter {
|
||||||
|
|||||||
226
deltas/0.1.3/pre.006.md
Normal file
226
deltas/0.1.3/pre.006.md
Normal 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.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/000-README.md -->
|
<!-- file: docs/plans/000-README.md -->
|
||||||
<!-- version: 12 -->
|
<!-- version: 13 -->
|
||||||
|
|
||||||
# Plans KSP
|
# 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 ;
|
- [`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`.
|
- [`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.
|
Le `pre.001` de chaque release fonctionnelle peut introduire son propre plan détaillé lorsque la release s'ouvre.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md -->
|
<!-- file: docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md -->
|
||||||
<!-- version: 10 -->
|
<!-- version: 11 -->
|
||||||
|
|
||||||
# Séquence des releases fonctionnelles KSP
|
# 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 15–20 minutes de travail effectif.
|
Cette prévision n'est pas un plafond : chaque prerelease doit rester une petite tranche, avec scission explicite si l'objectif dépasse environ 15–20 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
|
## `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.
|
La décision `0.1.3-pre.001` conserve cette release comme étape suivante par défaut.
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<!-- file: docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md -->
|
<!-- file: docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md -->
|
||||||
<!-- version: 8 -->
|
<!-- version: 9 -->
|
||||||
|
|
||||||
# Plan `0.1.3` — Configuration foundation
|
# Plan `0.1.3` — Configuration foundation
|
||||||
|
|
||||||
## 1. Statut et objectif
|
## 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`.
|
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 ;
|
- JSON + ANSI console est refusé comme combinaison incohérente ;
|
||||||
- aucune dépendance Config n'entre dans Logging.
|
- 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`
|
### `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.
|
Objectif unique : fermer proprement la dimension `OutputFilter.domains[]` avant que Config ne fige le schema Logging.
|
||||||
|
|
||||||
- extraire le champ structuré `domain` des events/spans ;
|
Implémentation candidate livrée par `pre.006` :
|
||||||
- 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.
|
|
||||||
|
|
||||||
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
|
### `0.1.3-pre.007` — JSON/JSON Schema + premier document Logging
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user