v0.1.3-pre.004

This commit is contained in:
2026-08-15 19:58:18 +02:00
parent 0629a48e97
commit 29660fd9f0
13 changed files with 1134 additions and 174 deletions

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-logging-lib/README.md -->
<!-- version: 2 -->
<!-- version: 3 -->
# ksp-logging-lib
@@ -11,11 +11,13 @@ La crate possède :
- les cinq niveaux KSP `error`, `warn`, `info`, `debug` et `trace` ;
- les macros d'événements et de spans qui préservent le callsite du consommateur ;
- `LoggingSettings` et les settings console/fichier indépendants de Config ;
- `LoggingSettings`, la console explicite et les settings fichier indépendants de Config ;
- les formats publics `Human/Compact/Pretty/Json` et les filtres par output (`level`, targets KSP et domains) ;
- zéro, un ou plusieurs descriptors fichier identifiés par `output_id` unique ;
- l'installation unique du subscriber global ;
- le hot reload via `reinitialize` sans second subscriber global ;
- le takeover des logs : les targets externes sont silencieux par défaut ;
- les sorties console et fichier non bloquantes ;
- le runtime non bloquant console/fichier existant, avec activation multi-sink/routing enrichie finalisée dans la tranche runtime dédiée ;
- les `WorkerGuard`, compteurs de lignes abandonnées, rotation fichier et stripping ANSI ;
- l'instrumentation de scopes synchrones et de `Future` async.

View File

