v0.1.3-pre.005
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 44
|
||||
# version: 45
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.3-pre.4"
|
||||
version = "0.1.3-pre.5"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
@@ -16,7 +16,7 @@ publish = false
|
||||
[workspace.dependencies]
|
||||
solana-pubkey = { version = "^4.3", default-features = false }
|
||||
tracing = { version = "^0.1", default-features = false, features = ["std"] }
|
||||
tracing-subscriber = { version = "^0.3", default-features = false, features = ["fmt"] }
|
||||
tracing-subscriber = { version = "^0.3", default-features = false, features = ["fmt", "json", "ansi"] }
|
||||
tracing-appender = { version = "^0.2", default-features = false }
|
||||
tokio = { version = "^1.53", default-features = false, features = ["rt", "rt-multi-thread", "macros"] }
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-logging-lib/README.md -->
|
||||
<!-- version: 3 -->
|
||||
<!-- version: 4 -->
|
||||
|
||||
# ksp-logging-lib
|
||||
|
||||
@@ -12,17 +12,34 @@ La crate possède :
|
||||
- les cinq niveaux KSP `error`, `warn`, `info`, `debug` et `trace` ;
|
||||
- les macros d'événements et de spans qui préservent le callsite du consommateur ;
|
||||
- `LoggingSettings`, la console explicite et les settings fichier indépendants de Config ;
|
||||
- les formats publics `Human/Compact/Pretty/Json` et les filtres par output (`level`, targets KSP et domains) ;
|
||||
- zéro, un ou plusieurs descriptors fichier identifiés par `output_id` unique ;
|
||||
- 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 ;
|
||||
- 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 ;
|
||||
- le runtime non bloquant console/fichier existant, avec activation multi-sink/routing enrichie finalisée dans la tranche runtime dédiée ;
|
||||
- les `WorkerGuard`, compteurs de lignes abandonnées, rotation fichier et stripping ANSI ;
|
||||
- les writers non bloquants console/fichier et leurs `WorkerGuard` ;
|
||||
- les compteurs agrégés de lignes abandonnées et le compteur cumulatif par `output_id` fichier ;
|
||||
- la rotation fichier, le stripping ANSI persistant et l'ANSI configurable pour la console ;
|
||||
- l'instrumentation de scopes synchrones et de `Future` async.
|
||||
|
||||
L'API async de production reste indépendante de tout executor. Tokio est utilisé uniquement comme `dev-dependency` afin de valider `instrument(...)` sur un executor réel en mode current-thread et multi-thread ; il ne fait pas partie des dépendances runtime de la crate.
|
||||
|
||||
## Routing `domain`
|
||||
|
||||
`OutputFilter` représente déjà :
|
||||
|
||||
```text
|
||||
level
|
||||
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`.
|
||||
|
||||
Ce refus évite d'assimiler artificiellement `domain` au target ou d'ignorer silencieusement la dimension demandée.
|
||||
|
||||
## Frontières
|
||||
|
||||
Une crate KSP comportementale qui journalise son activité dépend de `ksp-logging-lib` et n'utilise pas directement `tracing`, `tracing-subscriber` ou `tracing-appender`.
|
||||
|
||||
@@ -1,20 +1,33 @@
|
||||
<!-- file: crates/ksp-logging-lib/TODO.md -->
|
||||
<!-- version: 3 -->
|
||||
<!-- version: 4 -->
|
||||
|
||||
# TODO ksp-logging-lib
|
||||
|
||||
## À fermer pendant `0.1.3`
|
||||
|
||||
`0.1.3-pre.004` expose désormais le modèle public multi-output. La tranche runtime suivante doit encore :
|
||||
`0.1.3-pre.005` active le runtime multi-sink pour les dimensions disponibles directement dans les métadonnées `tracing` : niveau et target.
|
||||
|
||||
- activer simultanément zéro, un ou plusieurs fichiers ;
|
||||
- appliquer réellement `OutputFilter` par sink sur niveau, target et domain ;
|
||||
- appliquer `LogFormat::{Human, Compact, Pretty, Json}` par sink ;
|
||||
- appliquer l'ANSI console configurable tout en gardant les fichiers sans séquences ANSI ;
|
||||
- généraliser les writers/guards et compteurs de lignes abandonnées par output sans perdre la vue agrégée utile ;
|
||||
- conserver le subscriber global unique, le takeover KSP et le hot reload transactionnel.
|
||||
La tranche Logging suivante doit encore :
|
||||
|
||||
Tant que cette tranche runtime n'est pas livrée, `initialize/reinitialize` refusent explicitement les capacités du nouveau contrat qu'ils ne savent pas encore appliquer.
|
||||
- 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`.
|
||||
|
||||
## Capacités désormais actives
|
||||
|
||||
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 ;
|
||||
- `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.
|
||||
|
||||
## Capacités différées
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-logging-lib/USAGE.md -->
|
||||
<!-- version: 3 -->
|
||||
<!-- version: 4 -->
|
||||
|
||||
# Utilisation de ksp-logging-lib
|
||||
|
||||
@@ -46,15 +46,22 @@ Une sortie fichier possède un `output_id` stable et unique dans `LoggingSetting
|
||||
|
||||
Les fichiers persistants interdisent `ansi = true`.
|
||||
|
||||
## Compatibilité runtime de `pre.004`
|
||||
## Runtime multi-output de `pre.005`
|
||||
|
||||
`0.1.3-pre.004` étend d'abord le **contrat public**. Le runtime multi-sink/routing est livré séparément en `pre.005`.
|
||||
`0.1.3-pre.005` active réellement :
|
||||
|
||||
Jusqu'à cette tranche suivante, `initialize`/`reinitialize` refusent explicitement toute capacité nouvellement représentée qu'ils ne savent pas encore appliquer : plusieurs fichiers actifs, format non `Human`, ANSI console ou filtre propre à un output. Ces valeurs ne sont donc jamais acceptées puis ignorées silencieusement.
|
||||
- plusieurs fichiers simultanés ;
|
||||
- les formats `Human`, `Compact`, `Pretty` et `Json` ;
|
||||
- l'ANSI console ;
|
||||
- le routing par niveau et target 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.
|
||||
|
||||
Les helpers `ConsoleSettings::stdout()` et `ConsoleSettings::stderr()` construisent une console compatible avec le runtime actuel : activée, `Human`, sans ANSI et sans filtre supplémentaire par rapport au takeover 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.
|
||||
|
||||
## Initialisation compatible avec le runtime actuel
|
||||
Les helpers `ConsoleSettings::stdout()` et `ConsoleSettings::stderr()` restent des raccourcis `Human`, sans ANSI et sans restriction supplémentaire par output.
|
||||
|
||||
## Initialisation
|
||||
|
||||
```rust
|
||||
let file = ksp_logging_lib::FileSettings::new(
|
||||
@@ -83,9 +90,9 @@ let mut logging_guard = match initialize_result {
|
||||
|
||||
Une configuration sans output actif est valide et installe une infrastructure initialement silencieuse qui pourra être activée plus tard par hot reload.
|
||||
|
||||
## Construction d'un contrat multi-output
|
||||
## Construction d'un runtime multi-output
|
||||
|
||||
Le contrat public peut déjà représenter la future configuration Config complète :
|
||||
Le runtime peut maintenant activer plusieurs sorties ayant des formats et filtres metadata distincts :
|
||||
|
||||
```rust
|
||||
let console = ksp_logging_lib::ConsoleSettings::new(
|
||||
@@ -110,7 +117,7 @@ let error_file = ksp_logging_lib::FileSettings::new(
|
||||
ksp_logging_lib::OutputFilter::new(
|
||||
ksp_logging_lib::LogFilterLevel::Error,
|
||||
std::vec!["ksp-config-lib".to_string()],
|
||||
std::vec!["config".to_string()],
|
||||
std::vec!["*".to_string()],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -124,11 +131,11 @@ let settings = ksp_logging_lib::LoggingSettings::new(
|
||||
let validation = settings.validate();
|
||||
```
|
||||
|
||||
`validate()` vérifie le contrat structurel. L'activation runtime de ce routage enrichi appartient à `pre.005`.
|
||||
`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é.
|
||||
|
||||
## Hot reload
|
||||
|
||||
Une configuration compatible avec les capacités runtime actives peut être appliquée sans redémarrer le processus ou le worker :
|
||||
Une configuration peut être appliquée sans redémarrer le processus ou le worker :
|
||||
|
||||
```rust
|
||||
let debug_settings = ksp_logging_lib::LoggingSettings::new(
|
||||
@@ -184,7 +191,7 @@ La future instrumentée entre/sort du span pendant ses polls et lors de son `Dro
|
||||
|
||||
## Lignes abandonnées
|
||||
|
||||
Les sorties utilisent des queues lossy afin de ne pas appliquer de backpressure au hot path. Les compteurs restent encore agrégés console/fichier pendant `pre.004` ; leur généralisation par output appartient au runtime multi-sink :
|
||||
Les sorties utilisent des queues lossy afin de ne pas appliquer de backpressure au hot path. La vue agrégée console/fichier reste disponible :
|
||||
|
||||
```rust
|
||||
let dropped = logging_guard.dropped_lines();
|
||||
@@ -197,6 +204,14 @@ ksp_logging_lib::warn!(
|
||||
);
|
||||
```
|
||||
|
||||
Pour un fichier précis :
|
||||
|
||||
```rust
|
||||
let dropped_for_output = logging_guard.dropped_file_lines("file.worker");
|
||||
```
|
||||
|
||||
Le compteur par `output_id` reste cumulatif à travers les hot reloads tant que le même `LoggingGuard` est conservé.
|
||||
|
||||
## Instrumentation async et executor
|
||||
|
||||
`instrument(span, future)` accepte une `Future` standard et ne dépend d'aucun executor particulier. Tokio n'est présent qu'en `dev-dependency` pour valider la surface sur un executor réel, y compris après plusieurs suspensions et sur un runtime multi-thread.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-logging-lib/src/lib.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
@@ -7,9 +7,9 @@
|
||||
//! KSP-owned logging and tracing facade.
|
||||
//!
|
||||
//! This crate owns the KSP runtime logging contract. Behavioral KSP crates emit events and spans through this facade rather than depending directly on the
|
||||
//! `tracing` stack. The crate owns the single global subscriber, KSP takeover filtering, hot reload and non-blocking outputs. `0.1.3-pre.004` extends
|
||||
//! the public settings contract with explicit console properties, multiple file descriptors, per-output level/target/domain filters and selectable formats;
|
||||
//! runtime activation of the newly represented multi-output routing is completed separately so unsupported capabilities are never ignored silently.
|
||||
//! `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.
|
||||
|
||||
mod error;
|
||||
mod macros;
|
||||
@@ -38,9 +38,9 @@ pub use self::runtime::reinitialize;
|
||||
pub use self::settings::ConsoleOutput;
|
||||
/// Runtime settings for the optional console output.
|
||||
pub use self::settings::ConsoleSettings;
|
||||
/// Rotation cadence for the optional file output.
|
||||
/// Rotation cadence for one file output.
|
||||
pub use self::settings::FileRotation;
|
||||
/// Runtime settings for the optional file output.
|
||||
/// Runtime settings for one file output.
|
||||
pub use self::settings::FileSettings;
|
||||
/// Runtime filter level used by KSP logging settings.
|
||||
pub use self::settings::LogFilterLevel;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-logging-lib/src/runtime.rs
|
||||
// version: 9
|
||||
// version: 10
|
||||
|
||||
use tracing_subscriber::Layer; // rust-rules: trait-import
|
||||
use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import
|
||||
@@ -47,6 +47,7 @@ pub struct LoggingGuard {
|
||||
settings: crate::LoggingSettings,
|
||||
outputs: RuntimeOutputs,
|
||||
retired_dropped_lines: crate::DroppedLines,
|
||||
retired_file_dropped_lines: std::collections::HashMap<std::string::String, usize>,
|
||||
}
|
||||
|
||||
impl LoggingGuard {
|
||||
@@ -61,6 +62,19 @@ impl LoggingGuard {
|
||||
pub fn dropped_lines(&self) -> crate::DroppedLines {
|
||||
return self.retired_dropped_lines.saturating_add(self.outputs.dropped_lines());
|
||||
}
|
||||
|
||||
/// Returns cumulative dropped-line counters for one file `output_id` when that output has existed in the runtime.
|
||||
#[must_use]
|
||||
pub fn dropped_file_lines(&self, output_id: &str) -> std::option::Option<usize> {
|
||||
let retired = self.retired_file_dropped_lines.get(output_id).copied();
|
||||
let active = self.outputs.file_dropped_lines(output_id);
|
||||
return match (retired, active) {
|
||||
(std::option::Option::Some(retired), std::option::Option::Some(active)) => std::option::Option::Some(retired.saturating_add(active)),
|
||||
(std::option::Option::Some(retired), std::option::Option::None) => std::option::Option::Some(retired),
|
||||
(std::option::Option::None, std::option::Option::Some(active)) => std::option::Option::Some(active),
|
||||
(std::option::Option::None, std::option::Option::None) => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
type BoxedRuntimeLayer = std::boxed::Box<dyn tracing_subscriber::Layer<tracing_subscriber::Registry> + std::marker::Send + std::marker::Sync + 'static>;
|
||||
@@ -75,7 +89,7 @@ struct PreparedRuntime {
|
||||
#[derive(Default)]
|
||||
struct RuntimeOutputs {
|
||||
console: std::option::Option<RuntimeOutput>,
|
||||
file: std::option::Option<RuntimeOutput>,
|
||||
files: std::vec::Vec<RuntimeFileOutput>,
|
||||
}
|
||||
|
||||
impl RuntimeOutputs {
|
||||
@@ -84,12 +98,44 @@ impl RuntimeOutputs {
|
||||
std::option::Option::Some(output) => output.dropped_lines(),
|
||||
std::option::Option::None => 0,
|
||||
};
|
||||
let file = match self.file.as_ref() {
|
||||
std::option::Option::Some(output) => output.dropped_lines(),
|
||||
std::option::Option::None => 0,
|
||||
};
|
||||
let mut file = 0_usize;
|
||||
for output in &self.files {
|
||||
file = file.saturating_add(output.output.dropped_lines());
|
||||
}
|
||||
return crate::DroppedLines { console, file };
|
||||
}
|
||||
|
||||
fn file_dropped_lines(&self, output_id: &str) -> std::option::Option<usize> {
|
||||
return self
|
||||
.files
|
||||
.iter()
|
||||
.find(|output| -> bool {
|
||||
return output.output_id == output_id;
|
||||
})
|
||||
.map(|output| -> usize {
|
||||
return output.output.dropped_lines();
|
||||
});
|
||||
}
|
||||
|
||||
fn accumulate_file_dropped_lines(&self, destination: &mut std::collections::HashMap<std::string::String, usize>) {
|
||||
for output in &self.files {
|
||||
let dropped = output.output.dropped_lines();
|
||||
match destination.entry(output.output_id.clone()) {
|
||||
std::collections::hash_map::Entry::Occupied(mut entry) => {
|
||||
let cumulative = entry.get().saturating_add(dropped);
|
||||
*entry.get_mut() = cumulative;
|
||||
},
|
||||
std::collections::hash_map::Entry::Vacant(entry) => {
|
||||
entry.insert(dropped);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RuntimeFileOutput {
|
||||
output_id: std::string::String,
|
||||
output: RuntimeOutput,
|
||||
}
|
||||
|
||||
struct RuntimeOutput {
|
||||
@@ -108,6 +154,12 @@ struct PreparedOutput {
|
||||
output: RuntimeOutput,
|
||||
}
|
||||
|
||||
struct PreparedFileOutput {
|
||||
output_id: std::string::String,
|
||||
layer: BoxedRuntimeLayer,
|
||||
output: RuntimeOutput,
|
||||
}
|
||||
|
||||
/// Installs the global KSP tracing subscriber.
|
||||
///
|
||||
/// This function may succeed only once for the lifetime of the process. The returned guard owns all non-blocking writer guards and is then used by
|
||||
@@ -124,6 +176,7 @@ pub fn initialize(settings: &crate::LoggingSettings) -> ksp_core_lib::Result<cra
|
||||
settings: settings.clone(),
|
||||
outputs,
|
||||
retired_dropped_lines: crate::DroppedLines::zero(),
|
||||
retired_file_dropped_lines: std::collections::HashMap::new(),
|
||||
}),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_ALREADY_INITIALIZED, "the global KSP tracing subscriber is already installed").with_source(error),
|
||||
@@ -147,6 +200,7 @@ pub fn reinitialize(guard: &mut crate::LoggingGuard, settings: &crate::LoggingSe
|
||||
return match reload_result {
|
||||
std::result::Result::Ok(()) => {
|
||||
guard.retired_dropped_lines = guard.retired_dropped_lines.saturating_add(guard.outputs.dropped_lines());
|
||||
guard.outputs.accumulate_file_dropped_lines(&mut guard.retired_file_dropped_lines);
|
||||
let retired_outputs = std::mem::replace(&mut guard.outputs, outputs);
|
||||
guard.settings = settings.clone();
|
||||
drop(retired_layers);
|
||||
@@ -172,76 +226,84 @@ fn prepare_runtime(settings: &crate::LoggingSettings) -> ksp_core_lib::Result<Pr
|
||||
let enabled_console = settings.console().filter(|console| -> bool {
|
||||
return console.enabled();
|
||||
});
|
||||
let enabled_file = settings.files().iter().find(|file| -> bool {
|
||||
let enabled_files = settings.files().iter().filter(|file| -> bool {
|
||||
return file.enabled();
|
||||
});
|
||||
if enabled_console.is_none() && enabled_file.is_none() {
|
||||
return std::result::Result::Ok(PreparedRuntime { layers: RuntimeLayers::new(), outputs: RuntimeOutputs::default() });
|
||||
}
|
||||
let prepared_file = match enabled_file {
|
||||
std::option::Option::Some(file) => match build_file_output(file, settings) {
|
||||
std::result::Result::Ok(output) => std::option::Option::Some(output),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
},
|
||||
std::option::Option::None => std::option::Option::None,
|
||||
};
|
||||
let prepared_console = enabled_console.map(|console| -> PreparedOutput {
|
||||
return build_console_output(console, settings);
|
||||
});
|
||||
let mut output_layers = RuntimeLayers::new();
|
||||
let mut outputs = RuntimeOutputs::default();
|
||||
if let std::option::Option::Some(console) = prepared_console {
|
||||
output_layers.push(console.layer);
|
||||
outputs.console = std::option::Option::Some(console.output);
|
||||
if let std::option::Option::Some(console) = enabled_console {
|
||||
let prepared_console = build_console_output(console, settings);
|
||||
output_layers.push(prepared_console.layer);
|
||||
outputs.console = std::option::Option::Some(prepared_console.output);
|
||||
}
|
||||
if let std::option::Option::Some(file) = prepared_file {
|
||||
output_layers.push(file.layer);
|
||||
outputs.file = std::option::Option::Some(file.output);
|
||||
for file in enabled_files {
|
||||
let prepared_file_result = build_file_output(file, settings);
|
||||
let prepared_file = match prepared_file_result {
|
||||
std::result::Result::Ok(output) => output,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
output_layers.push(prepared_file.layer);
|
||||
outputs.files.push(RuntimeFileOutput { output_id: prepared_file.output_id, output: prepared_file.output });
|
||||
}
|
||||
if output_layers.is_empty() {
|
||||
return std::result::Result::Ok(PreparedRuntime { layers: RuntimeLayers::new(), outputs });
|
||||
}
|
||||
let takeover_layer = build_target_filter(settings).and_then(output_layers).boxed();
|
||||
let layers = vec![takeover_layer];
|
||||
return std::result::Result::Ok(PreparedRuntime { layers, outputs });
|
||||
return std::result::Result::Ok(PreparedRuntime { layers: vec![takeover_layer], outputs });
|
||||
}
|
||||
|
||||
fn validate_current_runtime_capabilities(settings: &crate::LoggingSettings) -> ksp_core_lib::Result<()> {
|
||||
let enabled_file_count = settings
|
||||
.files()
|
||||
.iter()
|
||||
.filter(|file| -> bool {
|
||||
return file.enabled();
|
||||
})
|
||||
.count();
|
||||
if enabled_file_count > 1 {
|
||||
return runtime_capability_error("multiple enabled file outputs require the multi-sink runtime");
|
||||
}
|
||||
if let std::option::Option::Some(console) = settings.console()
|
||||
&& console.enabled()
|
||||
&& (console.ansi() || console.format() != crate::LogFormat::Human || !console.filter().is_unrestricted())
|
||||
&& !domains_are_unrestricted(console.filter())
|
||||
{
|
||||
return runtime_capability_error("console ANSI, selectable formats and per-output routing require the multi-sink runtime");
|
||||
return runtime_capability_error("domain routing requires the dedicated structured-domain runtime tranche");
|
||||
}
|
||||
for file in settings.files() {
|
||||
if file.enabled() && (file.format() != crate::LogFormat::Human || !file.filter().is_unrestricted()) {
|
||||
return runtime_capability_error("file formats and per-output routing require the multi-sink runtime");
|
||||
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", "single-output-compatibility"),
|
||||
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(std::io::stdout(), "ksp-logging-console", settings, true),
|
||||
crate::ConsoleOutput::Stderr => build_non_blocking_output(std::io::stderr(), "ksp-logging-console", settings, true),
|
||||
crate::ConsoleOutput::Stdout => build_non_blocking_output(
|
||||
std::io::stdout(),
|
||||
"ksp-logging-console",
|
||||
settings.span_events(),
|
||||
true,
|
||||
console.ansi(),
|
||||
console.format(),
|
||||
console.filter(),
|
||||
),
|
||||
crate::ConsoleOutput::Stderr => build_non_blocking_output(
|
||||
std::io::stderr(),
|
||||
"ksp-logging-console",
|
||||
settings.span_events(),
|
||||
true,
|
||||
console.ansi(),
|
||||
console.format(),
|
||||
console.filter(),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn build_file_output(file: &crate::FileSettings, settings: &crate::LoggingSettings) -> ksp_core_lib::Result<PreparedOutput> {
|
||||
fn build_file_output(file: &crate::FileSettings, settings: &crate::LoggingSettings) -> ksp_core_lib::Result<PreparedFileOutput> {
|
||||
let appender_result = tracing_appender::rolling::RollingFileAppender::builder()
|
||||
.rotation(map_file_rotation(file.rotation()))
|
||||
.filename_prefix(file.file_name_prefix())
|
||||
@@ -251,6 +313,7 @@ fn build_file_output(file: &crate::FileSettings, settings: &crate::LoggingSettin
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_FILE_OUTPUT_INITIALIZATION_FAILED, "unable to initialize the KSP rolling file appender")
|
||||
.with_context("output_id", file.output_id())
|
||||
.with_context("directory", file.directory().display().to_string())
|
||||
.with_context("file_name_prefix", file.file_name_prefix())
|
||||
.with_source(error),
|
||||
@@ -258,16 +321,26 @@ fn build_file_output(file: &crate::FileSettings, settings: &crate::LoggingSettin
|
||||
},
|
||||
};
|
||||
let stripped_writer = crate::writer::StripAnsiWriter::new(appender);
|
||||
return std::result::Result::Ok(build_non_blocking_output(stripped_writer, "ksp-logging-file", settings, false));
|
||||
let thread_name = format!("ksp-logging-{}", file.output_id());
|
||||
let prepared = build_non_blocking_output(stripped_writer, thread_name.as_str(), settings.span_events(), false, false, file.format(), file.filter());
|
||||
return std::result::Result::Ok(PreparedFileOutput { output_id: file.output_id().to_string(), layer: prepared.layer, output: prepared.output });
|
||||
}
|
||||
|
||||
fn build_non_blocking_output<W>(writer: W, thread_name: &str, settings: &crate::LoggingSettings, ansi_sanitization: bool) -> PreparedOutput
|
||||
fn build_non_blocking_output<W>(
|
||||
writer: W,
|
||||
thread_name: &str,
|
||||
span_events: crate::SpanEvents,
|
||||
ansi_sanitization: bool,
|
||||
ansi: bool,
|
||||
format: crate::LogFormat,
|
||||
filter: &crate::OutputFilter,
|
||||
) -> PreparedOutput
|
||||
where
|
||||
W: std::io::Write + std::marker::Send + 'static,
|
||||
{
|
||||
let (non_blocking, worker_guard) = non_blocking_builder(thread_name).finish(writer);
|
||||
let error_counter = non_blocking.error_counter();
|
||||
let layer = build_format_layer(non_blocking, settings, ansi_sanitization);
|
||||
let layer = build_format_layer(non_blocking, span_events, ansi_sanitization, ansi, format, filter);
|
||||
return PreparedOutput { layer, output: RuntimeOutput { _worker_guard: worker_guard, error_counter } };
|
||||
}
|
||||
|
||||
@@ -275,16 +348,55 @@ fn non_blocking_builder(thread_name: &str) -> tracing_appender::non_blocking::No
|
||||
return tracing_appender::non_blocking::NonBlockingBuilder::default().lossy(true).thread_name(thread_name);
|
||||
}
|
||||
|
||||
fn build_format_layer(writer: tracing_appender::non_blocking::NonBlocking, settings: &crate::LoggingSettings, ansi_sanitization: bool) -> BoxedRuntimeLayer {
|
||||
return tracing_subscriber::fmt::layer()
|
||||
.with_writer(writer)
|
||||
.with_ansi(false)
|
||||
.with_ansi_sanitization(ansi_sanitization)
|
||||
.with_target(true)
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.with_span_events(map_span_events(settings.span_events()))
|
||||
.boxed();
|
||||
fn build_format_layer(
|
||||
writer: tracing_appender::non_blocking::NonBlocking,
|
||||
span_events: crate::SpanEvents,
|
||||
ansi_sanitization: bool,
|
||||
ansi: bool,
|
||||
format: crate::LogFormat,
|
||||
filter: &crate::OutputFilter,
|
||||
) -> BoxedRuntimeLayer {
|
||||
let span_events = map_span_events(span_events);
|
||||
return match format {
|
||||
crate::LogFormat::Human => tracing_subscriber::fmt::layer()
|
||||
.with_writer(crate::writer::RouteMakeWriter::new(writer, filter.clone()))
|
||||
.with_ansi(ansi)
|
||||
.with_ansi_sanitization(ansi_sanitization)
|
||||
.with_target(true)
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.with_span_events(span_events)
|
||||
.boxed(),
|
||||
crate::LogFormat::Compact => tracing_subscriber::fmt::layer()
|
||||
.compact()
|
||||
.with_writer(crate::writer::RouteMakeWriter::new(writer, filter.clone()))
|
||||
.with_ansi(ansi)
|
||||
.with_ansi_sanitization(ansi_sanitization)
|
||||
.with_target(true)
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.with_span_events(span_events)
|
||||
.boxed(),
|
||||
crate::LogFormat::Pretty => tracing_subscriber::fmt::layer()
|
||||
.pretty()
|
||||
.with_writer(crate::writer::RouteMakeWriter::new(writer, filter.clone()))
|
||||
.with_ansi(ansi)
|
||||
.with_ansi_sanitization(ansi_sanitization)
|
||||
.with_target(true)
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.with_span_events(span_events)
|
||||
.boxed(),
|
||||
crate::LogFormat::Json => tracing_subscriber::fmt::layer()
|
||||
.json()
|
||||
.with_writer(crate::writer::RouteMakeWriter::new(writer, filter.clone()))
|
||||
.with_ansi(false)
|
||||
.with_target(true)
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.with_span_events(span_events)
|
||||
.boxed(),
|
||||
};
|
||||
}
|
||||
|
||||
fn build_target_filter(settings: &crate::LoggingSettings) -> tracing_subscriber::filter::Targets {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-logging-lib/src/settings.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
/// Runtime filter level used by KSP logging settings.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
@@ -109,19 +109,6 @@ impl OutputFilter {
|
||||
pub fn domains(&self) -> &[std::string::String] {
|
||||
return self.domains.as_slice();
|
||||
}
|
||||
|
||||
/// Returns whether this filter leaves routing entirely to the global takeover policy.
|
||||
pub(crate) fn is_unrestricted(&self) -> bool {
|
||||
let targets_all = match self.targets.as_slice() {
|
||||
[selector] => selector == "*",
|
||||
_ => false,
|
||||
};
|
||||
let domains_all = match self.domains.as_slice() {
|
||||
[selector] => selector == "*",
|
||||
_ => false,
|
||||
};
|
||||
return self.level == crate::LogFilterLevel::Trace && targets_all && domains_all;
|
||||
}
|
||||
}
|
||||
|
||||
/// Console stream selected for human-readable logs.
|
||||
@@ -376,6 +363,12 @@ impl LoggingSettings {
|
||||
}
|
||||
}
|
||||
if let std::option::Option::Some(console) = self.console.as_ref() {
|
||||
if console.ansi() && console.format() == crate::LogFormat::Json {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "ANSI formatting is not compatible with JSON console output")
|
||||
.with_context("field", "console.ansi"),
|
||||
);
|
||||
}
|
||||
let validation_result = validate_output_filter(console.filter(), "console.filter");
|
||||
if let std::result::Result::Err(error) = validation_result {
|
||||
return std::result::Result::Err(error);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-logging-lib/src/writer.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum StripAnsiState {
|
||||
@@ -100,6 +100,96 @@ impl<W> StripAnsiWriter<W> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct RouteMakeWriter<W> {
|
||||
inner: W,
|
||||
filter: crate::OutputFilter,
|
||||
}
|
||||
|
||||
impl<W> RouteMakeWriter<W> {
|
||||
pub(crate) fn new(inner: W, filter: crate::OutputFilter) -> Self {
|
||||
return Self { inner, filter };
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum RoutedWriter<W> {
|
||||
Enabled(W),
|
||||
Disabled,
|
||||
}
|
||||
|
||||
impl<W> std::io::Write for RoutedWriter<W>
|
||||
where
|
||||
W: std::io::Write,
|
||||
{
|
||||
fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
|
||||
return match self {
|
||||
Self::Enabled(writer) => std::io::Write::write(writer, buffer),
|
||||
Self::Disabled => std::result::Result::Ok(buffer.len()),
|
||||
};
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
return match self {
|
||||
Self::Enabled(writer) => std::io::Write::flush(writer),
|
||||
Self::Disabled => std::result::Result::Ok(()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl<'writer, W> tracing_subscriber::fmt::MakeWriter<'writer> for RouteMakeWriter<W>
|
||||
where
|
||||
W: tracing_subscriber::fmt::MakeWriter<'writer>,
|
||||
{
|
||||
type Writer = RoutedWriter<W::Writer>;
|
||||
|
||||
fn make_writer(&'writer self) -> Self::Writer {
|
||||
return RoutedWriter::Enabled(tracing_subscriber::fmt::MakeWriter::make_writer(&self.inner));
|
||||
}
|
||||
|
||||
fn make_writer_for(&'writer self, metadata: &tracing::Metadata<'_>) -> Self::Writer {
|
||||
if metadata_matches_filter(metadata, &self.filter) {
|
||||
return RoutedWriter::Enabled(tracing_subscriber::fmt::MakeWriter::make_writer_for(&self.inner, metadata));
|
||||
}
|
||||
return RoutedWriter::Disabled;
|
||||
}
|
||||
}
|
||||
|
||||
fn metadata_matches_filter(metadata: &tracing::Metadata<'_>, filter: &crate::OutputFilter) -> bool {
|
||||
if !level_is_enabled(metadata.level(), filter.level()) {
|
||||
return false;
|
||||
}
|
||||
return filter.targets().iter().any(|selector| -> bool {
|
||||
return selector == "*" || metadata.target().starts_with(selector.as_str());
|
||||
});
|
||||
}
|
||||
|
||||
fn level_is_enabled(level: &tracing::Level, filter: crate::LogFilterLevel) -> bool {
|
||||
return match filter {
|
||||
crate::LogFilterLevel::Off => false,
|
||||
crate::LogFilterLevel::Error => level_rank(level) <= 1,
|
||||
crate::LogFilterLevel::Warn => level_rank(level) <= 2,
|
||||
crate::LogFilterLevel::Info => level_rank(level) <= 3,
|
||||
crate::LogFilterLevel::Debug => level_rank(level) <= 4,
|
||||
crate::LogFilterLevel::Trace => level_rank(level) <= 5,
|
||||
};
|
||||
}
|
||||
|
||||
fn level_rank(level: &tracing::Level) -> u8 {
|
||||
if level == &tracing::Level::ERROR {
|
||||
return 1;
|
||||
}
|
||||
if level == &tracing::Level::WARN {
|
||||
return 2;
|
||||
}
|
||||
if level == &tracing::Level::INFO {
|
||||
return 3;
|
||||
}
|
||||
if level == &tracing::Level::DEBUG {
|
||||
return 4;
|
||||
}
|
||||
return 5;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/writer.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-logging-lib/tests/public_api.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
//! Integration tests for the public crate-root surface of `ksp-logging-lib`.
|
||||
|
||||
@@ -85,6 +85,7 @@ fn public_runtime_surface_is_addressable_without_installing_it() {
|
||||
let _already_initialized = ksp_logging_lib::ERROR_CODE_ALREADY_INITIALIZED;
|
||||
let _reload_failed = ksp_logging_lib::ERROR_CODE_RELOAD_FAILED;
|
||||
let _file_initialization_failed = ksp_logging_lib::ERROR_CODE_FILE_OUTPUT_INITIALIZATION_FAILED;
|
||||
let _per_file_counter = ksp_logging_lib::LoggingGuard::dropped_file_lines;
|
||||
let dropped = ksp_logging_lib::DroppedLines::zero();
|
||||
assert_eq!(dropped.console(), 0);
|
||||
assert_eq!(dropped.file(), 0);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-logging-lib/tests/runtime.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
//! Integration tests for global initialization, takeover filtering, non-blocking outputs and hot reload.
|
||||
|
||||
@@ -172,33 +172,103 @@ 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 log_directory = root.join("logs");
|
||||
let file_enabled = ksp_logging_lib::LoggingSettings::new(
|
||||
let domain_routed = ksp_logging_lib::LoggingSettings::new(
|
||||
ksp_logging_lib::LogFilterLevel::Info,
|
||||
ksp_logging_lib::SpanEvents::Off,
|
||||
std::option::Option::None,
|
||||
std::vec![ksp_logging_lib::FileSettings::new(
|
||||
"file.runtime",
|
||||
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::new(
|
||||
true,
|
||||
log_directory.as_path(),
|
||||
"runtime-test.log",
|
||||
ksp_logging_lib::FileRotation::Never,
|
||||
ksp_logging_lib::ConsoleOutput::Stderr,
|
||||
false,
|
||||
ksp_logging_lib::LogFormat::Human,
|
||||
ksp_logging_lib::OutputFilter::unrestricted(),
|
||||
)],
|
||||
ksp_logging_lib::OutputFilter::new(ksp_logging_lib::LogFilterLevel::Info, std::vec!["*".to_string()], std::vec!["logging".to_string()]),
|
||||
)),
|
||||
std::vec::Vec::new(),
|
||||
);
|
||||
let file_reload = ksp_logging_lib::reinitialize(&mut guard, &file_enabled);
|
||||
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");
|
||||
let json_directory = root.join("json");
|
||||
let files_enabled = ksp_logging_lib::LoggingSettings::new(
|
||||
ksp_logging_lib::LogFilterLevel::Trace,
|
||||
ksp_logging_lib::SpanEvents::Off,
|
||||
std::option::Option::None,
|
||||
std::vec![
|
||||
ksp_logging_lib::FileSettings::new(
|
||||
"file.human.logging",
|
||||
true,
|
||||
human_directory.as_path(),
|
||||
"human.log",
|
||||
ksp_logging_lib::FileRotation::Never,
|
||||
ksp_logging_lib::LogFormat::Human,
|
||||
ksp_logging_lib::OutputFilter::new(ksp_logging_lib::LogFilterLevel::Info, std::vec![LOGGING_TARGET.to_string()], std::vec!["*".to_string()],),
|
||||
),
|
||||
ksp_logging_lib::FileSettings::new(
|
||||
"file.compact.store",
|
||||
true,
|
||||
compact_directory.as_path(),
|
||||
"compact.log",
|
||||
ksp_logging_lib::FileRotation::Never,
|
||||
ksp_logging_lib::LogFormat::Compact,
|
||||
ksp_logging_lib::OutputFilter::new(ksp_logging_lib::LogFilterLevel::Warn, std::vec![OTHER_KSP_TARGET.to_string()], std::vec!["*".to_string()],),
|
||||
),
|
||||
ksp_logging_lib::FileSettings::new(
|
||||
"file.pretty.error",
|
||||
true,
|
||||
pretty_directory.as_path(),
|
||||
"pretty.log",
|
||||
ksp_logging_lib::FileRotation::Never,
|
||||
ksp_logging_lib::LogFormat::Pretty,
|
||||
ksp_logging_lib::OutputFilter::new(ksp_logging_lib::LogFilterLevel::Error, std::vec![LOGGING_TARGET.to_string()], std::vec!["*".to_string()],),
|
||||
),
|
||||
ksp_logging_lib::FileSettings::new(
|
||||
"file.json.logging",
|
||||
true,
|
||||
json_directory.as_path(),
|
||||
"runtime.jsonl",
|
||||
ksp_logging_lib::FileRotation::Never,
|
||||
ksp_logging_lib::LogFormat::Json,
|
||||
ksp_logging_lib::OutputFilter::new(ksp_logging_lib::LogFilterLevel::Trace, std::vec![LOGGING_TARGET.to_string()], std::vec!["*".to_string()],),
|
||||
),
|
||||
],
|
||||
);
|
||||
let file_reload = ksp_logging_lib::reinitialize(&mut guard, &files_enabled);
|
||||
assert!(file_reload.is_ok());
|
||||
ksp_logging_lib::info!(target: LOGGING_TARGET, "file \x1b[31moutput\x1b[0m marker");
|
||||
ksp_logging_lib::info!(target: LOGGING_TARGET, "logging info \x1b[31mmarker\x1b[0m");
|
||||
ksp_logging_lib::error!(target: LOGGING_TARGET, "logging error marker");
|
||||
ksp_logging_lib::warn!(target: OTHER_KSP_TARGET, "store warning marker");
|
||||
ksp_logging_lib::info!(target: OTHER_KSP_TARGET, "store info must be filtered");
|
||||
tracing::error!(target: EXTERNAL_TARGET, "external marker must remain silent");
|
||||
let disable_after_file = ksp_logging_lib::reinitialize(&mut guard, &disabled);
|
||||
assert!(disable_after_file.is_ok());
|
||||
let file_text = read_directory_text(log_directory.as_path());
|
||||
assert!(file_text.contains("file output marker"));
|
||||
assert!(file_text.contains(LOGGING_TARGET));
|
||||
assert!(file_text.contains("runtime.rs"));
|
||||
assert!(!file_text.contains("\x1b["));
|
||||
assert!(!file_text.contains("external marker must remain silent"));
|
||||
let human_text = read_directory_text(human_directory.as_path());
|
||||
let compact_text = read_directory_text(compact_directory.as_path());
|
||||
let pretty_text = read_directory_text(pretty_directory.as_path());
|
||||
let json_text = read_directory_text(json_directory.as_path());
|
||||
assert!(human_text.contains("logging info marker"));
|
||||
assert!(human_text.contains("logging error marker"));
|
||||
assert!(!human_text.contains("store warning marker"));
|
||||
assert!(!human_text.contains("\x1b["));
|
||||
assert!(compact_text.contains("store warning marker"));
|
||||
assert!(!compact_text.contains("store info must be filtered"));
|
||||
assert!(!compact_text.contains("logging info marker"));
|
||||
assert!(pretty_text.contains("logging error marker"));
|
||||
assert!(!pretty_text.contains("logging info marker"));
|
||||
assert!(json_text.contains("logging info marker"));
|
||||
assert!(json_text.contains("logging error marker"));
|
||||
assert!(json_text.contains(LOGGING_TARGET));
|
||||
assert!(!json_text.contains("store warning marker"));
|
||||
assert!(!human_text.contains("external marker must remain silent"));
|
||||
assert!(!compact_text.contains("external marker must remain silent"));
|
||||
assert!(!pretty_text.contains("external marker must remain silent"));
|
||||
assert!(!json_text.contains("external marker must remain silent"));
|
||||
assert_eq!(guard.dropped_file_lines("file.human.logging"), std::option::Option::Some(0));
|
||||
assert_eq!(guard.dropped_file_lines("file.compact.store"), 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.unknown"), std::option::Option::None);
|
||||
let dropped = guard.dropped_lines();
|
||||
assert_eq!(dropped.total(), dropped.console().saturating_add(dropped.file()));
|
||||
let second_initialize = ksp_logging_lib::initialize(&disabled);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-logging-lib/unit_tests/runtime.rs
|
||||
// version: 6
|
||||
// version: 8
|
||||
|
||||
#[test]
|
||||
fn level_mapping_covers_all_ksp_levels() {
|
||||
@@ -56,7 +56,7 @@ fn disabled_runtime_has_no_layers_or_outputs() {
|
||||
};
|
||||
assert!(prepared.layers.is_empty());
|
||||
assert!(prepared.outputs.console.is_none());
|
||||
assert!(prepared.outputs.file.is_none());
|
||||
assert!(prepared.outputs.files.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -75,7 +75,7 @@ fn console_runtime_composes_takeover_filter_before_formatter_and_owns_guard() {
|
||||
};
|
||||
assert_eq!(prepared.layers.len(), 1);
|
||||
assert!(prepared.outputs.console.is_some());
|
||||
assert!(prepared.outputs.file.is_none());
|
||||
assert!(prepared.outputs.files.is_empty());
|
||||
assert_eq!(prepared.outputs.dropped_lines(), crate::DroppedLines::zero());
|
||||
}
|
||||
|
||||
@@ -103,40 +103,74 @@ fn takeover_filter_prefers_more_specific_ksp_prefixes_and_supports_off() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_multi_sink_runtime_rejects_new_capabilities_instead_of_ignoring_them() {
|
||||
let routed_console = crate::ConsoleSettings::new(
|
||||
fn multi_sink_runtime_accepts_metadata_routing_formats_and_console_ansi() {
|
||||
let root = std::env::temp_dir().join(format!("ksp-pre005-unit-{}", std::process::id()));
|
||||
let _cleanup_before = std::fs::remove_dir_all(root.as_path());
|
||||
let console = crate::ConsoleSettings::new(
|
||||
true,
|
||||
crate::ConsoleOutput::Stdout,
|
||||
true,
|
||||
crate::LogFormat::Compact,
|
||||
crate::OutputFilter::new(crate::LogFilterLevel::Debug, std::vec!["ksp-logging-lib".to_string()], std::vec!["logging".to_string()]),
|
||||
crate::OutputFilter::new(crate::LogFilterLevel::Debug, std::vec!["ksp-logging-lib".to_string()], std::vec!["*".to_string()]),
|
||||
);
|
||||
let routed_settings =
|
||||
crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::Some(routed_console), std::vec::Vec::new());
|
||||
assert!(routed_settings.validate().is_ok());
|
||||
assert!(super::prepare_runtime(&routed_settings).is_err());
|
||||
let first_file = crate::FileSettings::new(
|
||||
"file.first",
|
||||
true,
|
||||
"logs",
|
||||
root.join("first"),
|
||||
"first.log",
|
||||
crate::FileRotation::Never,
|
||||
crate::LogFormat::Human,
|
||||
crate::OutputFilter::unrestricted(),
|
||||
crate::LogFormat::Pretty,
|
||||
crate::OutputFilter::new(crate::LogFilterLevel::Info, std::vec!["ksp-logging-lib".to_string()], std::vec!["*".to_string()]),
|
||||
);
|
||||
let second_file = crate::FileSettings::new(
|
||||
"file.second",
|
||||
true,
|
||||
"logs",
|
||||
"second.log",
|
||||
root.join("second"),
|
||||
"second.jsonl",
|
||||
crate::FileRotation::Never,
|
||||
crate::LogFormat::Human,
|
||||
crate::OutputFilter::unrestricted(),
|
||||
crate::LogFormat::Json,
|
||||
crate::OutputFilter::new(crate::LogFilterLevel::Error, std::vec!["*".to_string()], std::vec!["*".to_string()]),
|
||||
);
|
||||
let multi_file_settings =
|
||||
crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::vec![first_file, second_file]);
|
||||
assert!(multi_file_settings.validate().is_ok());
|
||||
assert!(super::prepare_runtime(&multi_file_settings).is_err());
|
||||
let settings = crate::LoggingSettings::new(
|
||||
crate::LogFilterLevel::Trace,
|
||||
crate::SpanEvents::Off,
|
||||
std::option::Option::Some(console),
|
||||
std::vec![first_file, second_file],
|
||||
);
|
||||
let result = super::prepare_runtime(&settings);
|
||||
assert!(result.is_ok());
|
||||
let prepared = match result {
|
||||
std::result::Result::Ok(prepared) => prepared,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(prepared.layers.len(), 1);
|
||||
assert!(prepared.outputs.console.is_some());
|
||||
assert_eq!(prepared.outputs.files.len(), 2);
|
||||
assert_eq!(prepared.outputs.file_dropped_lines("file.first"), std::option::Option::Some(0));
|
||||
assert_eq!(prepared.outputs.file_dropped_lines("file.second"), std::option::Option::Some(0));
|
||||
drop(prepared);
|
||||
let cleanup_after = std::fs::remove_dir_all(root.as_path());
|
||||
assert!(cleanup_after.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn domain_routing_is_explicitly_deferred_instead_of_ignored() {
|
||||
let console = crate::ConsoleSettings::new(
|
||||
true,
|
||||
crate::ConsoleOutput::Stdout,
|
||||
false,
|
||||
crate::LogFormat::Human,
|
||||
crate::OutputFilter::new(crate::LogFilterLevel::Debug, std::vec!["*".to_string()], std::vec!["logging".to_string()]),
|
||||
);
|
||||
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);
|
||||
}
|
||||
|
||||
struct BlockingWriter {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-logging-lib/unit_tests/settings.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
fn unrestricted_file(output_id: &str) -> crate::FileSettings {
|
||||
return crate::FileSettings::new(
|
||||
@@ -50,8 +50,10 @@ fn output_filter_preserves_level_targets_and_domains() {
|
||||
assert_eq!(filter.domains().len(), 2);
|
||||
assert_eq!(filter.domains()[0], "config");
|
||||
assert_eq!(filter.domains()[1], "logging.runtime");
|
||||
assert!(!filter.is_unrestricted());
|
||||
assert!(crate::OutputFilter::unrestricted().is_unrestricted());
|
||||
let unrestricted = crate::OutputFilter::unrestricted();
|
||||
assert_eq!(unrestricted.level(), crate::LogFilterLevel::Trace);
|
||||
assert_eq!(unrestricted.targets(), &["*".to_string()]);
|
||||
assert_eq!(unrestricted.domains(), &["*".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -74,7 +76,7 @@ fn compatibility_console_constructors_are_unrestricted_human_and_non_ansi() {
|
||||
assert!(stdout.enabled());
|
||||
assert!(!stdout.ansi());
|
||||
assert_eq!(stdout.format(), crate::LogFormat::Human);
|
||||
assert!(stdout.filter().is_unrestricted());
|
||||
assert_eq!(stdout.filter(), &crate::OutputFilter::unrestricted());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -175,21 +177,21 @@ fn validation_rejects_empty_file_paths_and_persistent_ansi() {
|
||||
crate::OutputFilter::unrestricted(),
|
||||
)
|
||||
.with_ansi(true);
|
||||
assert!(
|
||||
crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::vec![empty_directory])
|
||||
.validate()
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::vec![empty_prefix])
|
||||
.validate()
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::vec![ansi_file])
|
||||
.validate()
|
||||
.is_err()
|
||||
);
|
||||
let empty_directory_settings =
|
||||
crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::vec![empty_directory]);
|
||||
let empty_prefix_settings =
|
||||
crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::vec![empty_prefix]);
|
||||
let ansi_file_settings = crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::vec![ansi_file]);
|
||||
assert!(empty_directory_settings.validate().is_err());
|
||||
assert!(empty_prefix_settings.validate().is_err());
|
||||
assert!(ansi_file_settings.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_rejects_ansi_json_console() {
|
||||
let console = crate::ConsoleSettings::new(true, crate::ConsoleOutput::Stdout, true, crate::LogFormat::Json, crate::OutputFilter::unrestricted());
|
||||
let settings = crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::Some(console), std::vec::Vec::new());
|
||||
assert!(settings.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-logging-lib/unit_tests/writer.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
#[test]
|
||||
fn ansi_writer_strips_csi_sequences() {
|
||||
@@ -28,3 +28,24 @@ fn ansi_writer_strips_osc_sequences_terminated_by_bell_or_st() {
|
||||
assert!(second.is_ok());
|
||||
assert_eq!(writer.into_inner(), b"abcd");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_level_routing_covers_all_filter_levels() {
|
||||
assert!(!super::level_is_enabled(&tracing::Level::ERROR, crate::LogFilterLevel::Off));
|
||||
assert!(super::level_is_enabled(&tracing::Level::ERROR, crate::LogFilterLevel::Error));
|
||||
assert!(!super::level_is_enabled(&tracing::Level::WARN, crate::LogFilterLevel::Error));
|
||||
assert!(super::level_is_enabled(&tracing::Level::WARN, crate::LogFilterLevel::Warn));
|
||||
assert!(!super::level_is_enabled(&tracing::Level::INFO, crate::LogFilterLevel::Warn));
|
||||
assert!(super::level_is_enabled(&tracing::Level::INFO, crate::LogFilterLevel::Info));
|
||||
assert!(super::level_is_enabled(&tracing::Level::DEBUG, crate::LogFilterLevel::Debug));
|
||||
assert!(super::level_is_enabled(&tracing::Level::TRACE, crate::LogFilterLevel::Trace));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_routed_writer_discards_bytes_without_error() {
|
||||
let mut writer = super::RoutedWriter::<std::vec::Vec<u8>>::Disabled;
|
||||
let write_result = std::io::Write::write_all(&mut writer, b"discarded");
|
||||
let flush_result = std::io::Write::flush(&mut writer);
|
||||
assert!(write_result.is_ok());
|
||||
assert!(flush_result.is_ok());
|
||||
}
|
||||
|
||||
313
deltas/0.1.3/pre.005.md
Normal file
313
deltas/0.1.3/pre.005.md
Normal file
@@ -0,0 +1,313 @@
|
||||
<!-- file: deltas/0.1.3/pre.005.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta 0.1.3-pre.005
|
||||
|
||||
## Base requise
|
||||
|
||||
Livraison précédente validée :
|
||||
|
||||
```text
|
||||
0.1.3-pre.004
|
||||
```
|
||||
|
||||
Version technique de cette base :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.3-pre.4"
|
||||
Cargo.toml header version = 44
|
||||
```
|
||||
|
||||
Validations utilisateur de `pre.004` 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
|
||||
```
|
||||
|
||||
## Objet de pre.005
|
||||
|
||||
Cette tranche active le runtime multi-sink derrière les contrats publics stabilisés en `pre.004`, mais reste bornée aux dimensions de routing directement portées par les métadonnées des events/spans :
|
||||
|
||||
```text
|
||||
level
|
||||
target
|
||||
```
|
||||
|
||||
Le routing par champ structuré `domain` est scindé en `pre.006` plutôt que d'être approximé ou assimilé au target.
|
||||
|
||||
## Multi-sink runtime
|
||||
|
||||
`RuntimeOutputs` possède maintenant :
|
||||
|
||||
```text
|
||||
console: Option<RuntimeOutput>
|
||||
files: Vec<RuntimeFileOutput>
|
||||
```
|
||||
|
||||
Tous les `FileSettings` actifs sont préparés et installés simultanément.
|
||||
|
||||
Chaque fichier conserve :
|
||||
|
||||
```text
|
||||
output_id
|
||||
WorkerGuard
|
||||
ErrorCounter
|
||||
```
|
||||
|
||||
Le lifecycle de hot reload reste celui stabilisé en `0.1.2` :
|
||||
|
||||
1. validation et préparation complète des nouvelles sorties ;
|
||||
2. swap du groupe de layers via le handle reload existant ;
|
||||
3. conservation des compteurs des sorties retirées ;
|
||||
4. destruction des anciens layers ;
|
||||
5. destruction des anciens guards/writers.
|
||||
|
||||
Un échec de préparation avant le swap laisse la configuration active intacte.
|
||||
|
||||
## Routing par output : level + target
|
||||
|
||||
Chaque formatter utilise désormais un `RouteMakeWriter` interne.
|
||||
|
||||
Lors de `make_writer_for(metadata)` :
|
||||
|
||||
- `OutputFilter.level` décide si le niveau est accepté ;
|
||||
- `OutputFilter.targets[]` accepte le wildcard `*` ou un préfixe de target KSP ;
|
||||
- un writer désactivé absorbe la sortie sans écrire dans le sink concerné.
|
||||
|
||||
Le takeover global existant reste distinct :
|
||||
|
||||
```text
|
||||
default_filter + TargetFilter[]
|
||||
↓
|
||||
groupe de sinks
|
||||
↓
|
||||
OutputFilter level/target propre à chaque sink
|
||||
```
|
||||
|
||||
Le routing par output ne transforme pas les layers en `Filtered` reloadables. La composition `Targets.and_then(output_layers)` stabilisée pendant `0.1.2` est conservée.
|
||||
|
||||
## Scission du routing `domain`
|
||||
|
||||
`domain` est un champ structuré déclaré par les callers, par exemple :
|
||||
|
||||
```rust
|
||||
ksp_logging_lib::info!(
|
||||
target: "ksp-config-lib",
|
||||
domain = "config",
|
||||
"configuration event"
|
||||
);
|
||||
```
|
||||
|
||||
Il n'est pas une propriété des métadonnées de target.
|
||||
|
||||
Pour respecter le budget de tranche et éviter une sémantique incorrecte, `pre.005` refuse explicitement un output **actif** dont :
|
||||
|
||||
```text
|
||||
domains != ["*"]
|
||||
```
|
||||
|
||||
Le refus utilise `logging.invalid_settings` avec le contexte :
|
||||
|
||||
```text
|
||||
runtime_contract = metadata-routing-before-domain-routing
|
||||
```
|
||||
|
||||
Les contrats publics de `pre.004` restent inchangés : un output désactivé peut déjà porter son futur filtre domain et `LoggingSettings::validate()` continue de le valider structurellement.
|
||||
|
||||
`pre.006` est insérée pour traiter :
|
||||
|
||||
- domain porté directement par un event ;
|
||||
- domain d'un span ;
|
||||
- héritage vers les events sans champ direct ;
|
||||
- spans imbriqués ;
|
||||
- lifecycle `SpanEvents` ;
|
||||
- interaction avec les quatre formats et plusieurs sinks.
|
||||
|
||||
## Formats runtime
|
||||
|
||||
Les quatre formats publics sont maintenant actifs :
|
||||
|
||||
```text
|
||||
Human
|
||||
Compact
|
||||
Pretty
|
||||
Json
|
||||
```
|
||||
|
||||
Le manifest racine active pour `tracing-subscriber` :
|
||||
|
||||
```toml
|
||||
features = ["fmt", "json", "ansi"]
|
||||
```
|
||||
|
||||
Aucune nouvelle dépendance directe n'est ajoutée au workspace.
|
||||
|
||||
## ANSI
|
||||
|
||||
La console applique désormais réellement `ConsoleSettings::ansi()` pour les formats humains.
|
||||
|
||||
Les fichiers restent construits derrière `StripAnsiWriter` et persistent sans ANSI.
|
||||
|
||||
La combinaison :
|
||||
|
||||
```text
|
||||
console.format = Json
|
||||
console.ansi = true
|
||||
```
|
||||
|
||||
est refusée par `LoggingSettings::validate()` afin de ne pas accepter une option qui serait sans effet.
|
||||
|
||||
## Compteurs par output fichier
|
||||
|
||||
La vue agrégée existante reste disponible :
|
||||
|
||||
```rust
|
||||
LoggingGuard::dropped_lines() -> DroppedLines
|
||||
```
|
||||
|
||||
avec :
|
||||
|
||||
```text
|
||||
console
|
||||
file
|
||||
total
|
||||
```
|
||||
|
||||
`pre.005` ajoute :
|
||||
|
||||
```rust
|
||||
LoggingGuard::dropped_file_lines(output_id: &str) -> Option<usize>
|
||||
```
|
||||
|
||||
Le compteur est cumulatif à travers les reloads pour un même `LoggingGuard` et un même `output_id`, y compris après retrait du sink.
|
||||
|
||||
Un `output_id` qui n'a jamais été actif retourne `None`.
|
||||
|
||||
## Tests
|
||||
|
||||
Les tests unitaires couvrent notamment :
|
||||
|
||||
- préparation de plusieurs fichiers simultanés ;
|
||||
- console ANSI + format Compact ;
|
||||
- fichiers Pretty et Json ;
|
||||
- refus explicite d'un domain spécifique ;
|
||||
- mapping des niveaux de routing ;
|
||||
- writer désactivé ;
|
||||
- rejet JSON + ANSI console ;
|
||||
- conservation des tests de takeover, rotation, saturation et span lifecycle.
|
||||
|
||||
Le test global runtime est étendu pour vérifier en un seul subscriber global :
|
||||
|
||||
- échec transactionnel d'une configuration domain non encore supportée ;
|
||||
- quatre fichiers simultanés ;
|
||||
- formats Human, Compact, Pretty et Json ;
|
||||
- routing indépendant par target et niveau ;
|
||||
- silence des targets externes ;
|
||||
- stripping ANSI fichier ;
|
||||
- counters par `output_id` après retrait des sinks ;
|
||||
- hot reload concurrent existant ;
|
||||
- impossibilité d'une seconde initialisation globale.
|
||||
|
||||
## Plan de release regranularisé
|
||||
|
||||
La scission explicite décale le reste du cycle :
|
||||
|
||||
```text
|
||||
pre.005 Logging : runtime multi-sink + level/target/formats
|
||||
pre.006 Logging : routing structuré domain
|
||||
pre.007 JSON/JSON Schema + std.logging.json
|
||||
pre.008 globals + profils + default_profile
|
||||
pre.009 compositions génériques par file_id
|
||||
pre.010 .env + process env + resolver ${...}
|
||||
pre.011 sensibilité + real/safe/provenance
|
||||
pre.012 adapter Config -> Logging
|
||||
pre.013 management + persistence JSON/.env
|
||||
pre.014 ownership audits + robustesse
|
||||
pre.015 clôture
|
||||
```
|
||||
|
||||
Le schema `std.logging.schema.json` n'est donc pas figé avant validation du routing domain.
|
||||
|
||||
## Version technique
|
||||
|
||||
La prerelease devient :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.3-pre.5"
|
||||
```
|
||||
|
||||
Le manifest racine devient :
|
||||
|
||||
```text
|
||||
# version: 45
|
||||
```
|
||||
|
||||
## Fichier ajouté
|
||||
|
||||
```text
|
||||
deltas/0.1.3/pre.005.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/settings.rs
|
||||
crates/ksp-logging-lib/src/writer.rs
|
||||
crates/ksp-logging-lib/tests/public_api.rs
|
||||
crates/ksp-logging-lib/tests/runtime.rs
|
||||
crates/ksp-logging-lib/unit_tests/runtime.rs
|
||||
crates/ksp-logging-lib/unit_tests/settings.rs
|
||||
crates/ksp-logging-lib/unit_tests/writer.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 confirmé
|
||||
|
||||
Cette tranche n'implémente pas :
|
||||
|
||||
- le routing runtime par champ structuré `domain` ;
|
||||
- JSON Config ou JSON Schema ;
|
||||
- `std.logging.json` ;
|
||||
- profils/composites Config ;
|
||||
- `.env` ou interpolation ;
|
||||
- secrets/redaction Config ;
|
||||
- modification de `ksp-config-lib` ;
|
||||
- rotation par taille/rétention/compression.
|
||||
|
||||
## Validations à exécuter par l'utilisateur
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Les commandes Cargo ne sont pas déclarées réussies dans ce delta tant qu'elles n'ont pas été exécutées sur l'environnement utilisateur.
|
||||
|
||||
## Suite
|
||||
|
||||
Après validation de cette tranche :
|
||||
|
||||
```text
|
||||
0.1.3-pre.006 — ksp-logging-lib : routing structuré domain
|
||||
```
|
||||
|
||||
Cette tranche devra fermer la dernière dimension du contrat `OutputFilter` avant l'introduction de `std.logging.schema.json`.
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/000-README.md -->
|
||||
<!-- version: 11 -->
|
||||
<!-- version: 12 -->
|
||||
|
||||
# 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, puis regranularisé par `pre.001-fix.003` pour séparer le bootstrap, le registre et les deux tranches Logging avant développement fonctionnel de `ksp-config-lib`.
|
||||
- [`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.
|
||||
|
||||
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 -->
|
||||
<!-- version: 9 -->
|
||||
<!-- version: 10 -->
|
||||
|
||||
# Séquence des releases fonctionnelles KSP
|
||||
|
||||
@@ -198,22 +198,23 @@ Le `pre.001` ne scinde pas Config : `0.1.3` reste une release unique et `0.1.4`
|
||||
|
||||
Cette décision repose sur l'absence volontaire de documents Transport/Wallet/Store/Execution et de watcher générique dans le premier cycle. Si une tranche ultérieure révèle une contrainte technique majeure réellement non bornable, la séquence peut encore être corrigée par un delta explicite plutôt que de comprimer artificiellement le périmètre.
|
||||
|
||||
Prévision souple regranularisée par `pre.001-fix.003` :
|
||||
Prévision souple regranularisée par `pre.001-fix.003`, puis scindée à nouveau pendant `pre.005` afin de traiter `domain` comme un champ structuré distinct :
|
||||
|
||||
```text
|
||||
pre.002 crate Config + bootstrap cfgpath/schemapath
|
||||
pre.003 registre file_id -> filename + --filemap
|
||||
pre.004 Logging : contrats/settings multi-output
|
||||
pre.005 Logging : runtime multi-sink + routing/filter
|
||||
pre.006 JSON/JSON Schema + std.logging.json
|
||||
pre.007 globals + profils + default_profile
|
||||
pre.008 compositions génériques par file_id
|
||||
pre.009 .env + process env + resolver ${...}
|
||||
pre.010 sensibilité + real/safe/provenance
|
||||
pre.011 adapter Config -> Logging
|
||||
pre.012 management + persistence JSON/.env
|
||||
pre.013 ownership audits + robustesse
|
||||
pre.014 clôture
|
||||
pre.005 Logging : runtime multi-sink + level/target/formats
|
||||
pre.006 Logging : routing structuré domain
|
||||
pre.007 JSON/JSON Schema + std.logging.json
|
||||
pre.008 globals + profils + default_profile
|
||||
pre.009 compositions génériques par file_id
|
||||
pre.010 .env + process env + resolver ${...}
|
||||
pre.011 sensibilité + real/safe/provenance
|
||||
pre.012 adapter Config -> Logging
|
||||
pre.013 management + persistence JSON/.env
|
||||
pre.014 ownership audits + robustesse
|
||||
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.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<!-- file: docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md -->
|
||||
<!-- version: 7 -->
|
||||
<!-- version: 8 -->
|
||||
|
||||
# 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`, et `pre.004` étend maintenant les contrats publics de `ksp-logging-lib` nécessaires au futur `std.logging.json` sans ouvrir encore le runtime multi-sink complet.
|
||||
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.
|
||||
|
||||
La base auditée reste la release stable `v0.1.2`.
|
||||
|
||||
@@ -230,25 +230,25 @@ La complétion Logging n'ouvre ni une nouvelle façade ni une dépendance invers
|
||||
|
||||
## 5. Matrice de responsabilités
|
||||
|
||||
| Responsabilité | Propriétaire | Consommateurs | Interdit |
|
||||
|---|---|---|---|
|
||||
| Définir les `file_id` connus et leur mapping par défaut | `ksp-config-lib` | bootstrap Config | noms physiques codés dans les consumers |
|
||||
| Résoudre `file_id -> filename` | `ksp-config-lib` | session Config | référence inter-document par filename |
|
||||
| Interpréter `--cfgpath`, `--schemapath` et overrides de mapping KSP | `ksp-config-lib` | applications qui transmettent argv/options | parser ces options différemment dans chaque binaire |
|
||||
| Lire un document Config JSON | `ksp-config-lib` | crates/apps via API Config | lecture directe par consumer |
|
||||
| Valider JSON Schema | `ksp-config-lib` | orchestration/management | validation divergente dans chaque crate |
|
||||
| Résoudre globals/profils/compositions | `ksp-config-lib` | orchestration | résolution locale dans un binaire |
|
||||
| Lire `KSP_*` / `KSPB_*` du processus | `ksp-config-lib` | crates/apps via API Config | `std::env::var*` applicatif hors Config |
|
||||
| Lire `.env` | `ksp-config-lib` | crates/apps via API Config | loader dotenv direct hors Config |
|
||||
| Résoudre `${...}` et fallback | `ksp-config-lib` | tous les consumers | interpolation locale dans les consumers |
|
||||
| Classer sensibilité et construire une valeur sûre | `ksp-config-lib` | runtime/logging/diagnostics | redaction ad hoc dans chaque crate |
|
||||
| Modifier/sauvegarder JSON Config | `ksp-config-lib` | management explicite | écriture directe par application |
|
||||
| Créer/modifier/supprimer une entrée `.env` | `ksp-config-lib` | management explicite | édition directe par application |
|
||||
| Modifier l'environnement externe du shell/systemd/parent | propriétaire externe de ce processus | Config le lit seulement | prétendre qu'un child process peut administrer son parent |
|
||||
| Posséder les settings/sinks/routing Logging | `ksp-logging-lib` | Config construit les contrats publics | logique de routing dupliquée dans Config |
|
||||
| Posséder `LoggingGuard` | orchestration/application | lifecycle Logging | singleton Config global |
|
||||
| Initialiser/recharger Logging | orchestration | Config fournit settings/changement | Logging lisant Config |
|
||||
| DTO/bindings Tauri | application Tauri | frontend | TS-RS automatique dans Config |
|
||||
| Responsabilité | Propriétaire | Consommateurs | Interdit |
|
||||
|---------------------------------------------------------------------|--------------------------------------|--------------------------------------------|-----------------------------------------------------------|
|
||||
| Définir les `file_id` connus et leur mapping par défaut | `ksp-config-lib` | bootstrap Config | noms physiques codés dans les consumers |
|
||||
| Résoudre `file_id -> filename` | `ksp-config-lib` | session Config | référence inter-document par filename |
|
||||
| Interpréter `--cfgpath`, `--schemapath` et overrides de mapping KSP | `ksp-config-lib` | applications qui transmettent argv/options | parser ces options différemment dans chaque binaire |
|
||||
| Lire un document Config JSON | `ksp-config-lib` | crates/apps via API Config | lecture directe par consumer |
|
||||
| Valider JSON Schema | `ksp-config-lib` | orchestration/management | validation divergente dans chaque crate |
|
||||
| Résoudre globals/profils/compositions | `ksp-config-lib` | orchestration | résolution locale dans un binaire |
|
||||
| Lire `KSP_*` / `KSPB_*` du processus | `ksp-config-lib` | crates/apps via API Config | `std::env::var*` applicatif hors Config |
|
||||
| Lire `.env` | `ksp-config-lib` | crates/apps via API Config | loader dotenv direct hors Config |
|
||||
| Résoudre `${...}` et fallback | `ksp-config-lib` | tous les consumers | interpolation locale dans les consumers |
|
||||
| Classer sensibilité et construire une valeur sûre | `ksp-config-lib` | runtime/logging/diagnostics | redaction ad hoc dans chaque crate |
|
||||
| Modifier/sauvegarder JSON Config | `ksp-config-lib` | management explicite | écriture directe par application |
|
||||
| Créer/modifier/supprimer une entrée `.env` | `ksp-config-lib` | management explicite | édition directe par application |
|
||||
| Modifier l'environnement externe du shell/systemd/parent | propriétaire externe de ce processus | Config le lit seulement | prétendre qu'un child process peut administrer son parent |
|
||||
| Posséder les settings/sinks/routing Logging | `ksp-logging-lib` | Config construit les contrats publics | logique de routing dupliquée dans Config |
|
||||
| Posséder `LoggingGuard` | orchestration/application | lifecycle Logging | singleton Config global |
|
||||
| Initialiser/recharger Logging | orchestration | Config fournit settings/changement | Logging lisant Config |
|
||||
| DTO/bindings Tauri | application Tauri | frontend | TS-RS automatique dans Config |
|
||||
|
||||
## 6. Registre de fichiers, bootstrap, arborescence et nomenclature
|
||||
|
||||
@@ -580,23 +580,23 @@ Décisions :
|
||||
|
||||
### 8.1 Écart `ksp-logging-lib 0.1.2` à fermer
|
||||
|
||||
| Capacité | `0.1.2` | Requise par `std.logging.json` |
|
||||
|---|---:|---:|
|
||||
| filtre global | oui | oui |
|
||||
| overrides par target | oui | oui |
|
||||
| lifecycle spans | oui | oui |
|
||||
| console stdout/stderr | oui | oui |
|
||||
| console enabled | via `Option` | oui explicite |
|
||||
| console ANSI configurable | non | oui |
|
||||
| format console configurable | non | oui |
|
||||
| plusieurs fichiers | non | oui |
|
||||
| rotation par fichier | un seul fichier | oui par sink |
|
||||
| format par fichier | non | oui |
|
||||
| filtre par sink/target | non | oui |
|
||||
| filtre par sink/domain | non | oui |
|
||||
| filtre par sink/niveau | non indépendant | oui |
|
||||
| hot reload transactionnel | oui | à conserver |
|
||||
| non-blocking/guards/drop counters | oui | à conserver et généraliser par sinks |
|
||||
| Capacité | `0.1.2` | Requise par `std.logging.json` |
|
||||
|-----------------------------------|----------------:|-------------------------------------:|
|
||||
| filtre global | oui | oui |
|
||||
| overrides par target | oui | oui |
|
||||
| lifecycle spans | oui | oui |
|
||||
| console stdout/stderr | oui | oui |
|
||||
| console enabled | via `Option` | oui explicite |
|
||||
| console ANSI configurable | non | oui |
|
||||
| format console configurable | non | oui |
|
||||
| plusieurs fichiers | non | oui |
|
||||
| rotation par fichier | un seul fichier | oui par sink |
|
||||
| format par fichier | non | oui |
|
||||
| filtre par sink/target | non | oui |
|
||||
| filtre par sink/domain | non | oui |
|
||||
| filtre par sink/niveau | non indépendant | oui |
|
||||
| hot reload transactionnel | oui | à conserver |
|
||||
| non-blocking/guards/drop counters | oui | à conserver et généraliser par sinks |
|
||||
|
||||
Ce tableau est un **gap identifié**, pas une invitation à déplacer Logging dans Config. La tranche qui le ferme modifie `ksp-logging-lib` uniquement dans son domaine propriétaire.
|
||||
|
||||
@@ -1646,37 +1646,68 @@ Implémentation candidate livrée par `pre.004` :
|
||||
- jusqu'à `pre.005`, `initialize/reinitialize` refusent explicitement les nouvelles capacités qu'ils ne savent pas encore appliquer (plusieurs fichiers actifs, formats enrichis, ANSI console ou routing par output) plutôt que de les ignorer silencieusement ;
|
||||
- aucune dépendance Config n'entre dans Logging et aucune dépendance externe supplémentaire n'est ajoutée.
|
||||
|
||||
La validation utilisateur de `pre.004` reste requise avant l'ouverture de `pre.005`.
|
||||
`pre.004` a été validée par l'utilisateur le 2026-08-15 avec `cargo fmt --all`, `cargo check --workspace`, `cargo clippy --workspace --all-targets`, `cargo test --workspace` et les trois vues `cargo tree -p ksp-logging-lib` demandées, sans warning ni échec.
|
||||
|
||||
### `0.1.3-pre.005` — `ksp-logging-lib` : runtime multi-sink + routing
|
||||
### `0.1.3-pre.005` — `ksp-logging-lib` : runtime multi-sink + routing metadata
|
||||
|
||||
Objectif unique : implémenter derrière les contrats de `pre.004` le comportement runtime manquant.
|
||||
Objectif unique : activer derrière les contrats de `pre.004` les capacités runtime bornées aux métadonnées directement disponibles par output.
|
||||
|
||||
- 0..N fichiers simultanés ;
|
||||
- console indépendante ;
|
||||
- routing/filter par output, level, target et domain ;
|
||||
- rotation/format/ANSI selon settings ;
|
||||
- writers non bloquants et guards ;
|
||||
- conserver subscriber unique et takeover ;
|
||||
- routing/filter par output sur level + target ;
|
||||
- rotation/format `Human/Compact/Pretty/Json` ;
|
||||
- ANSI console configurable, jamais persisté dans les fichiers ;
|
||||
- writers non bloquants et guards indépendants ;
|
||||
- compteurs agrégés conservés + compteur cumulatif fichier par `output_id` ;
|
||||
- conserver subscriber unique et takeover global KSP ;
|
||||
- conserver `initialize/reinitialize` transactionnel ;
|
||||
- conserver drop counters et comportement de saturation ;
|
||||
- tests de non-régression et hot reload.
|
||||
- conserver le comportement de saturation ;
|
||||
- tests multi-sink, formats, routing metadata et hot reload.
|
||||
|
||||
Si l'audit de `pre.004` montre que multi-sink et routing/hot-reload ne tiennent pas proprement ensemble dans le budget, `pre.005` est scindée avant implémentation ; le numéro de clôture est alors décalé explicitement.
|
||||
Décision de scission prise pendant l'implémentation : `domain` est un champ structuré et ne doit être ni assimilé au target ni filtré approximativement au niveau du writer. `pre.005` refuse donc explicitement tout output actif avec `domains != ["*"]`.
|
||||
|
||||
### `0.1.3-pre.006` — JSON/JSON Schema + premier document Logging
|
||||
Implémentation candidate de `pre.005` :
|
||||
|
||||
- chaque formatter possède son writer routé par `OutputFilter.level` + `OutputFilter.targets` ;
|
||||
- la politique globale `default_filter` + `TargetFilter` reste composée devant tout le groupe de sinks ;
|
||||
- les layers reloadables ne deviennent pas des `Filtered`, afin de préserver la composition/hot reload stabilisée en `0.1.2` ;
|
||||
- `tracing-subscriber` active ses features `json` et `ansi` pour les formats publics déjà décidés en `pre.004` ;
|
||||
- `LoggingGuard::dropped_file_lines(output_id)` expose le compteur cumulatif d'un fichier à travers les reloads ;
|
||||
- `DroppedLines` conserve la vue agrégée console/fichier ;
|
||||
- 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`.
|
||||
|
||||
### `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.
|
||||
|
||||
Une solution qui réduirait `domain` à une convention de target est interdite : il s'agit d'une dimension structurée distincte.
|
||||
|
||||
### `0.1.3-pre.007` — JSON/JSON Schema + premier document Logging
|
||||
|
||||
- `serde`/`serde_json`/`jsonschema` au workspace avec versions revérifiées ;
|
||||
- infrastructure générique de lecture JSON ;
|
||||
- association descriptor -> schema par `file_id` ;
|
||||
- validation JSON Schema ;
|
||||
- `config/`, `config/schemas/`, `config/examples/` ;
|
||||
- `std.logging.schema.json` aligné sur les contrats Logging réellement stabilisés en `pre.004/.005` ;
|
||||
- `std.logging.schema.json` aligné sur les contrats Logging réellement stabilisés en `pre.004/.005/.006` ;
|
||||
- runtime `config/std.logging.json` ;
|
||||
- exemple Logging ;
|
||||
- parse/schema/validation sémantique de base.
|
||||
|
||||
### `0.1.3-pre.007` — globals + profils + `default_profile`
|
||||
### `0.1.3-pre.008` — globals + profils + `default_profile`
|
||||
|
||||
- paramètres globaux hors profils ;
|
||||
- `profile_id` unique ;
|
||||
@@ -1686,7 +1717,7 @@ Si l'audit de `pre.004` montre que multi-sink et routing/hot-reload ne tiennent
|
||||
- provenance document/global/profile ;
|
||||
- tests sur `std.logging.json`.
|
||||
|
||||
### `0.1.3-pre.008` — compositions génériques par `file_id`
|
||||
### `0.1.3-pre.009` — compositions génériques par `file_id`
|
||||
|
||||
- contrat composite générique ;
|
||||
- schema/example composite ;
|
||||
@@ -1695,7 +1726,7 @@ Si l'audit de `pre.004` montre que multi-sink et routing/hot-reload ne tiennent
|
||||
- résolution composition -> document -> profil/global ;
|
||||
- aucun composite runtime fictif obligatoire.
|
||||
|
||||
### `0.1.3-pre.009` — `.env` + process env + resolver `${...}`
|
||||
### `0.1.3-pre.010` — `.env` + process env + resolver `${...}`
|
||||
|
||||
- lecture process env ;
|
||||
- lecture `.env` sans écrasement process ;
|
||||
@@ -1706,7 +1737,7 @@ Si l'audit de `pre.004` montre que multi-sink et routing/hot-reload ne tiennent
|
||||
- namespaces KSP/KSPB ;
|
||||
- tests d'isolation process env.
|
||||
|
||||
### `0.1.3-pre.010` — sensibilité + valeurs real/safe/provenance
|
||||
### `0.1.3-pre.011` — sensibilité + valeurs real/safe/provenance
|
||||
|
||||
- `Public/Internal/Secret` ;
|
||||
- propagation de sensibilité par chaîne composée ;
|
||||
@@ -1716,7 +1747,7 @@ Si l'audit de `pre.004` montre que multi-sink et routing/hot-reload ne tiennent
|
||||
- interdiction des secrets dans logs/diagnostics ordinaires ;
|
||||
- tests canary de non-divulgation.
|
||||
|
||||
### `0.1.3-pre.011` — adapter Config -> Logging
|
||||
### `0.1.3-pre.012` — adapter Config -> Logging
|
||||
|
||||
- `ResolvedLoggingConfig` ;
|
||||
- conversion explicite vers les contrats publics `ksp_logging_lib::*` ;
|
||||
@@ -1726,7 +1757,7 @@ Si l'audit de `pre.004` montre que multi-sink et routing/hot-reload ne tiennent
|
||||
- tests de mapping multi-output/filter/domain ;
|
||||
- vérification qu'aucune valeur secrète n'est utilisée dans les diagnostics Logging.
|
||||
|
||||
### `0.1.3-pre.012` — management + persistence JSON/.env
|
||||
### `0.1.3-pre.013` — management + persistence JSON/.env
|
||||
|
||||
- lecture management ;
|
||||
- reveal secret explicite ;
|
||||
@@ -1736,7 +1767,7 @@ Si l'audit de `pre.004` montre que multi-sink et routing/hot-reload ne tiennent
|
||||
- desired/effective/shadow report ;
|
||||
- refus des mutations non autorisées.
|
||||
|
||||
### `0.1.3-pre.013` — ownership audits + robustesse
|
||||
### `0.1.3-pre.014` — ownership audits + robustesse
|
||||
|
||||
- audits interdisant les accès Config/env directs ailleurs ;
|
||||
- invalid documents/env/placeholders/file mappings ;
|
||||
@@ -1745,7 +1776,7 @@ Si l'audit de `pre.004` montre que multi-sink et routing/hot-reload ne tiennent
|
||||
- robustesse des diagnostics sans fuite ;
|
||||
- vérification de la granularité réelle des responsabilités publiques.
|
||||
|
||||
### `0.1.3-pre.014` — clôture
|
||||
### `0.1.3-pre.015` — clôture
|
||||
|
||||
- validations finales ;
|
||||
- documentation durable ;
|
||||
@@ -1832,9 +1863,9 @@ Le `pre.001-fix.003` est validable lorsque les décisions suivantes sont accept
|
||||
- l'écart de capacités de `ksp-logging-lib 0.1.2` est reconnu et sera fermé dans Logging, pas contourné par Config ;
|
||||
- le lifecycle/ownership Logging `initialize/reinitialize/LoggingGuard` reste inchangé ;
|
||||
- persistence JSON et `.env` atomique ;
|
||||
- `0.1.3` reste une release unique bornée avec clôture prévisionnelle en `pre.014`, sous réserve de nouvelles scissions explicites si une tranche dépasse le budget 15–20 minutes ;
|
||||
- `0.1.3` reste une release unique bornée avec clôture prévisionnelle désormais en `pre.015`, sous réserve de nouvelles scissions explicites si une tranche dépasse le budget 15–20 minutes ;
|
||||
- `pre.002` est limitée à la création de la crate et au bootstrap `cfgpath`/`schemapath`; le registre `file_id` commence en `pre.003` ;
|
||||
- les évolutions `ksp-logging-lib` sont explicites en `pre.004` (contrats/settings multi-output) puis `pre.005` (runtime multi-sink/routing), avant gel du schema Logging ;
|
||||
- les évolutions `ksp-logging-lib` sont explicites en `pre.004` (contrats/settings), `pre.005` (runtime multi-sink + level/target/formats) puis `pre.006` (routing structuré domain), avant gel du schema Logging ;
|
||||
- chaque prerelease vise environ 15–20 minutes de travail effectif et doit être scindée si ce budget devient manifestement irréaliste ;
|
||||
- aucune implémentation `pre.002` ne commence avant validation utilisateur de ce plan corrigé.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user