v0.1.3-pre.004
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 43
|
||||
# version: 44
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.3-pre.3"
|
||||
version = "0.1.3-pre.4"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-logging-lib/README.md -->
|
||||
<!-- version: 2 -->
|
||||
<!-- version: 3 -->
|
||||
|
||||
# ksp-logging-lib
|
||||
|
||||
@@ -11,11 +11,13 @@ La crate possède :
|
||||
|
||||
- les cinq niveaux KSP `error`, `warn`, `info`, `debug` et `trace` ;
|
||||
- les macros d'événements et de spans qui préservent le callsite du consommateur ;
|
||||
- `LoggingSettings` et les settings console/fichier indépendants de Config ;
|
||||
- `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 ;
|
||||
- l'installation unique du subscriber global ;
|
||||
- le hot reload via `reinitialize` sans second subscriber global ;
|
||||
- le takeover des logs : les targets externes sont silencieux par défaut ;
|
||||
- les sorties console et fichier non bloquantes ;
|
||||
- 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 ;
|
||||
- l'instrumentation de scopes synchrones et de `Future` async.
|
||||
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
<!-- file: crates/ksp-logging-lib/TODO.md -->
|
||||
<!-- version: 2 -->
|
||||
<!-- version: 3 -->
|
||||
|
||||
# TODO ksp-logging-lib
|
||||
|
||||
## À fermer avant la stable 0.1.2
|
||||
## À fermer pendant `0.1.3`
|
||||
|
||||
- exécuter les validations Cargo complètes de `pre.006`, y compris les tests Tokio current-thread/multi-thread ;
|
||||
- vérifier que `cargo tree -p ksp-logging-lib -e normal` ne contient pas Tokio et que Tokio apparaît uniquement dans le graphe dev attendu ;
|
||||
- refaire l'audit final du graphe/features et de l'ownership de la stack tracing ;
|
||||
- après validation de la prerelease finale, préparer `rel.001`, publier `workspace.package.version = "0.1.2"` et taguer `v0.1.2` conformément aux règles de release.
|
||||
`0.1.3-pre.004` expose désormais le modèle public multi-output. La tranche runtime suivante doit encore :
|
||||
|
||||
- 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.
|
||||
|
||||
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.
|
||||
|
||||
## Capacités différées
|
||||
|
||||
Ces éléments ne font pas partie du contrat `0.1.2` et ne doivent être ajoutés qu'après besoin concret :
|
||||
Ces éléments ne sont pas requis par la fondation Config `0.1.3` :
|
||||
|
||||
- plusieurs routes fichier indépendantes ;
|
||||
- rotation par taille, rétention/compression et symlink `latest` ;
|
||||
- formats JSON ou autres formats structurés alternatifs ;
|
||||
- OpenTelemetry/export réseau ;
|
||||
- watcher de fichiers de configuration, qui appartient à Config ou à une couche supérieure ;
|
||||
- benchmark/profiling de précision destiné aux chemins de trading sensibles à la latence.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-logging-lib/USAGE.md -->
|
||||
<!-- version: 2 -->
|
||||
<!-- version: 3 -->
|
||||
|
||||
# Utilisation de ksp-logging-lib
|
||||
|
||||
@@ -21,20 +21,57 @@ ksp_logging_lib::trace!(
|
||||
|
||||
Les champs `domain`, `component`, `operation` et autres champs structurés sont ajoutés par le caller lorsqu'ils sont utiles ; ils ne remplacent pas le target propriétaire.
|
||||
|
||||
## Initialisation
|
||||
## Contrat des outputs
|
||||
|
||||
`initialize` installe le subscriber global KSP une seule fois et retourne le `LoggingGuard` qui doit rester vivant pendant la durée du runtime :
|
||||
`LoggingSettings` possède :
|
||||
|
||||
- le `default_filter` global de takeover KSP ;
|
||||
- les `TargetFilter` globaux ;
|
||||
- la politique `SpanEvents` ;
|
||||
- une console déclarée optionnellement ;
|
||||
- zéro, un ou plusieurs `FileSettings`.
|
||||
|
||||
Chaque output possède un `OutputFilter` indépendant. Son niveau, ses targets et ses domains s'appliquent **en plus** de la politique globale. `*` signifie « tous » dans la dimension concernée.
|
||||
|
||||
Les formats publics sont :
|
||||
|
||||
```text
|
||||
Human
|
||||
Compact
|
||||
Pretty
|
||||
Json
|
||||
```
|
||||
|
||||
Une sortie fichier possède un `output_id` stable et unique dans `LoggingSettings`. Cet identifiant appartient au runtime Logging et ne doit pas être confondu avec les `file_id` utilisés par `ksp-config-lib` pour identifier ses documents.
|
||||
|
||||
Les fichiers persistants interdisent `ansi = true`.
|
||||
|
||||
## Compatibilité runtime de `pre.004`
|
||||
|
||||
`0.1.3-pre.004` étend d'abord le **contrat public**. Le runtime multi-sink/routing est livré séparément en `pre.005`.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Initialisation compatible avec le runtime actuel
|
||||
|
||||
```rust
|
||||
let file = ksp_logging_lib::FileSettings::new(
|
||||
"file.worker",
|
||||
true,
|
||||
"logs",
|
||||
"worker.log",
|
||||
ksp_logging_lib::FileRotation::Daily,
|
||||
ksp_logging_lib::LogFormat::Human,
|
||||
ksp_logging_lib::OutputFilter::unrestricted(),
|
||||
);
|
||||
|
||||
let settings = ksp_logging_lib::LoggingSettings::new(
|
||||
ksp_logging_lib::LogFilterLevel::Info,
|
||||
ksp_logging_lib::SpanEvents::NewAndClose,
|
||||
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
|
||||
std::option::Option::Some(ksp_logging_lib::FileSettings::new(
|
||||
"logs",
|
||||
"worker.log",
|
||||
ksp_logging_lib::FileRotation::Daily,
|
||||
)),
|
||||
std::vec![file],
|
||||
);
|
||||
|
||||
let initialize_result = ksp_logging_lib::initialize(&settings);
|
||||
@@ -44,18 +81,61 @@ let mut logging_guard = match initialize_result {
|
||||
};
|
||||
```
|
||||
|
||||
Une configuration sans console ni fichier est valide et installe une infrastructure initialement silencieuse qui pourra être activée plus tard par hot reload.
|
||||
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
|
||||
|
||||
Le contrat public peut déjà représenter la future configuration Config complète :
|
||||
|
||||
```rust
|
||||
let console = ksp_logging_lib::ConsoleSettings::new(
|
||||
true,
|
||||
ksp_logging_lib::ConsoleOutput::Stderr,
|
||||
true,
|
||||
ksp_logging_lib::LogFormat::Compact,
|
||||
ksp_logging_lib::OutputFilter::new(
|
||||
ksp_logging_lib::LogFilterLevel::Debug,
|
||||
std::vec!["*".to_string()],
|
||||
std::vec!["*".to_string()],
|
||||
),
|
||||
);
|
||||
|
||||
let error_file = ksp_logging_lib::FileSettings::new(
|
||||
"file.config.error",
|
||||
true,
|
||||
"logs/config",
|
||||
"error.jsonl",
|
||||
ksp_logging_lib::FileRotation::Daily,
|
||||
ksp_logging_lib::LogFormat::Json,
|
||||
ksp_logging_lib::OutputFilter::new(
|
||||
ksp_logging_lib::LogFilterLevel::Error,
|
||||
std::vec!["ksp-config-lib".to_string()],
|
||||
std::vec!["config".to_string()],
|
||||
),
|
||||
);
|
||||
|
||||
let settings = ksp_logging_lib::LoggingSettings::new(
|
||||
ksp_logging_lib::LogFilterLevel::Warn,
|
||||
ksp_logging_lib::SpanEvents::NewAndClose,
|
||||
std::option::Option::Some(console),
|
||||
std::vec![error_file],
|
||||
);
|
||||
|
||||
let validation = settings.validate();
|
||||
```
|
||||
|
||||
`validate()` vérifie le contrat structurel. L'activation runtime de ce routage enrichi appartient à `pre.005`.
|
||||
|
||||
## Hot reload
|
||||
|
||||
Une nouvelle configuration peut être appliquée sans redémarrer le processus ou le worker :
|
||||
Une configuration compatible avec les capacités runtime actives peut être appliquée sans redémarrer le processus ou le worker :
|
||||
|
||||
```rust
|
||||
let debug_settings = ksp_logging_lib::LoggingSettings::new(
|
||||
ksp_logging_lib::LogFilterLevel::Info,
|
||||
ksp_logging_lib::SpanEvents::NewAndClose,
|
||||
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
|
||||
std::option::Option::None,
|
||||
std::vec::Vec::new(),
|
||||
)
|
||||
.with_target_filter(ksp_logging_lib::TargetFilter::new(
|
||||
"ksp-store-lib",
|
||||
@@ -104,7 +184,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 pertes restent observables :
|
||||
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 :
|
||||
|
||||
```rust
|
||||
let dropped = logging_guard.dropped_lines();
|
||||
@@ -119,12 +199,4 @@ ksp_logging_lib::warn!(
|
||||
|
||||
## Instrumentation async et executor
|
||||
|
||||
`instrument(span, future)` accepte une `Future` standard et ne dépend d'aucun executor particulier :
|
||||
|
||||
```rust
|
||||
let span = ksp_logging_lib::trace_span!(target: LOGGING_TARGET, "load_transactions");
|
||||
let result = ksp_logging_lib::instrument(span, async_operation()).await;
|
||||
```
|
||||
|
||||
La crate ne requiert pas Tokio en production. Tokio n'est présent qu'en `dev-dependency` pour valider la surface sur un executor réel, y compris après plusieurs suspensions et sur un runtime multi-thread. Un consumer peut donc utiliser l'executor adapté à son propre contexte sans que Logging lui en impose un.
|
||||
|
||||
`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: 4
|
||||
// version: 5
|
||||
#![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. `0.1.2-pre.005` owns the single global subscriber, KSP takeover filtering, hot reload, non-blocking console/file outputs, rolling file
|
||||
//! appenders, ANSI stripping, dropped-line counters and the worker guards required to flush active queues. The integration surface is hardened by
|
||||
//! deterministic saturation, concurrent reload and ownership audits before final release validation.
|
||||
//! `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.
|
||||
|
||||
mod error;
|
||||
mod macros;
|
||||
@@ -44,8 +44,12 @@ pub use self::settings::FileRotation;
|
||||
pub use self::settings::FileSettings;
|
||||
/// Runtime filter level used by KSP logging settings.
|
||||
pub use self::settings::LogFilterLevel;
|
||||
/// Output format requested for one Logging sink.
|
||||
pub use self::settings::LogFormat;
|
||||
/// Complete runtime settings consumed by Logging initialization and reload.
|
||||
pub use self::settings::LoggingSettings;
|
||||
/// Per-output routing filter applied in addition to the global KSP takeover policy.
|
||||
pub use self::settings::OutputFilter;
|
||||
/// Lifecycle events emitted for spans by the formatted subscriber.
|
||||
pub use self::settings::SpanEvents;
|
||||
/// Per-target filter override owned by Logging.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-logging-lib/src/runtime.rs
|
||||
// version: 8
|
||||
// version: 9
|
||||
|
||||
use tracing_subscriber::Layer; // rust-rules: trait-import
|
||||
use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import
|
||||
@@ -165,17 +165,27 @@ fn prepare_runtime(settings: &crate::LoggingSettings) -> ksp_core_lib::Result<Pr
|
||||
if let std::option::Option::Some(error) = validation_error {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if settings.console().is_none() && settings.file().is_none() {
|
||||
let runtime_validation = validate_current_runtime_capabilities(settings);
|
||||
if let std::result::Result::Err(error) = runtime_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let enabled_console = settings.console().filter(|console| -> bool {
|
||||
return console.enabled();
|
||||
});
|
||||
let enabled_file = settings.files().iter().find(|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 settings.file() {
|
||||
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 = settings.console().map(|console| -> PreparedOutput {
|
||||
let prepared_console = enabled_console.map(|console| -> PreparedOutput {
|
||||
return build_console_output(console, settings);
|
||||
});
|
||||
let mut output_layers = RuntimeLayers::new();
|
||||
@@ -193,6 +203,37 @@ fn prepare_runtime(settings: &crate::LoggingSettings) -> ksp_core_lib::Result<Pr
|
||||
return std::result::Result::Ok(PreparedRuntime { layers, 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())
|
||||
{
|
||||
return runtime_capability_error("console ANSI, selectable formats and per-output routing require the multi-sink runtime");
|
||||
}
|
||||
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");
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
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"),
|
||||
);
|
||||
}
|
||||
|
||||
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),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-logging-lib/src/settings.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
/// Runtime filter level used by KSP logging settings.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
@@ -18,7 +18,7 @@ pub enum LogFilterLevel {
|
||||
Trace,
|
||||
}
|
||||
|
||||
/// Per-target filter override owned by Logging.
|
||||
/// Per-target filter override owned by Logging for the global KSP takeover policy.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct TargetFilter {
|
||||
target_prefix: std::string::String,
|
||||
@@ -56,6 +56,74 @@ pub enum SpanEvents {
|
||||
Full,
|
||||
}
|
||||
|
||||
/// Output format requested for one Logging sink.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum LogFormat {
|
||||
/// Standard human-readable formatter with metadata.
|
||||
Human,
|
||||
/// Compact human-readable formatter.
|
||||
Compact,
|
||||
/// Expanded pretty human-readable formatter.
|
||||
Pretty,
|
||||
/// Structured JSON formatter.
|
||||
Json,
|
||||
}
|
||||
|
||||
/// Per-output routing filter applied in addition to the global KSP takeover policy.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct OutputFilter {
|
||||
level: crate::LogFilterLevel,
|
||||
targets: std::vec::Vec<std::string::String>,
|
||||
domains: std::vec::Vec<std::string::String>,
|
||||
}
|
||||
|
||||
impl OutputFilter {
|
||||
/// Creates an explicit per-output filter.
|
||||
///
|
||||
/// `targets` contains KSP target prefixes or the single wildcard `*`. `domains` contains domain prefixes or the single wildcard `*`.
|
||||
#[must_use]
|
||||
pub fn new(level: crate::LogFilterLevel, targets: std::vec::Vec<std::string::String>, domains: std::vec::Vec<std::string::String>) -> Self {
|
||||
return Self { level, targets, domains };
|
||||
}
|
||||
|
||||
/// Creates an unrestricted routing filter that does not further constrain the global KSP takeover policy.
|
||||
#[must_use]
|
||||
pub fn unrestricted() -> Self {
|
||||
return Self::new(crate::LogFilterLevel::Trace, std::vec!["*".to_string()], std::vec!["*".to_string()]);
|
||||
}
|
||||
|
||||
/// Returns the maximum verbosity accepted by this output.
|
||||
#[must_use]
|
||||
pub const fn level(&self) -> crate::LogFilterLevel {
|
||||
return self.level;
|
||||
}
|
||||
|
||||
/// Returns target selectors in declaration order.
|
||||
#[must_use]
|
||||
pub fn targets(&self) -> &[std::string::String] {
|
||||
return self.targets.as_slice();
|
||||
}
|
||||
|
||||
/// Returns domain selectors in declaration order.
|
||||
#[must_use]
|
||||
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.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum ConsoleOutput {
|
||||
@@ -66,22 +134,38 @@ pub enum ConsoleOutput {
|
||||
}
|
||||
|
||||
/// Runtime settings for the optional console output.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ConsoleSettings {
|
||||
enabled: bool,
|
||||
output: crate::ConsoleOutput,
|
||||
ansi: bool,
|
||||
format: crate::LogFormat,
|
||||
filter: crate::OutputFilter,
|
||||
}
|
||||
|
||||
impl ConsoleSettings {
|
||||
/// Creates console settings targeting standard output.
|
||||
/// Creates explicit console settings.
|
||||
#[must_use]
|
||||
pub const fn stdout() -> Self {
|
||||
return Self { output: crate::ConsoleOutput::Stdout };
|
||||
pub fn new(enabled: bool, output: crate::ConsoleOutput, ansi: bool, format: crate::LogFormat, filter: crate::OutputFilter) -> Self {
|
||||
return Self { enabled, output, ansi, format, filter };
|
||||
}
|
||||
|
||||
/// Creates console settings targeting standard error.
|
||||
/// Creates enabled standard-output settings compatible with the `0.1.2` runtime behavior.
|
||||
#[must_use]
|
||||
pub const fn stderr() -> Self {
|
||||
return Self { output: crate::ConsoleOutput::Stderr };
|
||||
pub fn stdout() -> Self {
|
||||
return Self::new(true, crate::ConsoleOutput::Stdout, false, crate::LogFormat::Human, crate::OutputFilter::unrestricted());
|
||||
}
|
||||
|
||||
/// Creates enabled standard-error settings compatible with the `0.1.2` runtime behavior.
|
||||
#[must_use]
|
||||
pub fn stderr() -> Self {
|
||||
return Self::new(true, crate::ConsoleOutput::Stderr, false, crate::LogFormat::Human, crate::OutputFilter::unrestricted());
|
||||
}
|
||||
|
||||
/// Returns whether this console output is enabled.
|
||||
#[must_use]
|
||||
pub const fn enabled(&self) -> bool {
|
||||
return self.enabled;
|
||||
}
|
||||
|
||||
/// Returns the selected console stream.
|
||||
@@ -89,9 +173,27 @@ impl ConsoleSettings {
|
||||
pub const fn output(&self) -> crate::ConsoleOutput {
|
||||
return self.output;
|
||||
}
|
||||
|
||||
/// Returns whether ANSI formatting is requested for the console output.
|
||||
#[must_use]
|
||||
pub const fn ansi(&self) -> bool {
|
||||
return self.ansi;
|
||||
}
|
||||
|
||||
/// Returns the requested console format.
|
||||
#[must_use]
|
||||
pub const fn format(&self) -> crate::LogFormat {
|
||||
return self.format;
|
||||
}
|
||||
|
||||
/// Returns the routing filter associated with the console output.
|
||||
#[must_use]
|
||||
pub const fn filter(&self) -> &crate::OutputFilter {
|
||||
return &self.filter;
|
||||
}
|
||||
}
|
||||
|
||||
/// Rotation cadence for the optional file output.
|
||||
/// Rotation cadence for a file output.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum FileRotation {
|
||||
/// Keeps a single non-rotating file.
|
||||
@@ -102,23 +204,62 @@ pub enum FileRotation {
|
||||
Daily,
|
||||
}
|
||||
|
||||
/// Runtime settings for the optional file output.
|
||||
/// Runtime settings for one file output.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct FileSettings {
|
||||
output_id: std::string::String,
|
||||
enabled: bool,
|
||||
directory: std::path::PathBuf,
|
||||
file_name_prefix: std::string::String,
|
||||
rotation: crate::FileRotation,
|
||||
format: crate::LogFormat,
|
||||
ansi: bool,
|
||||
filter: crate::OutputFilter,
|
||||
}
|
||||
|
||||
impl FileSettings {
|
||||
/// Creates file output settings.
|
||||
/// Creates explicit file output settings.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
output_id: impl std::convert::Into<std::string::String>,
|
||||
enabled: bool,
|
||||
directory: impl std::convert::Into<std::path::PathBuf>,
|
||||
file_name_prefix: impl std::convert::Into<std::string::String>,
|
||||
rotation: crate::FileRotation,
|
||||
format: crate::LogFormat,
|
||||
filter: crate::OutputFilter,
|
||||
) -> Self {
|
||||
return Self { directory: directory.into(), file_name_prefix: file_name_prefix.into(), rotation };
|
||||
return Self {
|
||||
output_id: output_id.into(),
|
||||
enabled,
|
||||
directory: directory.into(),
|
||||
file_name_prefix: file_name_prefix.into(),
|
||||
rotation,
|
||||
format,
|
||||
ansi: false,
|
||||
filter,
|
||||
};
|
||||
}
|
||||
|
||||
/// Sets whether ANSI formatting is requested and returns the updated settings.
|
||||
///
|
||||
/// Persistent file outputs are required to keep this value `false`; [`crate::LoggingSettings::validate`] rejects `true`.
|
||||
#[must_use]
|
||||
pub fn with_ansi(mut self, ansi: bool) -> Self {
|
||||
self.ansi = ansi;
|
||||
return self;
|
||||
}
|
||||
|
||||
/// Returns the stable output identifier used for diagnostics and reload accounting.
|
||||
#[must_use]
|
||||
pub fn output_id(&self) -> &str {
|
||||
return self.output_id.as_str();
|
||||
}
|
||||
|
||||
/// Returns whether this file output is enabled.
|
||||
#[must_use]
|
||||
pub const fn enabled(&self) -> bool {
|
||||
return self.enabled;
|
||||
}
|
||||
|
||||
/// Returns the directory containing log files.
|
||||
@@ -138,6 +279,24 @@ impl FileSettings {
|
||||
pub const fn rotation(&self) -> crate::FileRotation {
|
||||
return self.rotation;
|
||||
}
|
||||
|
||||
/// Returns the requested file format.
|
||||
#[must_use]
|
||||
pub const fn format(&self) -> crate::LogFormat {
|
||||
return self.format;
|
||||
}
|
||||
|
||||
/// Returns whether ANSI formatting was requested for this file output.
|
||||
#[must_use]
|
||||
pub const fn ansi(&self) -> bool {
|
||||
return self.ansi;
|
||||
}
|
||||
|
||||
/// Returns the routing filter associated with this file output.
|
||||
#[must_use]
|
||||
pub const fn filter(&self) -> &crate::OutputFilter {
|
||||
return &self.filter;
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete runtime settings consumed by `ksp-logging-lib` initialization and reload.
|
||||
@@ -147,29 +306,29 @@ pub struct LoggingSettings {
|
||||
target_filters: std::vec::Vec<crate::TargetFilter>,
|
||||
span_events: crate::SpanEvents,
|
||||
console: std::option::Option<crate::ConsoleSettings>,
|
||||
file: std::option::Option<crate::FileSettings>,
|
||||
files: std::vec::Vec<crate::FileSettings>,
|
||||
}
|
||||
|
||||
impl LoggingSettings {
|
||||
/// Creates explicit Logging settings without any target override.
|
||||
/// Creates explicit Logging settings without any global target override.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
default_filter: crate::LogFilterLevel,
|
||||
span_events: crate::SpanEvents,
|
||||
console: std::option::Option<crate::ConsoleSettings>,
|
||||
file: std::option::Option<crate::FileSettings>,
|
||||
files: std::vec::Vec<crate::FileSettings>,
|
||||
) -> Self {
|
||||
return Self { default_filter, target_filters: std::vec::Vec::new(), span_events, console, file };
|
||||
return Self { default_filter, target_filters: std::vec::Vec::new(), span_events, console, files };
|
||||
}
|
||||
|
||||
/// Adds one target-prefix override and returns the updated settings.
|
||||
/// Adds one target-prefix override to the global KSP takeover policy and returns the updated settings.
|
||||
#[must_use]
|
||||
pub fn with_target_filter(mut self, target_filter: crate::TargetFilter) -> Self {
|
||||
self.target_filters.push(target_filter);
|
||||
return self;
|
||||
}
|
||||
|
||||
/// Returns the default level applied to KSP-owned targets.
|
||||
/// Returns the default level applied to KSP-owned targets by the global takeover policy.
|
||||
#[must_use]
|
||||
pub const fn default_filter(&self) -> crate::LogFilterLevel {
|
||||
return self.default_filter;
|
||||
@@ -187,16 +346,16 @@ impl LoggingSettings {
|
||||
return self.span_events;
|
||||
}
|
||||
|
||||
/// Returns console settings when console output is enabled.
|
||||
/// Returns console settings when the console output is declared.
|
||||
#[must_use]
|
||||
pub fn console(&self) -> std::option::Option<&crate::ConsoleSettings> {
|
||||
return self.console.as_ref();
|
||||
}
|
||||
|
||||
/// Returns file settings when file output is enabled.
|
||||
/// Returns all declared file outputs in declaration order.
|
||||
#[must_use]
|
||||
pub fn file(&self) -> std::option::Option<&crate::FileSettings> {
|
||||
return self.file.as_ref();
|
||||
pub fn files(&self) -> &[crate::FileSettings] {
|
||||
return self.files.as_slice();
|
||||
}
|
||||
|
||||
/// Validates backend-independent invariants of the runtime settings.
|
||||
@@ -216,18 +375,142 @@ impl LoggingSettings {
|
||||
);
|
||||
}
|
||||
}
|
||||
if let std::option::Option::Some(file) = self.file.as_ref()
|
||||
&& file.file_name_prefix().is_empty()
|
||||
{
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "file name prefix must not be empty")
|
||||
.with_context("field", "file.file_name_prefix"),
|
||||
);
|
||||
if let std::option::Option::Some(console) = self.console.as_ref() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
for (index, file) in self.files.iter().enumerate() {
|
||||
let output_id_validation = validate_output_id(file.output_id(), index);
|
||||
if let std::result::Result::Err(error) = output_id_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if file.directory().as_os_str().is_empty() {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "file output directory must not be empty")
|
||||
.with_context("field", format!("files[{index}].directory"))
|
||||
.with_context("output_id", file.output_id()),
|
||||
);
|
||||
}
|
||||
if file.file_name_prefix().is_empty() {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "file name prefix must not be empty")
|
||||
.with_context("field", format!("files[{index}].file_name_prefix"))
|
||||
.with_context("output_id", file.output_id()),
|
||||
);
|
||||
}
|
||||
if file.ansi() {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "ANSI sequences are not allowed in persistent file outputs")
|
||||
.with_context("field", format!("files[{index}].ansi"))
|
||||
.with_context("output_id", file.output_id()),
|
||||
);
|
||||
}
|
||||
let filter_validation = validate_output_filter(file.filter(), format!("files[{index}].filter").as_str());
|
||||
if let std::result::Result::Err(error) = filter_validation {
|
||||
return std::result::Result::Err(error.with_context("output_id", file.output_id()));
|
||||
}
|
||||
for previous in &self.files[..index] {
|
||||
if previous.output_id() == file.output_id() {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "file output identifiers must be unique")
|
||||
.with_context("field", format!("files[{index}].output_id"))
|
||||
.with_context("output_id", file.output_id()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_output_id(output_id: &str, index: usize) -> ksp_core_lib::Result<()> {
|
||||
let mut previous_was_separator = true;
|
||||
if output_id.is_empty() {
|
||||
return invalid_output_id(output_id, index);
|
||||
}
|
||||
for byte in output_id.bytes() {
|
||||
if byte == b'.' {
|
||||
if previous_was_separator {
|
||||
return invalid_output_id(output_id, index);
|
||||
}
|
||||
previous_was_separator = true;
|
||||
continue;
|
||||
}
|
||||
if !(byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_' || byte == b'-') {
|
||||
return invalid_output_id(output_id, index);
|
||||
}
|
||||
previous_was_separator = false;
|
||||
}
|
||||
if previous_was_separator {
|
||||
return invalid_output_id(output_id, index);
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn invalid_output_id(output_id: &str, index: usize) -> ksp_core_lib::Result<()> {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "file output identifier is invalid")
|
||||
.with_context("field", format!("files[{index}].output_id"))
|
||||
.with_context("output_id", output_id),
|
||||
);
|
||||
}
|
||||
|
||||
fn validate_output_filter(filter: &crate::OutputFilter, field: &str) -> ksp_core_lib::Result<()> {
|
||||
let target_validation = validate_selectors(filter.targets(), true, format!("{field}.targets").as_str());
|
||||
if let std::result::Result::Err(error) = target_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let domain_validation = validate_selectors(filter.domains(), false, format!("{field}.domains").as_str());
|
||||
if let std::result::Result::Err(error) = domain_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn validate_selectors(selectors: &[std::string::String], target_dimension: bool, field: &str) -> ksp_core_lib::Result<()> {
|
||||
if selectors.is_empty() {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "output selector list must not be empty").with_context("field", field),
|
||||
);
|
||||
}
|
||||
if selectors.len() > 1
|
||||
&& selectors.iter().any(|selector| -> bool {
|
||||
return selector == "*";
|
||||
})
|
||||
{
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "wildcard selector must be used alone").with_context("field", field),
|
||||
);
|
||||
}
|
||||
for (index, selector) in selectors.iter().enumerate() {
|
||||
if selector.trim().is_empty() {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "output selector must not be empty")
|
||||
.with_context("field", format!("{field}[{index}]")),
|
||||
);
|
||||
}
|
||||
if target_dimension && selector != "*" && !selector.starts_with("ksp-") {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "output target selector must identify a KSP-owned target")
|
||||
.with_context("field", format!("{field}[{index}]"))
|
||||
.with_context("selector", selector),
|
||||
);
|
||||
}
|
||||
for previous in &selectors[..index] {
|
||||
if previous == selector {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "output selectors must be unique")
|
||||
.with_context("field", format!("{field}[{index}]"))
|
||||
.with_context("selector", selector),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/settings.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-logging-lib/tests/public_api.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
//! Integration tests for the public crate-root surface of `ksp-logging-lib`.
|
||||
|
||||
@@ -7,16 +7,41 @@ const TEST_TARGET: &str = "ksp-logging-lib";
|
||||
|
||||
#[test]
|
||||
fn public_settings_surface_is_usable() {
|
||||
let console_filter =
|
||||
ksp_logging_lib::OutputFilter::new(ksp_logging_lib::LogFilterLevel::Debug, std::vec![TEST_TARGET.to_string()], std::vec!["logging".to_string()]);
|
||||
let file_filter = ksp_logging_lib::OutputFilter::new(
|
||||
ksp_logging_lib::LogFilterLevel::Error,
|
||||
std::vec![TEST_TARGET.to_string()],
|
||||
std::vec!["logging.runtime".to_string()],
|
||||
);
|
||||
let settings = ksp_logging_lib::LoggingSettings::new(
|
||||
ksp_logging_lib::LogFilterLevel::Info,
|
||||
ksp_logging_lib::SpanEvents::NewAndClose,
|
||||
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stdout()),
|
||||
std::option::Option::Some(ksp_logging_lib::FileSettings::new("logs", "ksp", ksp_logging_lib::FileRotation::Daily)),
|
||||
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::new(
|
||||
true,
|
||||
ksp_logging_lib::ConsoleOutput::Stdout,
|
||||
true,
|
||||
ksp_logging_lib::LogFormat::Compact,
|
||||
console_filter,
|
||||
)),
|
||||
std::vec![ksp_logging_lib::FileSettings::new(
|
||||
"file.logging.error",
|
||||
true,
|
||||
"logs",
|
||||
"error.jsonl",
|
||||
ksp_logging_lib::FileRotation::Daily,
|
||||
ksp_logging_lib::LogFormat::Json,
|
||||
file_filter,
|
||||
)],
|
||||
)
|
||||
.with_target_filter(ksp_logging_lib::TargetFilter::new(TEST_TARGET, ksp_logging_lib::LogFilterLevel::Trace));
|
||||
assert!(settings.validate().is_ok());
|
||||
assert_eq!(settings.default_filter(), ksp_logging_lib::LogFilterLevel::Info);
|
||||
assert_eq!(settings.console().map(ksp_logging_lib::ConsoleSettings::output), std::option::Option::Some(ksp_logging_lib::ConsoleOutput::Stdout));
|
||||
assert_eq!(settings.console().map(ksp_logging_lib::ConsoleSettings::format), std::option::Option::Some(ksp_logging_lib::LogFormat::Compact));
|
||||
assert_eq!(settings.files().len(), 1);
|
||||
assert_eq!(settings.files()[0].output_id(), "file.logging.error");
|
||||
assert_eq!(settings.files()[0].format(), ksp_logging_lib::LogFormat::Json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-logging-lib/tests/runtime.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
//! Integration tests for global initialization, takeover filtering, non-blocking outputs and hot reload.
|
||||
|
||||
@@ -67,7 +67,7 @@ fn exercise_concurrent_reload(guard: &mut ksp_logging_lib::LoggingGuard, disable
|
||||
ksp_logging_lib::LogFilterLevel::Off,
|
||||
ksp_logging_lib::SpanEvents::Off,
|
||||
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
|
||||
std::option::Option::None,
|
||||
std::vec::Vec::new(),
|
||||
);
|
||||
let quiet_reload = ksp_logging_lib::reinitialize(guard, &quiet_console);
|
||||
assert!(quiet_reload.is_ok());
|
||||
@@ -116,7 +116,7 @@ fn global_runtime_supports_takeover_non_blocking_outputs_hot_reload_and_single_i
|
||||
ksp_logging_lib::LogFilterLevel::Info,
|
||||
ksp_logging_lib::SpanEvents::Off,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
std::vec::Vec::new(),
|
||||
);
|
||||
let initialize_result = ksp_logging_lib::initialize(&disabled);
|
||||
assert!(initialize_result.is_ok());
|
||||
@@ -132,7 +132,7 @@ fn global_runtime_supports_takeover_non_blocking_outputs_hot_reload_and_single_i
|
||||
ksp_logging_lib::LogFilterLevel::Info,
|
||||
ksp_logging_lib::SpanEvents::NewAndClose,
|
||||
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
|
||||
std::option::Option::None,
|
||||
std::vec::Vec::new(),
|
||||
)
|
||||
.with_target_filter(ksp_logging_lib::TargetFilter::new(LOGGING_TARGET, ksp_logging_lib::LogFilterLevel::Trace));
|
||||
let reload_result = ksp_logging_lib::reinitialize(&mut guard, &console_enabled);
|
||||
@@ -151,7 +151,15 @@ fn global_runtime_supports_takeover_non_blocking_outputs_hot_reload_and_single_i
|
||||
ksp_logging_lib::LogFilterLevel::Error,
|
||||
ksp_logging_lib::SpanEvents::Full,
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(ksp_logging_lib::FileSettings::new(blocked_directory.as_path(), "invalid", ksp_logging_lib::FileRotation::Daily)),
|
||||
std::vec![ksp_logging_lib::FileSettings::new(
|
||||
"file.invalid",
|
||||
true,
|
||||
blocked_directory.as_path(),
|
||||
"invalid",
|
||||
ksp_logging_lib::FileRotation::Daily,
|
||||
ksp_logging_lib::LogFormat::Human,
|
||||
ksp_logging_lib::OutputFilter::unrestricted(),
|
||||
)],
|
||||
);
|
||||
let failed_reload = ksp_logging_lib::reinitialize(&mut guard, &invalid_file);
|
||||
assert!(failed_reload.is_err());
|
||||
@@ -169,7 +177,15 @@ fn global_runtime_supports_takeover_non_blocking_outputs_hot_reload_and_single_i
|
||||
ksp_logging_lib::LogFilterLevel::Info,
|
||||
ksp_logging_lib::SpanEvents::Off,
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(ksp_logging_lib::FileSettings::new(log_directory.as_path(), "runtime-test.log", ksp_logging_lib::FileRotation::Never)),
|
||||
std::vec![ksp_logging_lib::FileSettings::new(
|
||||
"file.runtime",
|
||||
true,
|
||||
log_directory.as_path(),
|
||||
"runtime-test.log",
|
||||
ksp_logging_lib::FileRotation::Never,
|
||||
ksp_logging_lib::LogFormat::Human,
|
||||
ksp_logging_lib::OutputFilter::unrestricted(),
|
||||
)],
|
||||
);
|
||||
let file_reload = ksp_logging_lib::reinitialize(&mut guard, &file_enabled);
|
||||
assert!(file_reload.is_ok());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-logging-lib/unit_tests/runtime.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
#[test]
|
||||
fn level_mapping_covers_all_ksp_levels() {
|
||||
@@ -17,7 +17,7 @@ fn takeover_filter_silences_external_targets_and_applies_ksp_overrides() {
|
||||
crate::LogFilterLevel::Info,
|
||||
crate::SpanEvents::Off,
|
||||
std::option::Option::Some(crate::ConsoleSettings::stdout()),
|
||||
std::option::Option::None,
|
||||
std::vec::Vec::new(),
|
||||
)
|
||||
.with_target_filter(crate::TargetFilter::new("ksp-logging-lib", crate::LogFilterLevel::Trace));
|
||||
let filter = super::build_target_filter(&settings);
|
||||
@@ -47,7 +47,7 @@ fn file_rotation_mapping_covers_supported_cadences() {
|
||||
|
||||
#[test]
|
||||
fn disabled_runtime_has_no_layers_or_outputs() {
|
||||
let settings = crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::option::Option::None);
|
||||
let settings = crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::vec::Vec::new());
|
||||
let result = super::prepare_runtime(&settings);
|
||||
assert!(result.is_ok());
|
||||
let prepared = match result {
|
||||
@@ -65,7 +65,7 @@ fn console_runtime_composes_takeover_filter_before_formatter_and_owns_guard() {
|
||||
crate::LogFilterLevel::Info,
|
||||
crate::SpanEvents::Off,
|
||||
std::option::Option::Some(crate::ConsoleSettings::stdout()),
|
||||
std::option::Option::None,
|
||||
std::vec::Vec::new(),
|
||||
);
|
||||
let result = super::prepare_runtime(&settings);
|
||||
assert!(result.is_ok());
|
||||
@@ -91,7 +91,7 @@ fn dropped_line_snapshots_add_saturating_by_sink() {
|
||||
|
||||
#[test]
|
||||
fn takeover_filter_prefers_more_specific_ksp_prefixes_and_supports_off() {
|
||||
let settings = crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::option::Option::None)
|
||||
let settings = crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::vec::Vec::new())
|
||||
.with_target_filter(crate::TargetFilter::new("ksp-store-", crate::LogFilterLevel::Debug))
|
||||
.with_target_filter(crate::TargetFilter::new("ksp-store-lib", crate::LogFilterLevel::Trace))
|
||||
.with_target_filter(crate::TargetFilter::new("ksp-wallet-lib", crate::LogFilterLevel::Off));
|
||||
@@ -102,6 +102,43 @@ fn takeover_filter_prefers_more_specific_ksp_prefixes_and_supports_off() {
|
||||
assert!(!filter.would_enable("ksp-wallet-lib", &tracing::Level::ERROR));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_multi_sink_runtime_rejects_new_capabilities_instead_of_ignoring_them() {
|
||||
let routed_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()]),
|
||||
);
|
||||
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",
|
||||
"first.log",
|
||||
crate::FileRotation::Never,
|
||||
crate::LogFormat::Human,
|
||||
crate::OutputFilter::unrestricted(),
|
||||
);
|
||||
let second_file = crate::FileSettings::new(
|
||||
"file.second",
|
||||
true,
|
||||
"logs",
|
||||
"second.log",
|
||||
crate::FileRotation::Never,
|
||||
crate::LogFormat::Human,
|
||||
crate::OutputFilter::unrestricted(),
|
||||
);
|
||||
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());
|
||||
}
|
||||
|
||||
struct BlockingWriter {
|
||||
first_write: bool,
|
||||
started: std::sync::mpsc::SyncSender<()>,
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
// file: crates/ksp-logging-lib/unit_tests/settings.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
fn unrestricted_file(output_id: &str) -> crate::FileSettings {
|
||||
return crate::FileSettings::new(
|
||||
output_id,
|
||||
true,
|
||||
"logs",
|
||||
format!("{output_id}.log"),
|
||||
crate::FileRotation::Daily,
|
||||
crate::LogFormat::Human,
|
||||
crate::OutputFilter::unrestricted(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn level_variants_are_distinct() {
|
||||
@@ -10,6 +22,13 @@ fn level_variants_are_distinct() {
|
||||
assert_ne!(crate::LogFilterLevel::Debug, crate::LogFilterLevel::Trace);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_variants_are_distinct() {
|
||||
assert_ne!(crate::LogFormat::Human, crate::LogFormat::Compact);
|
||||
assert_ne!(crate::LogFormat::Compact, crate::LogFormat::Pretty);
|
||||
assert_ne!(crate::LogFormat::Pretty, crate::LogFormat::Json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_filter_preserves_prefix_and_level() {
|
||||
let filter = crate::TargetFilter::new("ksp-store-lib", crate::LogFilterLevel::Trace);
|
||||
@@ -18,85 +37,188 @@ fn target_filter_preserves_prefix_and_level() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn console_settings_select_requested_stream() {
|
||||
assert_eq!(crate::ConsoleSettings::stdout().output(), crate::ConsoleOutput::Stdout);
|
||||
assert_eq!(crate::ConsoleSettings::stderr().output(), crate::ConsoleOutput::Stderr);
|
||||
fn output_filter_preserves_level_targets_and_domains() {
|
||||
let filter = crate::OutputFilter::new(
|
||||
crate::LogFilterLevel::Debug,
|
||||
std::vec!["ksp-config-lib".to_string(), "ksp-logging-lib".to_string()],
|
||||
std::vec!["config".to_string(), "logging.runtime".to_string()],
|
||||
);
|
||||
assert_eq!(filter.level(), crate::LogFilterLevel::Debug);
|
||||
assert_eq!(filter.targets().len(), 2);
|
||||
assert_eq!(filter.targets()[0], "ksp-config-lib");
|
||||
assert_eq!(filter.targets()[1], "ksp-logging-lib");
|
||||
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());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_settings_preserve_values() {
|
||||
let settings = crate::FileSettings::new("logs", "ksp", crate::FileRotation::Daily);
|
||||
assert_eq!(settings.directory(), std::path::Path::new("logs"));
|
||||
assert_eq!(settings.file_name_prefix(), "ksp");
|
||||
fn console_settings_preserve_enabled_stream_ansi_format_and_filter() {
|
||||
let filter = crate::OutputFilter::new(crate::LogFilterLevel::Debug, std::vec!["*".to_string()], std::vec!["config".to_string()]);
|
||||
let settings = crate::ConsoleSettings::new(true, crate::ConsoleOutput::Stderr, true, crate::LogFormat::Compact, filter.clone());
|
||||
assert!(settings.enabled());
|
||||
assert_eq!(settings.output(), crate::ConsoleOutput::Stderr);
|
||||
assert!(settings.ansi());
|
||||
assert_eq!(settings.format(), crate::LogFormat::Compact);
|
||||
assert_eq!(settings.filter(), &filter);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compatibility_console_constructors_are_unrestricted_human_and_non_ansi() {
|
||||
let stdout = crate::ConsoleSettings::stdout();
|
||||
let stderr = crate::ConsoleSettings::stderr();
|
||||
assert_eq!(stdout.output(), crate::ConsoleOutput::Stdout);
|
||||
assert_eq!(stderr.output(), crate::ConsoleOutput::Stderr);
|
||||
assert!(stdout.enabled());
|
||||
assert!(!stdout.ansi());
|
||||
assert_eq!(stdout.format(), crate::LogFormat::Human);
|
||||
assert!(stdout.filter().is_unrestricted());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_settings_preserve_multi_output_contract() {
|
||||
let filter = crate::OutputFilter::new(crate::LogFilterLevel::Error, std::vec!["ksp-config-lib".to_string()], std::vec!["config".to_string()]);
|
||||
let settings =
|
||||
crate::FileSettings::new("file.config.error", true, "logs/config", "error.jsonl", crate::FileRotation::Daily, crate::LogFormat::Json, filter.clone());
|
||||
assert_eq!(settings.output_id(), "file.config.error");
|
||||
assert!(settings.enabled());
|
||||
assert_eq!(settings.directory(), std::path::Path::new("logs/config"));
|
||||
assert_eq!(settings.file_name_prefix(), "error.jsonl");
|
||||
assert_eq!(settings.rotation(), crate::FileRotation::Daily);
|
||||
assert_eq!(settings.format(), crate::LogFormat::Json);
|
||||
assert!(!settings.ansi());
|
||||
assert_eq!(settings.filter(), &filter);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logging_settings_preserve_explicit_values() {
|
||||
fn logging_settings_represent_multiple_file_outputs() {
|
||||
let first = unrestricted_file("file.debug");
|
||||
let second = crate::FileSettings::new(
|
||||
"file.config.error",
|
||||
false,
|
||||
"logs/config",
|
||||
"error.jsonl",
|
||||
crate::FileRotation::Hourly,
|
||||
crate::LogFormat::Json,
|
||||
crate::OutputFilter::new(crate::LogFilterLevel::Error, std::vec!["ksp-config-lib".to_string()], std::vec!["config".to_string()]),
|
||||
);
|
||||
let settings = crate::LoggingSettings::new(
|
||||
crate::LogFilterLevel::Info,
|
||||
crate::SpanEvents::NewAndClose,
|
||||
std::option::Option::Some(crate::ConsoleSettings::stdout()),
|
||||
std::option::Option::Some(crate::FileSettings::new("logs", "ksp", crate::FileRotation::Hourly)),
|
||||
std::vec![first, second],
|
||||
)
|
||||
.with_target_filter(crate::TargetFilter::new("ksp-store-lib", crate::LogFilterLevel::Trace));
|
||||
assert_eq!(settings.default_filter(), crate::LogFilterLevel::Info);
|
||||
assert_eq!(settings.span_events(), crate::SpanEvents::NewAndClose);
|
||||
assert_eq!(settings.target_filters().len(), 1);
|
||||
assert_eq!(settings.target_filters()[0].target_prefix(), "ksp-store-lib");
|
||||
assert_eq!(settings.console(), std::option::Option::Some(&crate::ConsoleSettings::stdout()));
|
||||
assert_eq!(settings.file().map(crate::FileSettings::rotation), std::option::Option::Some(crate::FileRotation::Hourly));
|
||||
assert_eq!(settings.files().len(), 2);
|
||||
assert_eq!(settings.files()[0].output_id(), "file.debug");
|
||||
assert_eq!(settings.files()[1].format(), crate::LogFormat::Json);
|
||||
assert_eq!(settings.console().map(crate::ConsoleSettings::output), std::option::Option::Some(crate::ConsoleOutput::Stdout));
|
||||
assert!(settings.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_rejects_empty_target_prefix() {
|
||||
let settings = crate::LoggingSettings::new(
|
||||
crate::LogFilterLevel::Info,
|
||||
crate::SpanEvents::Off,
|
||||
std::option::Option::Some(crate::ConsoleSettings::stdout()),
|
||||
std::option::Option::None,
|
||||
)
|
||||
.with_target_filter(crate::TargetFilter::new("", crate::LogFilterLevel::Debug));
|
||||
assert!(settings.validate().is_err());
|
||||
fn validation_rejects_empty_or_external_global_target_prefix() {
|
||||
let empty = crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::vec::Vec::new())
|
||||
.with_target_filter(crate::TargetFilter::new("", crate::LogFilterLevel::Debug));
|
||||
let external = crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::vec::Vec::new())
|
||||
.with_target_filter(crate::TargetFilter::new("sqlx", crate::LogFilterLevel::Debug));
|
||||
assert!(empty.validate().is_err());
|
||||
assert!(external.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_rejects_external_target_prefix() {
|
||||
let settings = crate::LoggingSettings::new(
|
||||
crate::LogFilterLevel::Info,
|
||||
crate::SpanEvents::Off,
|
||||
std::option::Option::Some(crate::ConsoleSettings::stdout()),
|
||||
std::option::Option::None,
|
||||
)
|
||||
.with_target_filter(crate::TargetFilter::new("sqlx", crate::LogFilterLevel::Debug));
|
||||
assert!(settings.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_rejects_empty_file_prefix() {
|
||||
let settings = crate::LoggingSettings::new(
|
||||
fn validation_rejects_invalid_file_identity_and_duplicate_output_ids() {
|
||||
let invalid_id =
|
||||
crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::vec![unrestricted_file("File.Debug")]);
|
||||
let duplicate_id = crate::LoggingSettings::new(
|
||||
crate::LogFilterLevel::Info,
|
||||
crate::SpanEvents::Off,
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(crate::FileSettings::new("logs", "", crate::FileRotation::Never)),
|
||||
std::vec![unrestricted_file("file.debug"), unrestricted_file("file.debug")],
|
||||
);
|
||||
assert!(settings.validate().is_err());
|
||||
assert!(invalid_id.validate().is_err());
|
||||
assert!(duplicate_id.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_accepts_ksp_outputs_and_filters() {
|
||||
let settings = crate::LoggingSettings::new(
|
||||
crate::LogFilterLevel::Info,
|
||||
crate::SpanEvents::Full,
|
||||
std::option::Option::Some(crate::ConsoleSettings::stderr()),
|
||||
std::option::Option::Some(crate::FileSettings::new("logs", "worker", crate::FileRotation::Daily)),
|
||||
fn validation_rejects_empty_file_paths_and_persistent_ansi() {
|
||||
let empty_directory = crate::FileSettings::new(
|
||||
"file.empty.dir",
|
||||
true,
|
||||
"",
|
||||
"debug.log",
|
||||
crate::FileRotation::Never,
|
||||
crate::LogFormat::Human,
|
||||
crate::OutputFilter::unrestricted(),
|
||||
);
|
||||
let empty_prefix = crate::FileSettings::new(
|
||||
"file.empty.prefix",
|
||||
true,
|
||||
"logs",
|
||||
"",
|
||||
crate::FileRotation::Never,
|
||||
crate::LogFormat::Human,
|
||||
crate::OutputFilter::unrestricted(),
|
||||
);
|
||||
let ansi_file = crate::FileSettings::new(
|
||||
"file.ansi",
|
||||
true,
|
||||
"logs",
|
||||
"ansi.log",
|
||||
crate::FileRotation::Never,
|
||||
crate::LogFormat::Human,
|
||||
crate::OutputFilter::unrestricted(),
|
||||
)
|
||||
.with_target_filter(crate::TargetFilter::new("ksp-worker-", crate::LogFilterLevel::Debug));
|
||||
assert!(settings.validate().is_ok());
|
||||
.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()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_allow_logging_to_be_disabled() {
|
||||
let settings = crate::LoggingSettings::new(crate::LogFilterLevel::Off, crate::SpanEvents::Off, std::option::Option::None, std::option::Option::None);
|
||||
fn validation_rejects_invalid_output_selectors() {
|
||||
let external_target = crate::OutputFilter::new(crate::LogFilterLevel::Debug, std::vec!["sqlx".to_string()], std::vec!["*".to_string()]);
|
||||
let wildcard_mix =
|
||||
crate::OutputFilter::new(crate::LogFilterLevel::Debug, std::vec!["*".to_string(), "ksp-config-lib".to_string()], std::vec!["*".to_string()]);
|
||||
let duplicate_domain =
|
||||
crate::OutputFilter::new(crate::LogFilterLevel::Debug, std::vec!["*".to_string()], std::vec!["config".to_string(), "config".to_string()]);
|
||||
for filter in [external_target, wildcard_mix, duplicate_domain] {
|
||||
let console = crate::ConsoleSettings::new(true, crate::ConsoleOutput::Stdout, false, crate::LogFormat::Human, filter);
|
||||
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]
|
||||
fn settings_allow_all_outputs_to_be_disabled() {
|
||||
let console = crate::ConsoleSettings::new(false, crate::ConsoleOutput::Stderr, true, crate::LogFormat::Pretty, crate::OutputFilter::unrestricted());
|
||||
let file = crate::FileSettings::new(
|
||||
"file.disabled",
|
||||
false,
|
||||
"logs",
|
||||
"disabled.jsonl",
|
||||
crate::FileRotation::Daily,
|
||||
crate::LogFormat::Json,
|
||||
crate::OutputFilter::new(crate::LogFilterLevel::Error, std::vec!["ksp-config-lib".to_string()], std::vec!["config".to_string()]),
|
||||
);
|
||||
let settings = crate::LoggingSettings::new(crate::LogFilterLevel::Off, crate::SpanEvents::Off, std::option::Option::Some(console), std::vec![file]);
|
||||
assert!(settings.validate().is_ok());
|
||||
}
|
||||
|
||||
337
deltas/0.1.3/pre.004.md
Normal file
337
deltas/0.1.3/pre.004.md
Normal file
@@ -0,0 +1,337 @@
|
||||
<!-- file: deltas/0.1.3/pre.004.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta 0.1.3-pre.004
|
||||
|
||||
## Base requise
|
||||
|
||||
Livraison précédente validée :
|
||||
|
||||
```text
|
||||
0.1.3-pre.003
|
||||
```
|
||||
|
||||
Version technique de cette base :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.3-pre.3"
|
||||
Cargo.toml header version = 43
|
||||
```
|
||||
|
||||
Validations utilisateur de `pre.003` :
|
||||
|
||||
```text
|
||||
cargo fmt --all OK
|
||||
cargo check --workspace OK
|
||||
cargo clippy --workspace --all-targets OK
|
||||
cargo test --workspace OK
|
||||
```
|
||||
|
||||
Les tests Config livrés en `pre.003` réussissent : 21 tests unitaires et 4 tests d'API publique.
|
||||
|
||||
## Objet de pre.004
|
||||
|
||||
Cette tranche complète uniquement les **contrats/settings publics** de `ksp-logging-lib` nécessaires au futur `config/std.logging.json`.
|
||||
|
||||
Elle ne livre pas encore le runtime multi-sink complet. Le but est de stabiliser ce que Logging sait représenter avant de figer le schema JSON Config.
|
||||
|
||||
## Modèle public ajouté
|
||||
|
||||
### `LogFormat`
|
||||
|
||||
Formats publics représentables :
|
||||
|
||||
```text
|
||||
Human
|
||||
Compact
|
||||
Pretty
|
||||
Json
|
||||
```
|
||||
|
||||
L'activation backend de tous ces formats appartient à la tranche runtime suivante.
|
||||
|
||||
### `OutputFilter`
|
||||
|
||||
Chaque sink peut maintenant déclarer un filtre propre contenant :
|
||||
|
||||
```text
|
||||
level
|
||||
targets[]
|
||||
domains[]
|
||||
```
|
||||
|
||||
Les selectors `targets` sont des préfixes de targets KSP ou le wildcard `*`.
|
||||
|
||||
Les selectors `domains` sont des préfixes de domain ou le wildcard `*`.
|
||||
|
||||
Le wildcard doit être utilisé seul dans sa dimension. Les listes vides, selectors vides, doublons et targets externes à KSP sont refusés par la validation publique.
|
||||
|
||||
`OutputFilter::unrestricted()` représente l'absence de restriction supplémentaire par rapport au takeover global :
|
||||
|
||||
```text
|
||||
level = Trace
|
||||
targets = ["*"]
|
||||
domains = ["*"]
|
||||
```
|
||||
|
||||
## Console explicite
|
||||
|
||||
`ConsoleSettings` représente désormais explicitement :
|
||||
|
||||
```text
|
||||
enabled
|
||||
output = stdout | stderr
|
||||
ansi
|
||||
format
|
||||
filter
|
||||
```
|
||||
|
||||
Les helpers :
|
||||
|
||||
```text
|
||||
ConsoleSettings::stdout()
|
||||
ConsoleSettings::stderr()
|
||||
```
|
||||
|
||||
restent disponibles et produisent un contrat compatible avec le runtime `0.1.2` : console activée, format `Human`, ANSI désactivé et `OutputFilter::unrestricted()`.
|
||||
|
||||
## Plusieurs outputs fichier
|
||||
|
||||
`LoggingSettings` ne contient plus un unique :
|
||||
|
||||
```text
|
||||
Option<FileSettings>
|
||||
```
|
||||
|
||||
mais :
|
||||
|
||||
```text
|
||||
Vec<FileSettings>
|
||||
```
|
||||
|
||||
Chaque `FileSettings` porte :
|
||||
|
||||
```text
|
||||
output_id
|
||||
enabled
|
||||
directory
|
||||
file_name_prefix
|
||||
rotation
|
||||
format
|
||||
ansi
|
||||
filter
|
||||
```
|
||||
|
||||
`output_id` est une identité Logging stable, unique parmi les fichiers d'un même `LoggingSettings`.
|
||||
|
||||
Il ne doit pas être confondu avec le `file_id` de `ksp-config-lib` :
|
||||
|
||||
```text
|
||||
file_id -> identité d'un document/schema Config
|
||||
output_id -> identité d'une sortie Logging
|
||||
```
|
||||
|
||||
La syntaxe initiale d'un `output_id` accepte l'ASCII minuscule, chiffres, `_`, `-` et des segments séparés par `.` sans segment vide.
|
||||
|
||||
Exemples :
|
||||
|
||||
```text
|
||||
file.debug
|
||||
file.config.error
|
||||
file.ksp-config-lib.error
|
||||
```
|
||||
|
||||
## ANSI fichier
|
||||
|
||||
Une sortie persistante avec :
|
||||
|
||||
```text
|
||||
ansi = true
|
||||
```
|
||||
|
||||
est refusée par `LoggingSettings::validate()`.
|
||||
|
||||
Le contrat KSP conserve donc l'invariant qu'un fichier de logs ne persiste pas de séquences ANSI.
|
||||
|
||||
## Deux niveaux de filtrage conservés
|
||||
|
||||
Le nouveau filtre par sink ne remplace pas la politique existante.
|
||||
|
||||
`LoggingSettings` conserve :
|
||||
|
||||
```text
|
||||
default_filter
|
||||
target_filters[]
|
||||
```
|
||||
|
||||
qui définissent le takeover global KSP.
|
||||
|
||||
Puis chaque console/fichier possède son `OutputFilter`, appliqué conceptuellement **en plus** du takeover global.
|
||||
|
||||
Cette séparation permet de conserver les overrides généraux tout en préparant un routing indépendant par sink, target, domain et niveau.
|
||||
|
||||
## Compatibilité runtime transitoire
|
||||
|
||||
Cette tranche ne doit jamais accepter silencieusement un contrat que le runtime `0.1.2` ne sait pas encore appliquer.
|
||||
|
||||
Après la validation structurelle, `initialize/reinitialize` refusent donc temporairement :
|
||||
|
||||
- plusieurs fichiers **actifs** simultanément ;
|
||||
- ANSI console ;
|
||||
- format console différent de `Human` ;
|
||||
- format fichier différent de `Human` ;
|
||||
- filtre propre à un output différent de `OutputFilter::unrestricted()`.
|
||||
|
||||
Les outputs désactivés peuvent déjà porter leur future configuration sans influencer le runtime actif.
|
||||
|
||||
Le refus utilise actuellement `logging.invalid_settings` avec un contexte `runtime_contract = single-output-compatibility`.
|
||||
|
||||
Ce garde-fou transitoire sera supprimé/remplacé lorsque `pre.005` implémentera réellement les capacités représentées.
|
||||
|
||||
## Lifecycle Logging préservé
|
||||
|
||||
Cette tranche ne modifie pas les responsabilités suivantes :
|
||||
|
||||
```text
|
||||
initialize(...)
|
||||
reinitialize(...)
|
||||
LoggingGuard
|
||||
subscriber global unique
|
||||
takeover KSP
|
||||
hot reload transactionnel
|
||||
writers non bloquants
|
||||
DroppedLines agrégé console/fichier
|
||||
```
|
||||
|
||||
`DroppedLines` reste temporairement agrégé par type de sink ; sa généralisation éventuelle par `output_id` appartient à `pre.005`.
|
||||
|
||||
## Frontière Config préservée
|
||||
|
||||
Aucune dépendance inverse n'est introduite :
|
||||
|
||||
```text
|
||||
ksp-logging-lib -X-> ksp-config-lib
|
||||
```
|
||||
|
||||
`ksp-logging-lib` ne lit toujours aucun fichier JSON, aucun profil, aucun `.env` et aucune variable applicative.
|
||||
|
||||
Le futur `ksp-config-lib` construira ces contrats publics après résolution de `std.logging.json`.
|
||||
|
||||
## Dépendances
|
||||
|
||||
Aucune dépendance externe ou KSP supplémentaire n'est ajoutée.
|
||||
|
||||
Le graphe direct de `ksp-config-lib` reste inchangé dans cette tranche.
|
||||
|
||||
## Tests modifiés/ajoutés
|
||||
|
||||
Les tests settings couvrent notamment :
|
||||
|
||||
- les quatre formats ;
|
||||
- niveau/targets/domains de `OutputFilter` ;
|
||||
- wildcard unrestricted ;
|
||||
- console enabled/output/ANSI/format/filter ;
|
||||
- plusieurs `FileSettings` ;
|
||||
- unicité et syntaxe des `output_id` ;
|
||||
- fichiers sans ANSI ;
|
||||
- selectors invalides, doublons et targets externes ;
|
||||
- outputs déclarés mais désactivés.
|
||||
|
||||
Les tests runtime couvrent également le garde-fou transitoire :
|
||||
|
||||
- une console utilisant ANSI/format/routing enrichi est structurellement valide mais refusée par le backend courant ;
|
||||
- plusieurs fichiers actifs sont structurellement valides mais refusés par le backend courant ;
|
||||
- les contrats historiques compatibles continuent d'être préparés normalement.
|
||||
|
||||
Le test d'intégration d'API publique construit un contrat avec console enrichie et fichier JSON filtré afin de vérifier que la nouvelle surface est bien accessible depuis le crate-root.
|
||||
|
||||
## Documentation Logging
|
||||
|
||||
`README.md`, `USAGE.md` et `TODO.md` sont alignés sur la séparation :
|
||||
|
||||
```text
|
||||
pre.004 = contrat public multi-output
|
||||
pre.005 = activation runtime multi-sink/routing
|
||||
```
|
||||
|
||||
## Version technique
|
||||
|
||||
La prerelease devient :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.3-pre.4"
|
||||
```
|
||||
|
||||
Le manifest racine devient :
|
||||
|
||||
```text
|
||||
# version: 44
|
||||
```
|
||||
|
||||
Le plan Config devient :
|
||||
|
||||
```text
|
||||
<!-- version: 7 -->
|
||||
```
|
||||
|
||||
## Fichiers ajoutés
|
||||
|
||||
```text
|
||||
deltas/0.1.3/pre.004.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/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
|
||||
docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md
|
||||
```
|
||||
|
||||
## Hors scope confirmé
|
||||
|
||||
Cette tranche n'implémente pas :
|
||||
|
||||
- le runtime multi-sink complet ;
|
||||
- le routing réel par target/domain de chaque sink ;
|
||||
- les formatters Compact/Pretty/JSON runtime ;
|
||||
- l'ANSI console runtime ;
|
||||
- la généralisation complète des guards/drop counters par `output_id` ;
|
||||
- `serde`, `serde_json` ou `jsonschema` ;
|
||||
- `std.logging.json` ou son schema ;
|
||||
- profils Config ;
|
||||
- `.env` ou interpolation ;
|
||||
- modification de `ksp-config-lib`.
|
||||
|
||||
## 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 le 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.005 — ksp-logging-lib : runtime multi-sink + routing
|
||||
```
|
||||
|
||||
Cette tranche suivante devra activer réellement les contrats stabilisés ici sans réintroduire Config dans Logging.
|
||||
@@ -1,11 +1,11 @@
|
||||
<!-- file: docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md -->
|
||||
<!-- version: 6 -->
|
||||
<!-- version: 7 -->
|
||||
|
||||
# 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`, complété par `0.1.3-pre.001-fix.002`, puis regranularisé par `0.1.3-pre.001-fix.003`. `0.1.3-pre.002` ouvre maintenant le développement fonctionnel avec la seule fondation bootstrap de `ksp-config-lib`.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -1612,7 +1612,7 @@ Implémentation candidate livrée par `pre.003` :
|
||||
- l'unicité des `file_id`, les erreurs d'ID/mapping et les overrides config/schema sont couverts par tests unitaires et API publique ;
|
||||
- aucune dépendance externe, lecture JSON, validation schema ou `.env` n'est introduite par cette tranche.
|
||||
|
||||
La validation utilisateur de `pre.003` reste requise avant l'ouverture de `pre.004`.
|
||||
Validation utilisateur acquise pour `pre.003` : `cargo fmt --all`, `cargo check --workspace`, `cargo clippy --workspace --all-targets` et `cargo test --workspace` sont propres, avec 21 tests unitaires Config et 4 tests d'API publique Config réussis.
|
||||
|
||||
### `0.1.3-pre.004` — `ksp-logging-lib` : modèle public multi-output
|
||||
|
||||
@@ -1631,6 +1631,23 @@ Objectif unique : compléter les **contrats/settings publics** de Logging sans e
|
||||
|
||||
Cette tranche ne doit pas être gonflée par le runtime multi-sink complet. Si même le modèle public dépasse le budget, il est scindé avant de poursuivre.
|
||||
|
||||
Implémentation candidate livrée par `pre.004` :
|
||||
|
||||
- `LogFormat::{Human, Compact, Pretty, Json}` devient le contrat de format public ;
|
||||
- `OutputFilter` porte un niveau et des listes explicites de selectors target/domain, avec `*` comme wildcard exclusif d'une dimension ;
|
||||
- les selectors target restent bornés aux targets KSP (`ksp-*`) ;
|
||||
- `ConsoleSettings` porte explicitement `enabled`, `output`, `ansi`, `format` et `filter` ;
|
||||
- `FileSettings` porte `output_id`, `enabled`, destination actuelle `directory/file_name_prefix`, rotation, format, ANSI et filtre ;
|
||||
- les `output_id` fichier sont uniques, ASCII minuscules segmentables par `.`, et distincts des `file_id` Config ;
|
||||
- les fichiers persistants refusent `ansi = true` ;
|
||||
- `LoggingSettings` représente désormais `files: Vec<FileSettings>` au lieu d'un seul fichier optionnel, tout en conservant `default_filter`, `TargetFilter`, `SpanEvents` et la console ;
|
||||
- `validate()` valide le modèle multi-output indépendamment du backend runtime ;
|
||||
- les helpers `ConsoleSettings::stdout/stderr()` conservent un profil compatible avec le runtime historique ;
|
||||
- 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`.
|
||||
|
||||
### `0.1.3-pre.005` — `ksp-logging-lib` : runtime multi-sink + routing
|
||||
|
||||
Objectif unique : implémenter derrière les contrats de `pre.004` le comportement runtime manquant.
|
||||
|
||||
Reference in New Issue
Block a user