v0.1.3-pre.004
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 43
|
# version: 44
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = ["crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib"]
|
members = ["crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.3-pre.3"
|
version = "0.1.3-pre.4"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: crates/ksp-logging-lib/README.md -->
|
<!-- file: crates/ksp-logging-lib/README.md -->
|
||||||
<!-- version: 2 -->
|
<!-- version: 3 -->
|
||||||
|
|
||||||
# ksp-logging-lib
|
# ksp-logging-lib
|
||||||
|
|
||||||
@@ -11,11 +11,13 @@ La crate possède :
|
|||||||
|
|
||||||
- les cinq niveaux KSP `error`, `warn`, `info`, `debug` et `trace` ;
|
- 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 ;
|
- 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 ;
|
- l'installation unique du subscriber global ;
|
||||||
- le hot reload via `reinitialize` sans second subscriber global ;
|
- le hot reload via `reinitialize` sans second subscriber global ;
|
||||||
- le takeover des logs : les targets externes sont silencieux par défaut ;
|
- le takeover des logs : les targets externes sont silencieux par défaut ;
|
||||||
- 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 ;
|
- les `WorkerGuard`, compteurs de lignes abandonnées, rotation fichier et stripping ANSI ;
|
||||||
- l'instrumentation de scopes synchrones et de `Future` async.
|
- l'instrumentation de scopes synchrones et de `Future` async.
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
<!-- file: crates/ksp-logging-lib/TODO.md -->
|
<!-- file: crates/ksp-logging-lib/TODO.md -->
|
||||||
<!-- version: 2 -->
|
<!-- version: 3 -->
|
||||||
|
|
||||||
# TODO ksp-logging-lib
|
# 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 ;
|
`0.1.3-pre.004` expose désormais le modèle public multi-output. La tranche runtime suivante doit encore :
|
||||||
- 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 ;
|
- activer simultanément zéro, un ou plusieurs fichiers ;
|
||||||
- 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.
|
- 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
|
## 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` ;
|
- rotation par taille, rétention/compression et symlink `latest` ;
|
||||||
- formats JSON ou autres formats structurés alternatifs ;
|
|
||||||
- OpenTelemetry/export réseau ;
|
- OpenTelemetry/export réseau ;
|
||||||
- watcher de fichiers de configuration, qui appartient à Config ou à une couche supérieure ;
|
- 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.
|
- benchmark/profiling de précision destiné aux chemins de trading sensibles à la latence.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: crates/ksp-logging-lib/USAGE.md -->
|
<!-- file: crates/ksp-logging-lib/USAGE.md -->
|
||||||
<!-- version: 2 -->
|
<!-- version: 3 -->
|
||||||
|
|
||||||
# Utilisation de ksp-logging-lib
|
# 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.
|
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
|
```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(
|
let settings = ksp_logging_lib::LoggingSettings::new(
|
||||||
ksp_logging_lib::LogFilterLevel::Info,
|
ksp_logging_lib::LogFilterLevel::Info,
|
||||||
ksp_logging_lib::SpanEvents::NewAndClose,
|
ksp_logging_lib::SpanEvents::NewAndClose,
|
||||||
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
|
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
|
||||||
std::option::Option::Some(ksp_logging_lib::FileSettings::new(
|
std::vec![file],
|
||||||
"logs",
|
|
||||||
"worker.log",
|
|
||||||
ksp_logging_lib::FileRotation::Daily,
|
|
||||||
)),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let initialize_result = ksp_logging_lib::initialize(&settings);
|
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
|
## 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
|
```rust
|
||||||
let debug_settings = ksp_logging_lib::LoggingSettings::new(
|
let debug_settings = ksp_logging_lib::LoggingSettings::new(
|
||||||
ksp_logging_lib::LogFilterLevel::Info,
|
ksp_logging_lib::LogFilterLevel::Info,
|
||||||
ksp_logging_lib::SpanEvents::NewAndClose,
|
ksp_logging_lib::SpanEvents::NewAndClose,
|
||||||
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
|
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(
|
.with_target_filter(ksp_logging_lib::TargetFilter::new(
|
||||||
"ksp-store-lib",
|
"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
|
## 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
|
```rust
|
||||||
let dropped = logging_guard.dropped_lines();
|
let dropped = logging_guard.dropped_lines();
|
||||||
@@ -119,12 +199,4 @@ ksp_logging_lib::warn!(
|
|||||||
|
|
||||||
## Instrumentation async et executor
|
## Instrumentation async et executor
|
||||||
|
|
||||||
`instrument(span, future)` accepte une `Future` standard et ne dépend d'aucun executor particulier :
|
`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.
|
||||||
|
|
||||||
```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.
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-logging-lib/src/lib.rs
|
// file: crates/ksp-logging-lib/src/lib.rs
|
||||||
// version: 4
|
// version: 5
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
@@ -7,9 +7,9 @@
|
|||||||
//! KSP-owned logging and tracing facade.
|
//! KSP-owned logging and tracing facade.
|
||||||
//!
|
//!
|
||||||
//! This crate owns the KSP runtime logging contract. Behavioral KSP crates emit events and spans through this facade rather than depending directly on the
|
//! This crate owns the KSP runtime logging contract. Behavioral KSP crates emit events and spans through this facade rather than depending directly on the
|
||||||
//! `tracing` stack. `0.1.2-pre.005` owns the single global subscriber, KSP takeover filtering, hot reload, non-blocking console/file outputs, rolling file
|
//! `tracing` stack. The crate owns the single global subscriber, KSP takeover filtering, hot reload and non-blocking outputs. `0.1.3-pre.004` extends
|
||||||
//! appenders, ANSI stripping, dropped-line counters and the worker guards required to flush active queues. The integration surface is hardened by
|
//! the public settings contract with explicit console properties, multiple file descriptors, per-output level/target/domain filters and selectable formats;
|
||||||
//! deterministic saturation, concurrent reload and ownership audits before final release validation.
|
//! runtime activation of the newly represented multi-output routing is completed separately so unsupported capabilities are never ignored silently.
|
||||||
|
|
||||||
mod error;
|
mod error;
|
||||||
mod macros;
|
mod macros;
|
||||||
@@ -44,8 +44,12 @@ pub use self::settings::FileRotation;
|
|||||||
pub use self::settings::FileSettings;
|
pub use self::settings::FileSettings;
|
||||||
/// Runtime filter level used by KSP logging settings.
|
/// Runtime filter level used by KSP logging settings.
|
||||||
pub use self::settings::LogFilterLevel;
|
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.
|
/// Complete runtime settings consumed by Logging initialization and reload.
|
||||||
pub use self::settings::LoggingSettings;
|
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.
|
/// Lifecycle events emitted for spans by the formatted subscriber.
|
||||||
pub use self::settings::SpanEvents;
|
pub use self::settings::SpanEvents;
|
||||||
/// Per-target filter override owned by Logging.
|
/// Per-target filter override owned by Logging.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-logging-lib/src/runtime.rs
|
// file: crates/ksp-logging-lib/src/runtime.rs
|
||||||
// version: 8
|
// version: 9
|
||||||
|
|
||||||
use tracing_subscriber::Layer; // rust-rules: trait-import
|
use tracing_subscriber::Layer; // rust-rules: trait-import
|
||||||
use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import
|
use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import
|
||||||
@@ -165,17 +165,27 @@ fn prepare_runtime(settings: &crate::LoggingSettings) -> ksp_core_lib::Result<Pr
|
|||||||
if let std::option::Option::Some(error) = validation_error {
|
if let std::option::Option::Some(error) = validation_error {
|
||||||
return std::result::Result::Err(error);
|
return std::result::Result::Err(error);
|
||||||
}
|
}
|
||||||
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() });
|
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::option::Option::Some(file) => match build_file_output(file, settings) {
|
||||||
std::result::Result::Ok(output) => std::option::Option::Some(output),
|
std::result::Result::Ok(output) => std::option::Option::Some(output),
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
},
|
},
|
||||||
std::option::Option::None => std::option::Option::None,
|
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);
|
return build_console_output(console, settings);
|
||||||
});
|
});
|
||||||
let mut output_layers = RuntimeLayers::new();
|
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 });
|
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 {
|
fn build_console_output(console: &crate::ConsoleSettings, settings: &crate::LoggingSettings) -> PreparedOutput {
|
||||||
return match console.output() {
|
return match console.output() {
|
||||||
crate::ConsoleOutput::Stdout => build_non_blocking_output(std::io::stdout(), "ksp-logging-console", settings, true),
|
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
|
// file: crates/ksp-logging-lib/src/settings.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
/// Runtime filter level used by KSP logging settings.
|
/// Runtime filter level used by KSP logging settings.
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
@@ -18,7 +18,7 @@ pub enum LogFilterLevel {
|
|||||||
Trace,
|
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)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct TargetFilter {
|
pub struct TargetFilter {
|
||||||
target_prefix: std::string::String,
|
target_prefix: std::string::String,
|
||||||
@@ -56,6 +56,74 @@ pub enum SpanEvents {
|
|||||||
Full,
|
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.
|
/// Console stream selected for human-readable logs.
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
pub enum ConsoleOutput {
|
pub enum ConsoleOutput {
|
||||||
@@ -66,22 +134,38 @@ pub enum ConsoleOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Runtime settings for the optional console output.
|
/// Runtime settings for the optional console output.
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct ConsoleSettings {
|
pub struct ConsoleSettings {
|
||||||
|
enabled: bool,
|
||||||
output: crate::ConsoleOutput,
|
output: crate::ConsoleOutput,
|
||||||
|
ansi: bool,
|
||||||
|
format: crate::LogFormat,
|
||||||
|
filter: crate::OutputFilter,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConsoleSettings {
|
impl ConsoleSettings {
|
||||||
/// Creates console settings targeting standard output.
|
/// Creates explicit console settings.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn stdout() -> Self {
|
pub fn new(enabled: bool, output: crate::ConsoleOutput, ansi: bool, format: crate::LogFormat, filter: crate::OutputFilter) -> Self {
|
||||||
return Self { output: crate::ConsoleOutput::Stdout };
|
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]
|
#[must_use]
|
||||||
pub const fn stderr() -> Self {
|
pub fn stdout() -> Self {
|
||||||
return Self { output: crate::ConsoleOutput::Stderr };
|
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.
|
/// Returns the selected console stream.
|
||||||
@@ -89,9 +173,27 @@ impl ConsoleSettings {
|
|||||||
pub const fn output(&self) -> crate::ConsoleOutput {
|
pub const fn output(&self) -> crate::ConsoleOutput {
|
||||||
return self.output;
|
return self.output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns whether ANSI formatting is requested for the console output.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn ansi(&self) -> bool {
|
||||||
|
return self.ansi;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rotation cadence for the optional file output.
|
/// 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 a file output.
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
pub enum FileRotation {
|
pub enum FileRotation {
|
||||||
/// Keeps a single non-rotating file.
|
/// Keeps a single non-rotating file.
|
||||||
@@ -102,23 +204,62 @@ pub enum FileRotation {
|
|||||||
Daily,
|
Daily,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Runtime settings for the optional file output.
|
/// Runtime settings for one file output.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct FileSettings {
|
pub struct FileSettings {
|
||||||
|
output_id: std::string::String,
|
||||||
|
enabled: bool,
|
||||||
directory: std::path::PathBuf,
|
directory: std::path::PathBuf,
|
||||||
file_name_prefix: std::string::String,
|
file_name_prefix: std::string::String,
|
||||||
rotation: crate::FileRotation,
|
rotation: crate::FileRotation,
|
||||||
|
format: crate::LogFormat,
|
||||||
|
ansi: bool,
|
||||||
|
filter: crate::OutputFilter,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileSettings {
|
impl FileSettings {
|
||||||
/// Creates file output settings.
|
/// Creates explicit file output settings.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
|
output_id: impl std::convert::Into<std::string::String>,
|
||||||
|
enabled: bool,
|
||||||
directory: impl std::convert::Into<std::path::PathBuf>,
|
directory: impl std::convert::Into<std::path::PathBuf>,
|
||||||
file_name_prefix: impl std::convert::Into<std::string::String>,
|
file_name_prefix: impl std::convert::Into<std::string::String>,
|
||||||
rotation: crate::FileRotation,
|
rotation: crate::FileRotation,
|
||||||
|
format: crate::LogFormat,
|
||||||
|
filter: crate::OutputFilter,
|
||||||
) -> Self {
|
) -> 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.
|
/// Returns the directory containing log files.
|
||||||
@@ -138,6 +279,24 @@ impl FileSettings {
|
|||||||
pub const fn rotation(&self) -> crate::FileRotation {
|
pub const fn rotation(&self) -> crate::FileRotation {
|
||||||
return self.rotation;
|
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.
|
/// 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>,
|
target_filters: std::vec::Vec<crate::TargetFilter>,
|
||||||
span_events: crate::SpanEvents,
|
span_events: crate::SpanEvents,
|
||||||
console: std::option::Option<crate::ConsoleSettings>,
|
console: std::option::Option<crate::ConsoleSettings>,
|
||||||
file: std::option::Option<crate::FileSettings>,
|
files: std::vec::Vec<crate::FileSettings>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LoggingSettings {
|
impl LoggingSettings {
|
||||||
/// Creates explicit Logging settings without any target override.
|
/// Creates explicit Logging settings without any global target override.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
default_filter: crate::LogFilterLevel,
|
default_filter: crate::LogFilterLevel,
|
||||||
span_events: crate::SpanEvents,
|
span_events: crate::SpanEvents,
|
||||||
console: std::option::Option<crate::ConsoleSettings>,
|
console: std::option::Option<crate::ConsoleSettings>,
|
||||||
file: std::option::Option<crate::FileSettings>,
|
files: std::vec::Vec<crate::FileSettings>,
|
||||||
) -> Self {
|
) -> 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]
|
#[must_use]
|
||||||
pub fn with_target_filter(mut self, target_filter: crate::TargetFilter) -> Self {
|
pub fn with_target_filter(mut self, target_filter: crate::TargetFilter) -> Self {
|
||||||
self.target_filters.push(target_filter);
|
self.target_filters.push(target_filter);
|
||||||
return self;
|
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]
|
#[must_use]
|
||||||
pub const fn default_filter(&self) -> crate::LogFilterLevel {
|
pub const fn default_filter(&self) -> crate::LogFilterLevel {
|
||||||
return self.default_filter;
|
return self.default_filter;
|
||||||
@@ -187,16 +346,16 @@ impl LoggingSettings {
|
|||||||
return self.span_events;
|
return self.span_events;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns console settings when console output is enabled.
|
/// Returns console settings when the console output is declared.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn console(&self) -> std::option::Option<&crate::ConsoleSettings> {
|
pub fn console(&self) -> std::option::Option<&crate::ConsoleSettings> {
|
||||||
return self.console.as_ref();
|
return self.console.as_ref();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns file settings when file output is enabled.
|
/// Returns all declared file outputs in declaration order.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn file(&self) -> std::option::Option<&crate::FileSettings> {
|
pub fn files(&self) -> &[crate::FileSettings] {
|
||||||
return self.file.as_ref();
|
return self.files.as_slice();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validates backend-independent invariants of the runtime settings.
|
/// 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()
|
if let std::option::Option::Some(console) = self.console.as_ref() {
|
||||||
&& file.file_name_prefix().is_empty()
|
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(
|
return std::result::Result::Err(
|
||||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "file name prefix must not be empty")
|
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "file name prefix must not be empty")
|
||||||
.with_context("field", "file.file_name_prefix"),
|
.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(());
|
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)]
|
#[cfg(test)]
|
||||||
#[path = "../unit_tests/settings.rs"]
|
#[path = "../unit_tests/settings.rs"]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-logging-lib/tests/public_api.rs
|
// 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`.
|
//! Integration tests for the public crate-root surface of `ksp-logging-lib`.
|
||||||
|
|
||||||
@@ -7,16 +7,41 @@ const TEST_TARGET: &str = "ksp-logging-lib";
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn public_settings_surface_is_usable() {
|
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(
|
let settings = ksp_logging_lib::LoggingSettings::new(
|
||||||
ksp_logging_lib::LogFilterLevel::Info,
|
ksp_logging_lib::LogFilterLevel::Info,
|
||||||
ksp_logging_lib::SpanEvents::NewAndClose,
|
ksp_logging_lib::SpanEvents::NewAndClose,
|
||||||
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stdout()),
|
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::new(
|
||||||
std::option::Option::Some(ksp_logging_lib::FileSettings::new("logs", "ksp", ksp_logging_lib::FileRotation::Daily)),
|
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));
|
.with_target_filter(ksp_logging_lib::TargetFilter::new(TEST_TARGET, ksp_logging_lib::LogFilterLevel::Trace));
|
||||||
assert!(settings.validate().is_ok());
|
assert!(settings.validate().is_ok());
|
||||||
assert_eq!(settings.default_filter(), ksp_logging_lib::LogFilterLevel::Info);
|
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::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]
|
#[test]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-logging-lib/tests/runtime.rs
|
// 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.
|
//! 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::LogFilterLevel::Off,
|
||||||
ksp_logging_lib::SpanEvents::Off,
|
ksp_logging_lib::SpanEvents::Off,
|
||||||
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
|
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);
|
let quiet_reload = ksp_logging_lib::reinitialize(guard, &quiet_console);
|
||||||
assert!(quiet_reload.is_ok());
|
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::LogFilterLevel::Info,
|
||||||
ksp_logging_lib::SpanEvents::Off,
|
ksp_logging_lib::SpanEvents::Off,
|
||||||
std::option::Option::None,
|
std::option::Option::None,
|
||||||
std::option::Option::None,
|
std::vec::Vec::new(),
|
||||||
);
|
);
|
||||||
let initialize_result = ksp_logging_lib::initialize(&disabled);
|
let initialize_result = ksp_logging_lib::initialize(&disabled);
|
||||||
assert!(initialize_result.is_ok());
|
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::LogFilterLevel::Info,
|
||||||
ksp_logging_lib::SpanEvents::NewAndClose,
|
ksp_logging_lib::SpanEvents::NewAndClose,
|
||||||
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
|
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));
|
.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);
|
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::LogFilterLevel::Error,
|
||||||
ksp_logging_lib::SpanEvents::Full,
|
ksp_logging_lib::SpanEvents::Full,
|
||||||
std::option::Option::None,
|
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);
|
let failed_reload = ksp_logging_lib::reinitialize(&mut guard, &invalid_file);
|
||||||
assert!(failed_reload.is_err());
|
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::LogFilterLevel::Info,
|
||||||
ksp_logging_lib::SpanEvents::Off,
|
ksp_logging_lib::SpanEvents::Off,
|
||||||
std::option::Option::None,
|
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);
|
let file_reload = ksp_logging_lib::reinitialize(&mut guard, &file_enabled);
|
||||||
assert!(file_reload.is_ok());
|
assert!(file_reload.is_ok());
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-logging-lib/unit_tests/runtime.rs
|
// file: crates/ksp-logging-lib/unit_tests/runtime.rs
|
||||||
// version: 5
|
// version: 6
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn level_mapping_covers_all_ksp_levels() {
|
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::LogFilterLevel::Info,
|
||||||
crate::SpanEvents::Off,
|
crate::SpanEvents::Off,
|
||||||
std::option::Option::Some(crate::ConsoleSettings::stdout()),
|
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));
|
.with_target_filter(crate::TargetFilter::new("ksp-logging-lib", crate::LogFilterLevel::Trace));
|
||||||
let filter = super::build_target_filter(&settings);
|
let filter = super::build_target_filter(&settings);
|
||||||
@@ -47,7 +47,7 @@ fn file_rotation_mapping_covers_supported_cadences() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn disabled_runtime_has_no_layers_or_outputs() {
|
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);
|
let result = super::prepare_runtime(&settings);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
let prepared = match result {
|
let prepared = match result {
|
||||||
@@ -65,7 +65,7 @@ fn console_runtime_composes_takeover_filter_before_formatter_and_owns_guard() {
|
|||||||
crate::LogFilterLevel::Info,
|
crate::LogFilterLevel::Info,
|
||||||
crate::SpanEvents::Off,
|
crate::SpanEvents::Off,
|
||||||
std::option::Option::Some(crate::ConsoleSettings::stdout()),
|
std::option::Option::Some(crate::ConsoleSettings::stdout()),
|
||||||
std::option::Option::None,
|
std::vec::Vec::new(),
|
||||||
);
|
);
|
||||||
let result = super::prepare_runtime(&settings);
|
let result = super::prepare_runtime(&settings);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
@@ -91,7 +91,7 @@ fn dropped_line_snapshots_add_saturating_by_sink() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn takeover_filter_prefers_more_specific_ksp_prefixes_and_supports_off() {
|
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-", crate::LogFilterLevel::Debug))
|
||||||
.with_target_filter(crate::TargetFilter::new("ksp-store-lib", crate::LogFilterLevel::Trace))
|
.with_target_filter(crate::TargetFilter::new("ksp-store-lib", crate::LogFilterLevel::Trace))
|
||||||
.with_target_filter(crate::TargetFilter::new("ksp-wallet-lib", crate::LogFilterLevel::Off));
|
.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));
|
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 {
|
struct BlockingWriter {
|
||||||
first_write: bool,
|
first_write: bool,
|
||||||
started: std::sync::mpsc::SyncSender<()>,
|
started: std::sync::mpsc::SyncSender<()>,
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
// file: crates/ksp-logging-lib/unit_tests/settings.rs
|
// 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]
|
#[test]
|
||||||
fn level_variants_are_distinct() {
|
fn level_variants_are_distinct() {
|
||||||
@@ -10,6 +22,13 @@ fn level_variants_are_distinct() {
|
|||||||
assert_ne!(crate::LogFilterLevel::Debug, crate::LogFilterLevel::Trace);
|
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]
|
#[test]
|
||||||
fn target_filter_preserves_prefix_and_level() {
|
fn target_filter_preserves_prefix_and_level() {
|
||||||
let filter = crate::TargetFilter::new("ksp-store-lib", crate::LogFilterLevel::Trace);
|
let filter = crate::TargetFilter::new("ksp-store-lib", crate::LogFilterLevel::Trace);
|
||||||
@@ -18,85 +37,188 @@ fn target_filter_preserves_prefix_and_level() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn console_settings_select_requested_stream() {
|
fn output_filter_preserves_level_targets_and_domains() {
|
||||||
assert_eq!(crate::ConsoleSettings::stdout().output(), crate::ConsoleOutput::Stdout);
|
let filter = crate::OutputFilter::new(
|
||||||
assert_eq!(crate::ConsoleSettings::stderr().output(), crate::ConsoleOutput::Stderr);
|
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]
|
#[test]
|
||||||
fn file_settings_preserve_values() {
|
fn console_settings_preserve_enabled_stream_ansi_format_and_filter() {
|
||||||
let settings = crate::FileSettings::new("logs", "ksp", crate::FileRotation::Daily);
|
let filter = crate::OutputFilter::new(crate::LogFilterLevel::Debug, std::vec!["*".to_string()], std::vec!["config".to_string()]);
|
||||||
assert_eq!(settings.directory(), std::path::Path::new("logs"));
|
let settings = crate::ConsoleSettings::new(true, crate::ConsoleOutput::Stderr, true, crate::LogFormat::Compact, filter.clone());
|
||||||
assert_eq!(settings.file_name_prefix(), "ksp");
|
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.rotation(), crate::FileRotation::Daily);
|
||||||
|
assert_eq!(settings.format(), crate::LogFormat::Json);
|
||||||
|
assert!(!settings.ansi());
|
||||||
|
assert_eq!(settings.filter(), &filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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(
|
let settings = crate::LoggingSettings::new(
|
||||||
crate::LogFilterLevel::Info,
|
crate::LogFilterLevel::Info,
|
||||||
crate::SpanEvents::NewAndClose,
|
crate::SpanEvents::NewAndClose,
|
||||||
std::option::Option::Some(crate::ConsoleSettings::stdout()),
|
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));
|
.with_target_filter(crate::TargetFilter::new("ksp-store-lib", crate::LogFilterLevel::Trace));
|
||||||
assert_eq!(settings.default_filter(), crate::LogFilterLevel::Info);
|
assert_eq!(settings.default_filter(), crate::LogFilterLevel::Info);
|
||||||
assert_eq!(settings.span_events(), crate::SpanEvents::NewAndClose);
|
assert_eq!(settings.span_events(), crate::SpanEvents::NewAndClose);
|
||||||
assert_eq!(settings.target_filters().len(), 1);
|
assert_eq!(settings.target_filters().len(), 1);
|
||||||
assert_eq!(settings.target_filters()[0].target_prefix(), "ksp-store-lib");
|
assert_eq!(settings.files().len(), 2);
|
||||||
assert_eq!(settings.console(), std::option::Option::Some(&crate::ConsoleSettings::stdout()));
|
assert_eq!(settings.files()[0].output_id(), "file.debug");
|
||||||
assert_eq!(settings.file().map(crate::FileSettings::rotation), std::option::Option::Some(crate::FileRotation::Hourly));
|
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]
|
#[test]
|
||||||
fn validation_rejects_empty_target_prefix() {
|
fn validation_rejects_empty_or_external_global_target_prefix() {
|
||||||
let settings = crate::LoggingSettings::new(
|
let empty = crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::vec::Vec::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));
|
.with_target_filter(crate::TargetFilter::new("", crate::LogFilterLevel::Debug));
|
||||||
assert!(settings.validate().is_err());
|
let external = crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::vec::Vec::new())
|
||||||
}
|
|
||||||
|
|
||||||
#[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));
|
.with_target_filter(crate::TargetFilter::new("sqlx", crate::LogFilterLevel::Debug));
|
||||||
assert!(settings.validate().is_err());
|
assert!(empty.validate().is_err());
|
||||||
|
assert!(external.validate().is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn validation_rejects_empty_file_prefix() {
|
fn validation_rejects_invalid_file_identity_and_duplicate_output_ids() {
|
||||||
let settings = crate::LoggingSettings::new(
|
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::LogFilterLevel::Info,
|
||||||
crate::SpanEvents::Off,
|
crate::SpanEvents::Off,
|
||||||
std::option::Option::None,
|
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!(invalid_id.validate().is_err());
|
||||||
|
assert!(duplicate_id.validate().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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_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 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());
|
assert!(settings.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)),
|
|
||||||
)
|
|
||||||
.with_target_filter(crate::TargetFilter::new("ksp-worker-", crate::LogFilterLevel::Debug));
|
|
||||||
assert!(settings.validate().is_ok());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn settings_allow_logging_to_be_disabled() {
|
fn settings_allow_all_outputs_to_be_disabled() {
|
||||||
let settings = crate::LoggingSettings::new(crate::LogFilterLevel::Off, crate::SpanEvents::Off, std::option::Option::None, std::option::Option::None);
|
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());
|
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 -->
|
<!-- file: docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md -->
|
||||||
<!-- version: 6 -->
|
<!-- version: 7 -->
|
||||||
|
|
||||||
# Plan `0.1.3` — Configuration foundation
|
# Plan `0.1.3` — Configuration foundation
|
||||||
|
|
||||||
## 1. Statut et objectif
|
## 1. Statut et objectif
|
||||||
|
|
||||||
Ce plan a été établi par `0.1.3-pre.001`, corrigé par `0.1.3-pre.001-fix.001`, 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`.
|
La base auditée reste la release stable `v0.1.2`.
|
||||||
|
|
||||||
@@ -231,7 +231,7 @@ La complétion Logging n'ouvre ni une nouvelle façade ni une dépendance invers
|
|||||||
## 5. Matrice de responsabilités
|
## 5. Matrice de responsabilités
|
||||||
|
|
||||||
| Responsabilité | Propriétaire | Consommateurs | Interdit |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
||||||
@@ -581,7 +581,7 @@ Décisions :
|
|||||||
### 8.1 Écart `ksp-logging-lib 0.1.2` à fermer
|
### 8.1 Écart `ksp-logging-lib 0.1.2` à fermer
|
||||||
|
|
||||||
| Capacité | `0.1.2` | Requise par `std.logging.json` |
|
| Capacité | `0.1.2` | Requise par `std.logging.json` |
|
||||||
|-----------------------------------|----------------:|-------------------------------------:|
|
|---|---:|---:|
|
||||||
| filtre global | oui | oui |
|
| filtre global | oui | oui |
|
||||||
| overrides par target | oui | oui |
|
| overrides par target | oui | oui |
|
||||||
| lifecycle spans | oui | oui |
|
| lifecycle spans | oui | oui |
|
||||||
@@ -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 ;
|
- 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.
|
- 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
|
### `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.
|
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
|
### `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.
|
Objectif unique : implémenter derrière les contrats de `pre.004` le comportement runtime manquant.
|
||||||
|
|||||||
Reference in New Issue
Block a user