@@ -1,22 +1,26 @@
<!-- file: crates/ksp-logging-lib/TODO.md -->
<!-- version: 2 -->
<!-- version: 3 -->
# TODO ksp-logging-lib
## À fermer avant la stable 0.1.2
## À fermer pendant `0.1.3`
- exécuter les validations Cargo complètes de `pre.006`, y compris les tests Tokio current-thread/multi-thread ;
- vérifier que `cargo tree -p ksp-logging-lib -e normal` ne contient pas Tokio et que Tokio apparaît uniquement dans le graphe dev attendu ;
- refaire l'audit final du graphe/features et de l'ownership de la stack tracing ;
- après validation de la prerelease finale, préparer `rel.001`, publier `workspace.package.version = "0.1.2"` et taguer `v0.1.2` conformément aux règles de release.
`0.1.3-pre.004` expose désormais le modèle public multi-output. La tranche runtime suivante doit encore :
- activer simultanément zéro, un ou plusieurs fichiers ;
- appliquer réellement `OutputFilter` par sink sur niveau, target et domain ;
- appliquer `LogFormat::{Human, Compact, Pretty, Json}` par sink ;
- appliquer l'ANSI console configurable tout en gardant les fichiers sans séquences ANSI ;
- généraliser les writers/guards et compteurs de lignes abandonnées par output sans perdre la vue agrégée utile ;
- conserver le subscriber global unique, le takeover KSP et le hot reload transactionnel.
Tant que cette tranche runtime n'est pas livrée, `initialize/reinitialize` refusent explicitement les capacités du nouveau contrat qu'ils ne savent pas encore appliquer.
## Capacités différées
Ces éléments ne font pas partie du contrat `0.1.2` et ne doivent être ajoutés qu'après besoin concret :
Ces éléments ne sont pas requis par la fondation Config `0.1.3` :
- plusieurs routes fichier indépendantes ;
- rotation par taille, rétention/compression et symlink `latest` ;
- formats JSON ou autres formats structurés alternatifs ;
- OpenTelemetry/export réseau ;
- watcher de fichiers de configuration, qui appartient à Config ou à une couche supérieure ;
- benchmark/profiling de précision destiné aux chemins de trading sensibles à la latence.

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-logging-lib/USAGE.md -->
<!-- version: 2 -->
<!-- version: 3 -->
# Utilisation de ksp-logging-lib
@@ -21,20 +21,57 @@ ksp_logging_lib::trace!(
Les champs `domain`, `component`, `operation` et autres champs structurés sont ajoutés par le caller lorsqu'ils sont utiles ; ils ne remplacent pas le target propriétaire.
## Initialisation
## Contrat des outputs
`initialize` installe le subscriber global KSP une seule fois et retourne le `LoggingGuard` qui doit rester vivant pendant la durée du runtime :
`LoggingSettings` possède :
- le `default_filter` global de takeover KSP ;
- les `TargetFilter` globaux ;
- la politique `SpanEvents` ;
- une console déclarée optionnellement ;
- zéro, un ou plusieurs `FileSettings`.
Chaque output possède un `OutputFilter` indépendant. Son niveau, ses targets et ses domains s'appliquent **en plus** de la politique globale. `*` signifie « tous » dans la dimension concernée.
Les formats publics sont :
```text
Human
Compact
Pretty
Json
```
Une sortie fichier possède un `output_id` stable et unique dans `LoggingSettings`. Cet identifiant appartient au runtime Logging et ne doit pas être confondu avec les `file_id` utilisés par `ksp-config-lib` pour identifier ses documents.
Les fichiers persistants interdisent `ansi = true`.
## Compatibilité runtime de `pre.004`
`0.1.3-pre.004` étend d'abord le **contrat public**. Le runtime multi-sink/routing est livré séparément en `pre.005`.
Jusqu'à cette tranche suivante, `initialize`/`reinitialize` refusent explicitement toute capacité nouvellement représentée qu'ils ne savent pas encore appliquer : plusieurs fichiers actifs, format non `Human`, ANSI console ou filtre propre à un output. Ces valeurs ne sont donc jamais acceptées puis ignorées silencieusement.
Les helpers `ConsoleSettings::stdout()` et `ConsoleSettings::stderr()` construisent une console compatible avec le runtime actuel : activée, `Human`, sans ANSI et sans filtre supplémentaire par rapport au takeover global.
## Initialisation compatible avec le runtime actuel
```rust
let file = ksp_logging_lib::FileSettings::new(
"file.worker",
true,
"logs",
"worker.log",
ksp_logging_lib::FileRotation::Daily,
ksp_logging_lib::LogFormat::Human,
ksp_logging_lib::OutputFilter::unrestricted(),
);
let settings = ksp_logging_lib::LoggingSettings::new(
ksp_logging_lib::LogFilterLevel::Info,
ksp_logging_lib::SpanEvents::NewAndClose,
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
std::option::Option::Some(ksp_logging_lib::FileSettings::new(
"logs",
"worker.log",
ksp_logging_lib::FileRotation::Daily,
)),
std::vec![file],
);
let initialize_result = ksp_logging_lib::initialize(&settings);
@@ -44,18 +81,61 @@ let mut logging_guard = match initialize_result {
};
```
Une configuration sans console ni fichier est valide et installe une infrastructure initialement silencieuse qui pourra être activée plus tard par hot reload.
Une configuration sans output actif est valide et installe une infrastructure initialement silencieuse qui pourra être activée plus tard par hot reload.
## Construction d'un contrat multi-output
Le contrat public peut déjà représenter la future configuration Config complète :
```rust
let console = ksp_logging_lib::ConsoleSettings::new(
true,
ksp_logging_lib::ConsoleOutput::Stderr,
true,
ksp_logging_lib::LogFormat::Compact,
ksp_logging_lib::OutputFilter::new(
ksp_logging_lib::LogFilterLevel::Debug,
std::vec!["*".to_string()],
std::vec!["*".to_string()],
),
);
let error_file = ksp_logging_lib::FileSettings::new(
"file.config.error",
true,
"logs/config",
"error.jsonl",
ksp_logging_lib::FileRotation::Daily,
ksp_logging_lib::LogFormat::Json,
ksp_logging_lib::OutputFilter::new(
ksp_logging_lib::LogFilterLevel::Error,
std::vec!["ksp-config-lib".to_string()],
std::vec!["config".to_string()],
),
);
let settings = ksp_logging_lib::LoggingSettings::new(
ksp_logging_lib::LogFilterLevel::Warn,
ksp_logging_lib::SpanEvents::NewAndClose,
std::option::Option::Some(console),
std::vec![error_file],
);
let validation = settings.validate();
```
`validate()` vérifie le contrat structurel. L'activation runtime de ce routage enrichi appartient à `pre.005`.
## Hot reload
Une nouvelle configuration peut être appliquée sans redémarrer le processus ou le worker :
Une configuration compatible avec les capacités runtime actives peut être appliquée sans redémarrer le processus ou le worker :
```rust
let debug_settings = ksp_logging_lib::LoggingSettings::new(
ksp_logging_lib::LogFilterLevel::Info,
ksp_logging_lib::SpanEvents::NewAndClose,
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
std::option::Option::None,
std::vec::Vec::new(),
)
.with_target_filter(ksp_logging_lib::TargetFilter::new(
"ksp-store-lib",
@@ -104,7 +184,7 @@ La future instrumentée entre/sort du span pendant ses polls et lors de son `Dro
## Lignes abandonnées
Les sorties utilisent des queues lossy afin de ne pas appliquer de backpressure au hot path. Les pertes restent observables :
Les sorties utilisent des queues lossy afin de ne pas appliquer de backpressure au hot path. Les compteurs restent encore agrégés console/fichier pendant `pre.004` ; leur généralisation par output appartient au runtime multi-sink :
```rust
let dropped = logging_guard.dropped_lines();
@@ -119,12 +199,4 @@ ksp_logging_lib::warn!(
## Instrumentation async et executor
`instrument(span, future)` accepte une `Future` standard et ne dépend d'aucun executor particulier :
```rust
let span = ksp_logging_lib::trace_span!(target: LOGGING_TARGET, "load_transactions");
let result = ksp_logging_lib::instrument(span, async_operation()).await;
```
La crate ne requiert pas Tokio en production. Tokio n'est présent qu'en `dev-dependency` pour valider la surface sur un executor réel, y compris après plusieurs suspensions et sur un runtime multi-thread. Un consumer peut donc utiliser l'executor adapté à son propre contexte sans que Logging lui en impose un.
`instrument(span, future)` accepte une `Future` standard et ne dépend d'aucun executor particulier. Tokio n'est présent qu'en `dev-dependency` pour valider la surface sur un executor réel, y compris après plusieurs suspensions et sur un runtime multi-thread.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-logging-lib/src/lib.rs
// version: 4
// version: 5
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
@@ -7,9 +7,9 @@
//! KSP-owned logging and tracing facade.
//!
//! This crate owns the KSP runtime logging contract. Behavioral KSP crates emit events and spans through this facade rather than depending directly on the
//! `tracing` stack. `0.1.2-pre.005` owns the single global subscriber, KSP takeover filtering, hot reload, non-blocking console/file outputs, rolling file
//! appenders, ANSI stripping, dropped-line counters and the worker guards required to flush active queues. The integration surface is hardened by
//! deterministic saturation, concurrent reload and ownership audits before final release validation.
//! `tracing` stack. The crate owns the single global subscriber, KSP takeover filtering, hot reload and non-blocking outputs. `0.1.3-pre.004` extends
//! the public settings contract with explicit console properties, multiple file descriptors, per-output level/target/domain filters and selectable formats;
//! runtime activation of the newly represented multi-output routing is completed separately so unsupported capabilities are never ignored silently.
mod error;
mod macros;
@@ -44,8 +44,12 @@ pub use self::settings::FileRotation;
pub use self::settings::FileSettings;
/// Runtime filter level used by KSP logging settings.
pub use self::settings::LogFilterLevel;
/// Output format requested for one Logging sink.
pub use self::settings::LogFormat;
/// Complete runtime settings consumed by Logging initialization and reload.
pub use self::settings::LoggingSettings;
/// Per-output routing filter applied in addition to the global KSP takeover policy.
pub use self::settings::OutputFilter;
/// Lifecycle events emitted for spans by the formatted subscriber.
pub use self::settings::SpanEvents;
/// Per-target filter override owned by Logging.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-logging-lib/src/runtime.rs
// version: 8
// version: 9
use tracing_subscriber::Layer; // rust-rules: trait-import
use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import
@@ -165,17 +165,27 @@ fn prepare_runtime(settings: &crate::LoggingSettings) -> ksp_core_lib::Result<Pr
if let std::option::Option::Some(error) = validation_error {
return std::result::Result::Err(error);
}
if settings.console().is_none() && settings.file().is_none() {
let runtime_validation = validate_current_runtime_capabilities(settings);
if let std::result::Result::Err(error) = runtime_validation {
return std::result::Result::Err(error);
}
let enabled_console = settings.console().filter(|console| -> bool {
return console.enabled();
});
let enabled_file = settings.files().iter().find(|file| -> bool {
return file.enabled();
});
if enabled_console.is_none() && enabled_file.is_none() {
return std::result::Result::Ok(PreparedRuntime { layers: RuntimeLayers::new(), outputs: RuntimeOutputs::default() });
}
let prepared_file = match settings.file() {
let prepared_file = match enabled_file {
std::option::Option::Some(file) => match build_file_output(file, settings) {
std::result::Result::Ok(output) => std::option::Option::Some(output),
std::result::Result::Err(error) => return std::result::Result::Err(error),
},
std::option::Option::None => std::option::Option::None,
};
let prepared_console = settings.console().map(|console| -> PreparedOutput {
let prepared_console = enabled_console.map(|console| -> PreparedOutput {
return build_console_output(console, settings);
});
let mut output_layers = RuntimeLayers::new();
@@ -193,6 +203,37 @@ fn prepare_runtime(settings: &crate::LoggingSettings) -> ksp_core_lib::Result<Pr
return std::result::Result::Ok(PreparedRuntime { layers, outputs });
}
fn validate_current_runtime_capabilities(settings: &crate::LoggingSettings) -> ksp_core_lib::Result<()> {
let enabled_file_count = settings
.files()
.iter()
.filter(|file| -> bool {
return file.enabled();
})
.count();
if enabled_file_count > 1 {
return runtime_capability_error("multiple enabled file outputs require the multi-sink runtime");
}
if let std::option::Option::Some(console) = settings.console()
&& console.enabled()
&& (console.ansi() || console.format() != crate::LogFormat::Human || !console.filter().is_unrestricted())
{
return runtime_capability_error("console ANSI, selectable formats and per-output routing require the multi-sink runtime");
}
for file in settings.files() {
if file.enabled() && (file.format() != crate::LogFormat::Human || !file.filter().is_unrestricted()) {
return runtime_capability_error("file formats and per-output routing require the multi-sink runtime");
}
}
return std::result::Result::Ok(());
}
fn runtime_capability_error(message: &str) -> ksp_core_lib::Result<()> {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, message).with_context("runtime_contract", "single-output-compatibility"),
);
}
fn build_console_output(console: &crate::ConsoleSettings, settings: &crate::LoggingSettings) -> PreparedOutput {
return match console.output() {
crate::ConsoleOutput::Stdout => build_non_blocking_output(std::io::stdout(), "ksp-logging-console", settings, true),

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-logging-lib/src/settings.rs
// version: 2
// version: 3
/// Runtime filter level used by KSP logging settings.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
@@ -18,7 +18,7 @@ pub enum LogFilterLevel {
Trace,
}
/// Per-target filter override owned by Logging.
/// Per-target filter override owned by Logging for the global KSP takeover policy.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TargetFilter {
target_prefix: std::string::String,
@@ -56,6 +56,74 @@ pub enum SpanEvents {
Full,
}
/// Output format requested for one Logging sink.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum LogFormat {
/// Standard human-readable formatter with metadata.
Human,
/// Compact human-readable formatter.
Compact,
/// Expanded pretty human-readable formatter.
Pretty,
/// Structured JSON formatter.
Json,
}
/// Per-output routing filter applied in addition to the global KSP takeover policy.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OutputFilter {
level: crate::LogFilterLevel,
targets: std::vec::Vec<std::string::String>,
domains: std::vec::Vec<std::string::String>,
}
impl OutputFilter {
/// Creates an explicit per-output filter.
///
/// `targets` contains KSP target prefixes or the single wildcard `*`. `domains` contains domain prefixes or the single wildcard `*`.
#[must_use]
pub fn new(level: crate::LogFilterLevel, targets: std::vec::Vec<std::string::String>, domains: std::vec::Vec<std::string::String>) -> Self {
return Self { level, targets, domains };
}
/// Creates an unrestricted routing filter that does not further constrain the global KSP takeover policy.
#[must_use]
pub fn unrestricted() -> Self {
return Self::new(crate::LogFilterLevel::Trace, std::vec!["*".to_string()], std::vec!["*".to_string()]);
}
/// Returns the maximum verbosity accepted by this output.
#[must_use]
pub const fn level(&self) -> crate::LogFilterLevel {
return self.level;
}
/// Returns target selectors in declaration order.
#[must_use]
pub fn targets(&self) -> &[std::string::String] {
return self.targets.as_slice();
}
/// Returns domain selectors in declaration order.
#[must_use]
pub fn domains(&self) -> &[std::string::String] {
return self.domains.as_slice();
}
/// Returns whether this filter leaves routing entirely to the global takeover policy.
pub(crate) fn is_unrestricted(&self) -> bool {
let targets_all = match self.targets.as_slice() {
[selector] => selector == "*",
_ => false,
};
let domains_all = match self.domains.as_slice() {
[selector] => selector == "*",
_ => false,
};
return self.level == crate::LogFilterLevel::Trace && targets_all && domains_all;
}
}
/// Console stream selected for human-readable logs.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum ConsoleOutput {
@@ -66,22 +134,38 @@ pub enum ConsoleOutput {
}
/// Runtime settings for the optional console output.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ConsoleSettings {
enabled: bool,
output: crate::ConsoleOutput,
ansi: bool,
format: crate::LogFormat,
filter: crate::OutputFilter,
}
impl ConsoleSettings {
/// Creates console settings targeting standard output.
/// Creates explicit console settings.
#[must_use]
pub const fn stdout() -> Self {
return Self { output: crate::ConsoleOutput::Stdout };
pub fn new(enabled: bool, output: crate::ConsoleOutput, ansi: bool, format: crate::LogFormat, filter: crate::OutputFilter) -> Self {
return Self { enabled, output, ansi, format, filter };
}
/// Creates console settings targeting standard error.
/// Creates enabled standard-output settings compatible with the `0.1.2` runtime behavior.
#[must_use]
pub const fn stderr() -> Self {
return Self { output: crate::ConsoleOutput::Stderr };
pub fn stdout() -> Self {
return Self::new(true, crate::ConsoleOutput::Stdout, false, crate::LogFormat::Human, crate::OutputFilter::unrestricted());
}
/// Creates enabled standard-error settings compatible with the `0.1.2` runtime behavior.
#[must_use]
pub fn stderr() -> Self {
return Self::new(true, crate::ConsoleOutput::Stderr, false, crate::LogFormat::Human, crate::OutputFilter::unrestricted());
}
/// Returns whether this console output is enabled.
#[must_use]
pub const fn enabled(&self) -> bool {
return self.enabled;
}
/// Returns the selected console stream.
@@ -89,9 +173,27 @@ impl ConsoleSettings {
pub const fn output(&self) -> crate::ConsoleOutput {
return self.output;
}
/// Returns whether ANSI formatting is requested for the console output.
#[must_use]
pub const fn ansi(&self) -> bool {
return self.ansi;
}
/// Returns the requested console format.
#[must_use]
pub const fn format(&self) -> crate::LogFormat {
return self.format;
}
/// Returns the routing filter associated with the console output.
#[must_use]
pub const fn filter(&self) -> &crate::OutputFilter {
return &self.filter;
}
}
/// Rotation cadence for the optional file output.
/// Rotation cadence for a file output.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum FileRotation {
/// Keeps a single non-rotating file.
@@ -102,23 +204,62 @@ pub enum FileRotation {
Daily,
}
/// Runtime settings for the optional file output.
/// Runtime settings for one file output.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FileSettings {
output_id: std::string::String,
enabled: bool,
directory: std::path::PathBuf,
file_name_prefix: std::string::String,
rotation: crate::FileRotation,
format: crate::LogFormat,
ansi: bool,
filter: crate::OutputFilter,
}
impl FileSettings {
/// Creates file output settings.
/// Creates explicit file output settings.
#[must_use]
pub fn new(
output_id: impl std::convert::Into<std::string::String>,
enabled: bool,
directory: impl std::convert::Into<std::path::PathBuf>,
file_name_prefix: impl std::convert::Into<std::string::String>,
rotation: crate::FileRotation,
format: crate::LogFormat,
filter: crate::OutputFilter,
) -> Self {
return Self { directory: directory.into(), file_name_prefix: file_name_prefix.into(), rotation };
return Self {
output_id: output_id.into(),
enabled,
directory: directory.into(),
file_name_prefix: file_name_prefix.into(),
rotation,
format,
ansi: false,
filter,
};
}
/// Sets whether ANSI formatting is requested and returns the updated settings.
///
/// Persistent file outputs are required to keep this value `false`; [`crate::LoggingSettings::validate`] rejects `true`.
#[must_use]
pub fn with_ansi(mut self, ansi: bool) -> Self {
self.ansi = ansi;
return self;
}
/// Returns the stable output identifier used for diagnostics and reload accounting.
#[must_use]
pub fn output_id(&self) -> &str {
return self.output_id.as_str();
}
/// Returns whether this file output is enabled.
#[must_use]
pub const fn enabled(&self) -> bool {
return self.enabled;
}
/// Returns the directory containing log files.
@@ -138,6 +279,24 @@ impl FileSettings {
pub const fn rotation(&self) -> crate::FileRotation {
return self.rotation;
}
/// Returns the requested file format.
#[must_use]
pub const fn format(&self) -> crate::LogFormat {
return self.format;
}
/// Returns whether ANSI formatting was requested for this file output.
#[must_use]
pub const fn ansi(&self) -> bool {
return self.ansi;
}
/// Returns the routing filter associated with this file output.
#[must_use]
pub const fn filter(&self) -> &crate::OutputFilter {
return &self.filter;
}
}
/// Complete runtime settings consumed by `ksp-logging-lib` initialization and reload.
@@ -147,29 +306,29 @@ pub struct LoggingSettings {
target_filters: std::vec::Vec<crate::TargetFilter>,
span_events: crate::SpanEvents,
console: std::option::Option<crate::ConsoleSettings>,
file: std::option::Option<crate::FileSettings>,
files: std::vec::Vec<crate::FileSettings>,
}
impl LoggingSettings {
/// Creates explicit Logging settings without any target override.
/// Creates explicit Logging settings without any global target override.
#[must_use]
pub fn new(
default_filter: crate::LogFilterLevel,
span_events: crate::SpanEvents,
console: std::option::Option<crate::ConsoleSettings>,
file: std::option::Option<crate::FileSettings>,
files: std::vec::Vec<crate::FileSettings>,
) -> Self {
return Self { default_filter, target_filters: std::vec::Vec::new(), span_events, console, file };
return Self { default_filter, target_filters: std::vec::Vec::new(), span_events, console, files };
}
/// Adds one target-prefix override and returns the updated settings.
/// Adds one target-prefix override to the global KSP takeover policy and returns the updated settings.
#[must_use]
pub fn with_target_filter(mut self, target_filter: crate::TargetFilter) -> Self {
self.target_filters.push(target_filter);
return self;
}
/// Returns the default level applied to KSP-owned targets.
/// Returns the default level applied to KSP-owned targets by the global takeover policy.
#[must_use]
pub const fn default_filter(&self) -> crate::LogFilterLevel {
return self.default_filter;
@@ -187,16 +346,16 @@ impl LoggingSettings {
return self.span_events;
}
/// Returns console settings when console output is enabled.
/// Returns console settings when the console output is declared.
#[must_use]
pub fn console(&self) -> std::option::Option<&crate::ConsoleSettings> {
return self.console.as_ref();
}
/// Returns file settings when file output is enabled.
/// Returns all declared file outputs in declaration order.
#[must_use]
pub fn file(&self) -> std::option::Option<&crate::FileSettings> {
return self.file.as_ref();
pub fn files(&self) -> &[crate::FileSettings] {
return self.files.as_slice();
}
/// Validates backend-independent invariants of the runtime settings.
@@ -216,18 +375,142 @@ impl LoggingSettings {
);
}
}
if let std::option::Option::Some(file) = self.file.as_ref()
&& file.file_name_prefix().is_empty()
{
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "file name prefix must not be empty")
.with_context("field", "file.file_name_prefix"),
);
if let std::option::Option::Some(console) = self.console.as_ref() {
let validation_result = validate_output_filter(console.filter(), "console.filter");
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
}
for (index, file) in self.files.iter().enumerate() {
let output_id_validation = validate_output_id(file.output_id(), index);
if let std::result::Result::Err(error) = output_id_validation {
return std::result::Result::Err(error);
}
if file.directory().as_os_str().is_empty() {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "file output directory must not be empty")
.with_context("field", format!("files[{index}].directory"))
.with_context("output_id", file.output_id()),
);
}
if file.file_name_prefix().is_empty() {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "file name prefix must not be empty")
.with_context("field", format!("files[{index}].file_name_prefix"))
.with_context("output_id", file.output_id()),
);
}
if file.ansi() {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "ANSI sequences are not allowed in persistent file outputs")
.with_context("field", format!("files[{index}].ansi"))
.with_context("output_id", file.output_id()),
);
}
let filter_validation = validate_output_filter(file.filter(), format!("files[{index}].filter").as_str());
if let std::result::Result::Err(error) = filter_validation {
return std::result::Result::Err(error.with_context("output_id", file.output_id()));
}
for previous in &self.files[..index] {
if previous.output_id() == file.output_id() {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "file output identifiers must be unique")
.with_context("field", format!("files[{index}].output_id"))
.with_context("output_id", file.output_id()),
);
}
}
}
return std::result::Result::Ok(());
}
}
fn validate_output_id(output_id: &str, index: usize) -> ksp_core_lib::Result<()> {
let mut previous_was_separator = true;
if output_id.is_empty() {
return invalid_output_id(output_id, index);
}
for byte in output_id.bytes() {
if byte == b'.' {
if previous_was_separator {
return invalid_output_id(output_id, index);
}
previous_was_separator = true;
continue;
}
if !(byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_' || byte == b'-') {
return invalid_output_id(output_id, index);
}
previous_was_separator = false;
}
if previous_was_separator {
return invalid_output_id(output_id, index);
}
return std::result::Result::Ok(());
}
fn invalid_output_id(output_id: &str, index: usize) -> ksp_core_lib::Result<()> {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "file output identifier is invalid")
.with_context("field", format!("files[{index}].output_id"))
.with_context("output_id", output_id),
);
}
fn validate_output_filter(filter: &crate::OutputFilter, field: &str) -> ksp_core_lib::Result<()> {
let target_validation = validate_selectors(filter.targets(), true, format!("{field}.targets").as_str());
if let std::result::Result::Err(error) = target_validation {
return std::result::Result::Err(error);
}
let domain_validation = validate_selectors(filter.domains(), false, format!("{field}.domains").as_str());
if let std::result::Result::Err(error) = domain_validation {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(());
}
fn validate_selectors(selectors: &[std::string::String], target_dimension: bool, field: &str) -> ksp_core_lib::Result<()> {
if selectors.is_empty() {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "output selector list must not be empty").with_context("field", field),
);
}
if selectors.len() > 1
&& selectors.iter().any(|selector| -> bool {
return selector == "*";
})
{
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "wildcard selector must be used alone").with_context("field", field),
);
}
for (index, selector) in selectors.iter().enumerate() {
if selector.trim().is_empty() {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "output selector must not be empty")
.with_context("field", format!("{field}[{index}]")),
);
}
if target_dimension && selector != "*" && !selector.starts_with("ksp-") {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "output target selector must identify a KSP-owned target")
.with_context("field", format!("{field}[{index}]"))
.with_context("selector", selector),
);
}
for previous in &selectors[..index] {
if previous == selector {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "output selectors must be unique")
.with_context("field", format!("{field}[{index}]"))
.with_context("selector", selector),
);
}
}
}
return std::result::Result::Ok(());
}
#[cfg(test)]
#[path = "../unit_tests/settings.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-logging-lib/tests/public_api.rs
// version: 4
// version: 5
//! Integration tests for the public crate-root surface of `ksp-logging-lib`.
@@ -7,16 +7,41 @@ const TEST_TARGET: &str = "ksp-logging-lib";
#[test]
fn public_settings_surface_is_usable() {
let console_filter =
ksp_logging_lib::OutputFilter::new(ksp_logging_lib::LogFilterLevel::Debug, std::vec![TEST_TARGET.to_string()], std::vec!["logging".to_string()]);
let file_filter = ksp_logging_lib::OutputFilter::new(
ksp_logging_lib::LogFilterLevel::Error,
std::vec![TEST_TARGET.to_string()],
std::vec!["logging.runtime".to_string()],
);
let settings = ksp_logging_lib::LoggingSettings::new(
ksp_logging_lib::LogFilterLevel::Info,
ksp_logging_lib::SpanEvents::NewAndClose,
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stdout()),
std::option::Option::Some(ksp_logging_lib::FileSettings::new("logs", "ksp", ksp_logging_lib::FileRotation::Daily)),
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::new(
true,
ksp_logging_lib::ConsoleOutput::Stdout,
true,
ksp_logging_lib::LogFormat::Compact,
console_filter,
)),
std::vec![ksp_logging_lib::FileSettings::new(
"file.logging.error",
true,
"logs",
"error.jsonl",
ksp_logging_lib::FileRotation::Daily,
ksp_logging_lib::LogFormat::Json,
file_filter,
)],
)
.with_target_filter(ksp_logging_lib::TargetFilter::new(TEST_TARGET, ksp_logging_lib::LogFilterLevel::Trace));
assert!(settings.validate().is_ok());
assert_eq!(settings.default_filter(), ksp_logging_lib::LogFilterLevel::Info);
assert_eq!(settings.console().map(ksp_logging_lib::ConsoleSettings::output), std::option::Option::Some(ksp_logging_lib::ConsoleOutput::Stdout));
assert_eq!(settings.console().map(ksp_logging_lib::ConsoleSettings::format), std::option::Option::Some(ksp_logging_lib::LogFormat::Compact));
assert_eq!(settings.files().len(), 1);
assert_eq!(settings.files()[0].output_id(), "file.logging.error");
assert_eq!(settings.files()[0].format(), ksp_logging_lib::LogFormat::Json);
}
#[test]

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-logging-lib/tests/runtime.rs
// version: 4
// version: 5
//! Integration tests for global initialization, takeover filtering, non-blocking outputs and hot reload.
@@ -67,7 +67,7 @@ fn exercise_concurrent_reload(guard: &mut ksp_logging_lib::LoggingGuard, disable
ksp_logging_lib::LogFilterLevel::Off,
ksp_logging_lib::SpanEvents::Off,
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
std::option::Option::None,
std::vec::Vec::new(),
);
let quiet_reload = ksp_logging_lib::reinitialize(guard, &quiet_console);
assert!(quiet_reload.is_ok());
@@ -116,7 +116,7 @@ fn global_runtime_supports_takeover_non_blocking_outputs_hot_reload_and_single_i
ksp_logging_lib::LogFilterLevel::Info,
ksp_logging_lib::SpanEvents::Off,
std::option::Option::None,
std::option::Option::None,
std::vec::Vec::new(),
);
let initialize_result = ksp_logging_lib::initialize(&disabled);
assert!(initialize_result.is_ok());
@@ -132,7 +132,7 @@ fn global_runtime_supports_takeover_non_blocking_outputs_hot_reload_and_single_i
ksp_logging_lib::LogFilterLevel::Info,
ksp_logging_lib::SpanEvents::NewAndClose,
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
std::option::Option::None,
std::vec::Vec::new(),
)
.with_target_filter(ksp_logging_lib::TargetFilter::new(LOGGING_TARGET, ksp_logging_lib::LogFilterLevel::Trace));
let reload_result = ksp_logging_lib::reinitialize(&mut guard, &console_enabled);
@@ -151,7 +151,15 @@ fn global_runtime_supports_takeover_non_blocking_outputs_hot_reload_and_single_i
ksp_logging_lib::LogFilterLevel::Error,
ksp_logging_lib::SpanEvents::Full,
std::option::Option::None,
std::option::Option::Some(ksp_logging_lib::FileSettings::new(blocked_directory.as_path(), "invalid", ksp_logging_lib::FileRotation::Daily)),
std::vec![ksp_logging_lib::FileSettings::new(
"file.invalid",
true,
blocked_directory.as_path(),
"invalid",
ksp_logging_lib::FileRotation::Daily,
ksp_logging_lib::LogFormat::Human,
ksp_logging_lib::OutputFilter::unrestricted(),
)],
);
let failed_reload = ksp_logging_lib::reinitialize(&mut guard, &invalid_file);
assert!(failed_reload.is_err());
@@ -169,7 +177,15 @@ fn global_runtime_supports_takeover_non_blocking_outputs_hot_reload_and_single_i
ksp_logging_lib::LogFilterLevel::Info,
ksp_logging_lib::SpanEvents::Off,
std::option::Option::None,
std::option::Option::Some(ksp_logging_lib::FileSettings::new(log_directory.as_path(), "runtime-test.log", ksp_logging_lib::FileRotation::Never)),
std::vec![ksp_logging_lib::FileSettings::new(
"file.runtime",
true,
log_directory.as_path(),
"runtime-test.log",
ksp_logging_lib::FileRotation::Never,
ksp_logging_lib::LogFormat::Human,
ksp_logging_lib::OutputFilter::unrestricted(),
)],
);
let file_reload = ksp_logging_lib::reinitialize(&mut guard, &file_enabled);
assert!(file_reload.is_ok());

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-logging-lib/unit_tests/runtime.rs
// version: 5
// version: 6
#[test]
fn level_mapping_covers_all_ksp_levels() {
@@ -17,7 +17,7 @@ fn takeover_filter_silences_external_targets_and_applies_ksp_overrides() {
crate::LogFilterLevel::Info,
crate::SpanEvents::Off,
std::option::Option::Some(crate::ConsoleSettings::stdout()),
std::option::Option::None,
std::vec::Vec::new(),
)
.with_target_filter(crate::TargetFilter::new("ksp-logging-lib", crate::LogFilterLevel::Trace));
let filter = super::build_target_filter(&settings);
@@ -47,7 +47,7 @@ fn file_rotation_mapping_covers_supported_cadences() {
#[test]
fn disabled_runtime_has_no_layers_or_outputs() {
let settings = crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::option::Option::None);
let settings = crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::vec::Vec::new());
let result = super::prepare_runtime(&settings);
assert!(result.is_ok());
let prepared = match result {
@@ -65,7 +65,7 @@ fn console_runtime_composes_takeover_filter_before_formatter_and_owns_guard() {
crate::LogFilterLevel::Info,
crate::SpanEvents::Off,
std::option::Option::Some(crate::ConsoleSettings::stdout()),
std::option::Option::None,
std::vec::Vec::new(),
);
let result = super::prepare_runtime(&settings);
assert!(result.is_ok());
@@ -91,7 +91,7 @@ fn dropped_line_snapshots_add_saturating_by_sink() {
#[test]
fn takeover_filter_prefers_more_specific_ksp_prefixes_and_supports_off() {
let settings = crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::option::Option::None)
let settings = crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::vec::Vec::new())
.with_target_filter(crate::TargetFilter::new("ksp-store-", crate::LogFilterLevel::Debug))
.with_target_filter(crate::TargetFilter::new("ksp-store-lib", crate::LogFilterLevel::Trace))
.with_target_filter(crate::TargetFilter::new("ksp-wallet-lib", crate::LogFilterLevel::Off));
@@ -102,6 +102,43 @@ fn takeover_filter_prefers_more_specific_ksp_prefixes_and_supports_off() {
assert!(!filter.would_enable("ksp-wallet-lib", &tracing::Level::ERROR));
}
#[test]
fn pre_multi_sink_runtime_rejects_new_capabilities_instead_of_ignoring_them() {
let routed_console = crate::ConsoleSettings::new(
true,
crate::ConsoleOutput::Stdout,
true,
crate::LogFormat::Compact,
crate::OutputFilter::new(crate::LogFilterLevel::Debug, std::vec!["ksp-logging-lib".to_string()], std::vec!["logging".to_string()]),
);
let routed_settings =
crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::Some(routed_console), std::vec::Vec::new());
assert!(routed_settings.validate().is_ok());
assert!(super::prepare_runtime(&routed_settings).is_err());
let first_file = crate::FileSettings::new(
"file.first",
true,
"logs",
"first.log",
crate::FileRotation::Never,
crate::LogFormat::Human,
crate::OutputFilter::unrestricted(),
);
let second_file = crate::FileSettings::new(
"file.second",
true,
"logs",
"second.log",
crate::FileRotation::Never,
crate::LogFormat::Human,
crate::OutputFilter::unrestricted(),
);
let multi_file_settings =
crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::vec![first_file, second_file]);
assert!(multi_file_settings.validate().is_ok());
assert!(super::prepare_runtime(&multi_file_settings).is_err());
}
struct BlockingWriter {
first_write: bool,
started: std::sync::mpsc::SyncSender<()>,

View File

@@ -1,5 +1,17 @@
// file: crates/ksp-logging-lib/unit_tests/settings.rs
// version: 1
// version: 2
fn unrestricted_file(output_id: &str) -> crate::FileSettings {
return crate::FileSettings::new(
output_id,
true,
"logs",
format!("{output_id}.log"),
crate::FileRotation::Daily,
crate::LogFormat::Human,
crate::OutputFilter::unrestricted(),
);
}
#[test]
fn level_variants_are_distinct() {
@@ -10,6 +22,13 @@ fn level_variants_are_distinct() {
assert_ne!(crate::LogFilterLevel::Debug, crate::LogFilterLevel::Trace);
}
#[test]
fn format_variants_are_distinct() {
assert_ne!(crate::LogFormat::Human, crate::LogFormat::Compact);
assert_ne!(crate::LogFormat::Compact, crate::LogFormat::Pretty);
assert_ne!(crate::LogFormat::Pretty, crate::LogFormat::Json);
}
#[test]
fn target_filter_preserves_prefix_and_level() {
let filter = crate::TargetFilter::new("ksp-store-lib", crate::LogFilterLevel::Trace);
@@ -18,85 +37,188 @@ fn target_filter_preserves_prefix_and_level() {
}
#[test]
fn console_settings_select_requested_stream() {
assert_eq!(crate::ConsoleSettings::stdout().output(), crate::ConsoleOutput::Stdout);
assert_eq!(crate::ConsoleSettings::stderr().output(), crate::ConsoleOutput::Stderr);
fn output_filter_preserves_level_targets_and_domains() {
let filter = crate::OutputFilter::new(
crate::LogFilterLevel::Debug,
std::vec!["ksp-config-lib".to_string(), "ksp-logging-lib".to_string()],
std::vec!["config".to_string(), "logging.runtime".to_string()],
);
assert_eq!(filter.level(), crate::LogFilterLevel::Debug);
assert_eq!(filter.targets().len(), 2);
assert_eq!(filter.targets()[0], "ksp-config-lib");
assert_eq!(filter.targets()[1], "ksp-logging-lib");
assert_eq!(filter.domains().len(), 2);
assert_eq!(filter.domains()[0], "config");
assert_eq!(filter.domains()[1], "logging.runtime");
assert!(!filter.is_unrestricted());
assert!(crate::OutputFilter::unrestricted().is_unrestricted());
}
#[test]
fn file_settings_preserve_values() {
let settings = crate::FileSettings::new("logs", "ksp", crate::FileRotation::Daily);
assert_eq!(settings.directory(), std::path::Path::new("logs"));
assert_eq!(settings.file_name_prefix(), "ksp");
fn console_settings_preserve_enabled_stream_ansi_format_and_filter() {
let filter = crate::OutputFilter::new(crate::LogFilterLevel::Debug, std::vec!["*".to_string()], std::vec!["config".to_string()]);
let settings = crate::ConsoleSettings::new(true, crate::ConsoleOutput::Stderr, true, crate::LogFormat::Compact, filter.clone());
assert!(settings.enabled());
assert_eq!(settings.output(), crate::ConsoleOutput::Stderr);
assert!(settings.ansi());
assert_eq!(settings.format(), crate::LogFormat::Compact);
assert_eq!(settings.filter(), &filter);
}
#[test]
fn compatibility_console_constructors_are_unrestricted_human_and_non_ansi() {
let stdout = crate::ConsoleSettings::stdout();
let stderr = crate::ConsoleSettings::stderr();
assert_eq!(stdout.output(), crate::ConsoleOutput::Stdout);
assert_eq!(stderr.output(), crate::ConsoleOutput::Stderr);
assert!(stdout.enabled());
assert!(!stdout.ansi());
assert_eq!(stdout.format(), crate::LogFormat::Human);
assert!(stdout.filter().is_unrestricted());
}
#[test]
fn file_settings_preserve_multi_output_contract() {
let filter = crate::OutputFilter::new(crate::LogFilterLevel::Error, std::vec!["ksp-config-lib".to_string()], std::vec!["config".to_string()]);
let settings =
crate::FileSettings::new("file.config.error", true, "logs/config", "error.jsonl", crate::FileRotation::Daily, crate::LogFormat::Json, filter.clone());
assert_eq!(settings.output_id(), "file.config.error");
assert!(settings.enabled());
assert_eq!(settings.directory(), std::path::Path::new("logs/config"));
assert_eq!(settings.file_name_prefix(), "error.jsonl");
assert_eq!(settings.rotation(), crate::FileRotation::Daily);
assert_eq!(settings.format(), crate::LogFormat::Json);
assert!(!settings.ansi());
assert_eq!(settings.filter(), &filter);
}
#[test]
fn logging_settings_preserve_explicit_values() {
fn logging_settings_represent_multiple_file_outputs() {
let first = unrestricted_file("file.debug");
let second = crate::FileSettings::new(
"file.config.error",
false,
"logs/config",
"error.jsonl",
crate::FileRotation::Hourly,
crate::LogFormat::Json,
crate::OutputFilter::new(crate::LogFilterLevel::Error, std::vec!["ksp-config-lib".to_string()], std::vec!["config".to_string()]),
);
let settings = crate::LoggingSettings::new(
crate::LogFilterLevel::Info,
crate::SpanEvents::NewAndClose,
std::option::Option::Some(crate::ConsoleSettings::stdout()),
std::option::Option::Some(crate::FileSettings::new("logs", "ksp", crate::FileRotation::Hourly)),
std::vec![first, second],
)
.with_target_filter(crate::TargetFilter::new("ksp-store-lib", crate::LogFilterLevel::Trace));
assert_eq!(settings.default_filter(), crate::LogFilterLevel::Info);
assert_eq!(settings.span_events(), crate::SpanEvents::NewAndClose);
assert_eq!(settings.target_filters().len(), 1);
assert_eq!(settings.target_filters()[0].target_prefix(), "ksp-store-lib");
assert_eq!(settings.console(), std::option::Option::Some(&crate::ConsoleSettings::stdout()));
assert_eq!(settings.file().map(crate::FileSettings::rotation), std::option::Option::Some(crate::FileRotation::Hourly));
assert_eq!(settings.files().len(), 2);
assert_eq!(settings.files()[0].output_id(), "file.debug");
assert_eq!(settings.files()[1].format(), crate::LogFormat::Json);
assert_eq!(settings.console().map(crate::ConsoleSettings::output), std::option::Option::Some(crate::ConsoleOutput::Stdout));
assert!(settings.validate().is_ok());
}
#[test]
fn validation_rejects_empty_target_prefix() {
let settings = crate::LoggingSettings::new(
crate::LogFilterLevel::Info,
crate::SpanEvents::Off,
std::option::Option::Some(crate::ConsoleSettings::stdout()),
std::option::Option::None,
)
.with_target_filter(crate::TargetFilter::new("", crate::LogFilterLevel::Debug));
assert!(settings.validate().is_err());
fn validation_rejects_empty_or_external_global_target_prefix() {
let empty = crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::vec::Vec::new())
.with_target_filter(crate::TargetFilter::new("", crate::LogFilterLevel::Debug));
let external = crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::vec::Vec::new())
.with_target_filter(crate::TargetFilter::new("sqlx", crate::LogFilterLevel::Debug));
assert!(empty.validate().is_err());
assert!(external.validate().is_err());
}
#[test]
fn validation_rejects_external_target_prefix() {
let settings = crate::LoggingSettings::new(
crate::LogFilterLevel::Info,
crate::SpanEvents::Off,
std::option::Option::Some(crate::ConsoleSettings::stdout()),
std::option::Option::None,
)
.with_target_filter(crate::TargetFilter::new("sqlx", crate::LogFilterLevel::Debug));
assert!(settings.validate().is_err());
}
#[test]
fn validation_rejects_empty_file_prefix() {
let settings = crate::LoggingSettings::new(
fn validation_rejects_invalid_file_identity_and_duplicate_output_ids() {
let invalid_id =
crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::vec![unrestricted_file("File.Debug")]);
let duplicate_id = crate::LoggingSettings::new(
crate::LogFilterLevel::Info,
crate::SpanEvents::Off,
std::option::Option::None,
std::option::Option::Some(crate::FileSettings::new("logs", "", crate::FileRotation::Never)),
std::vec![unrestricted_file("file.debug"), unrestricted_file("file.debug")],
);
assert!(settings.validate().is_err());
assert!(invalid_id.validate().is_err());
assert!(duplicate_id.validate().is_err());
}
#[test]
fn validation_accepts_ksp_outputs_and_filters() {
let settings = crate::LoggingSettings::new(
crate::LogFilterLevel::Info,
crate::SpanEvents::Full,
std::option::Option::Some(crate::ConsoleSettings::stderr()),
std::option::Option::Some(crate::FileSettings::new("logs", "worker", crate::FileRotation::Daily)),
fn validation_rejects_empty_file_paths_and_persistent_ansi() {
let empty_directory = crate::FileSettings::new(
"file.empty.dir",
true,
"",
"debug.log",
crate::FileRotation::Never,
crate::LogFormat::Human,
crate::OutputFilter::unrestricted(),
);
let empty_prefix = crate::FileSettings::new(
"file.empty.prefix",
true,
"logs",
"",
crate::FileRotation::Never,
crate::LogFormat::Human,
crate::OutputFilter::unrestricted(),
);
let ansi_file = crate::FileSettings::new(
"file.ansi",
true,
"logs",
"ansi.log",
crate::FileRotation::Never,
crate::LogFormat::Human,
crate::OutputFilter::unrestricted(),
)
.with_target_filter(crate::TargetFilter::new("ksp-worker-", crate::LogFilterLevel::Debug));
assert!(settings.validate().is_ok());
.with_ansi(true);
assert!(
crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::vec![empty_directory])
.validate()
.is_err()
);
assert!(
crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::vec![empty_prefix])
.validate()
.is_err()
);
assert!(
crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::vec![ansi_file])
.validate()
.is_err()
);
}
#[test]
fn settings_allow_logging_to_be_disabled() {
let settings = crate::LoggingSettings::new(crate::LogFilterLevel::Off, crate::SpanEvents::Off, std::option::Option::None, std::option::Option::None);
fn validation_rejects_invalid_output_selectors() {
let external_target = crate::OutputFilter::new(crate::LogFilterLevel::Debug, std::vec!["sqlx".to_string()], std::vec!["*".to_string()]);
let wildcard_mix =
crate::OutputFilter::new(crate::LogFilterLevel::Debug, std::vec!["*".to_string(), "ksp-config-lib".to_string()], std::vec!["*".to_string()]);
let duplicate_domain =
crate::OutputFilter::new(crate::LogFilterLevel::Debug, std::vec!["*".to_string()], std::vec!["config".to_string(), "config".to_string()]);
for filter in [external_target, wildcard_mix, duplicate_domain] {
let console = crate::ConsoleSettings::new(true, crate::ConsoleOutput::Stdout, false, crate::LogFormat::Human, filter);
let settings =
crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::Some(console), std::vec::Vec::new());
assert!(settings.validate().is_err());
}
}
#[test]
fn settings_allow_all_outputs_to_be_disabled() {
let console = crate::ConsoleSettings::new(false, crate::ConsoleOutput::Stderr, true, crate::LogFormat::Pretty, crate::OutputFilter::unrestricted());
let file = crate::FileSettings::new(
"file.disabled",
false,
"logs",
"disabled.jsonl",
crate::FileRotation::Daily,
crate::LogFormat::Json,
crate::OutputFilter::new(crate::LogFilterLevel::Error, std::vec!["ksp-config-lib".to_string()], std::vec!["config".to_string()]),
);
let settings = crate::LoggingSettings::new(crate::LogFilterLevel::Off, crate::SpanEvents::Off, std::option::Option::Some(console), std::vec![file]);
assert!(settings.validate().is_ok());
}