v0.1.2-pre.002

This commit is contained in:
2026-08-14 18:03:20 +02:00
parent bd8401c0d0
commit 7b4444fb07
14 changed files with 1067 additions and 26 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml
# version: 26
# version: 27
[workspace]
resolver = "3"
members = ["crates/ksp-core-lib"]
members = ["crates/ksp-core-lib", "crates/ksp-logging-lib"]
[workspace.package]
version = "0.1.2-pre.1"
version = "0.1.2-pre.2"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
@@ -15,6 +15,7 @@ publish = false
[workspace.dependencies]
solana-pubkey = { version = "^4.3", default-features = false }
tracing = { version = "^0.1", default-features = false, features = ["std"] }
[workspace.lints.rust]
missing_docs = "warn"

View File

@@ -0,0 +1,15 @@
# file: crates/ksp-logging-lib/Cargo.toml
# version: 1
[package]
name = "ksp-logging-lib"
version.workspace = true
edition.workspace = true
repository.workspace = true
[dependencies]
ksp-core-lib = { path = "../ksp-core-lib" }
tracing.workspace = true
[lints]
workspace = true

View File

@@ -0,0 +1,5 @@
// file: crates/ksp-logging-lib/src/error.rs
// version: 1
/// Error code used when runtime logging settings are invalid.
pub const ERROR_CODE_INVALID_SETTINGS: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "invalid_settings");

View File

@@ -0,0 +1,42 @@
// file: crates/ksp-logging-lib/src/lib.rs
// version: 1
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
//! 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. Runtime subscriber initialization, filtering, outputs and hot reload are added by the following `0.1.2` prereleases.
mod error;
mod macros;
mod settings;
mod span;
/// Error code used when runtime logging settings are invalid.
pub use self::error::ERROR_CODE_INVALID_SETTINGS;
/// Console stream selected for human-readable logs.
pub use self::settings::ConsoleOutput;
/// Runtime settings for the optional console output.
pub use self::settings::ConsoleSettings;
/// Rotation cadence for the optional file output.
pub use self::settings::FileRotation;
/// Runtime settings for the optional file output.
pub use self::settings::FileSettings;
/// Runtime filter level used by KSP logging settings.
pub use self::settings::LogFilterLevel;
/// Complete runtime settings consumed by Logging initialization and reload.
pub use self::settings::LoggingSettings;
/// Lifecycle events emitted for spans by the formatted subscriber.
pub use self::settings::SpanEvents;
/// Per-target filter override owned by Logging.
pub use self::settings::TargetFilter;
/// KSP-owned handle to a tracing span.
pub use self::span::Span;
/// Instruments an asynchronous future with a KSP span.
pub use self::span::instrument;
#[doc(hidden)]
/// Internal macro bridge. KSP consumers must not use this reexport directly.
pub extern crate tracing as __private_tracing;

View File

@@ -0,0 +1,97 @@
// file: crates/ksp-logging-lib/src/macros.rs
// version: 1
/// Emits a KSP error event with an explicit owning target.
#[macro_export]
macro_rules! error {
(target: $target:expr, $($argument:tt)+) => {{
$crate::__private_tracing::error!(target: $target, $($argument)+);
}};
}
/// Emits a KSP warning event with an explicit owning target.
#[macro_export]
macro_rules! warn {
(target: $target:expr, $($argument:tt)+) => {{
$crate::__private_tracing::warn!(target: $target, $($argument)+);
}};
}
/// Emits a KSP informational event with an explicit owning target.
#[macro_export]
macro_rules! info {
(target: $target:expr, $($argument:tt)+) => {{
$crate::__private_tracing::info!(target: $target, $($argument)+);
}};
}
/// Emits a KSP debug event with an explicit owning target.
#[macro_export]
macro_rules! debug {
(target: $target:expr, $($argument:tt)+) => {{
$crate::__private_tracing::debug!(target: $target, $($argument)+);
}};
}
/// Emits a KSP trace event with an explicit owning target.
#[macro_export]
macro_rules! trace {
(target: $target:expr, $($argument:tt)+) => {{
$crate::__private_tracing::trace!(target: $target, $($argument)+);
}};
}
/// Creates a KSP error span with an explicit owning target.
#[macro_export]
macro_rules! error_span {
(target: $target:expr, $name:expr) => {{
$crate::Span::__from_tracing($crate::__private_tracing::error_span!(target: $target, $name))
}};
(target: $target:expr, $name:expr, $($field:tt)+) => {{
$crate::Span::__from_tracing($crate::__private_tracing::error_span!(target: $target, $name, $($field)+))
}};
}
/// Creates a KSP warning span with an explicit owning target.
#[macro_export]
macro_rules! warn_span {
(target: $target:expr, $name:expr) => {{
$crate::Span::__from_tracing($crate::__private_tracing::warn_span!(target: $target, $name))
}};
(target: $target:expr, $name:expr, $($field:tt)+) => {{
$crate::Span::__from_tracing($crate::__private_tracing::warn_span!(target: $target, $name, $($field)+))
}};
}
/// Creates a KSP informational span with an explicit owning target.
#[macro_export]
macro_rules! info_span {
(target: $target:expr, $name:expr) => {{
$crate::Span::__from_tracing($crate::__private_tracing::info_span!(target: $target, $name))
}};
(target: $target:expr, $name:expr, $($field:tt)+) => {{
$crate::Span::__from_tracing($crate::__private_tracing::info_span!(target: $target, $name, $($field)+))
}};
}
/// Creates a KSP debug span with an explicit owning target.
#[macro_export]
macro_rules! debug_span {
(target: $target:expr, $name:expr) => {{
$crate::Span::__from_tracing($crate::__private_tracing::debug_span!(target: $target, $name))
}};
(target: $target:expr, $name:expr, $($field:tt)+) => {{
$crate::Span::__from_tracing($crate::__private_tracing::debug_span!(target: $target, $name, $($field)+))
}};
}
/// Creates a KSP trace span with an explicit owning target.
#[macro_export]
macro_rules! trace_span {
(target: $target:expr, $name:expr) => {{
$crate::Span::__from_tracing($crate::__private_tracing::trace_span!(target: $target, $name))
}};
(target: $target:expr, $name:expr, $($field:tt)+) => {{
$crate::Span::__from_tracing($crate::__private_tracing::trace_span!(target: $target, $name, $($field)+))
}};
}

View File

@@ -0,0 +1,233 @@
// file: crates/ksp-logging-lib/src/settings.rs
// version: 1
/// Runtime filter level used by KSP logging settings.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum LogFilterLevel {
/// Disables matching logging events and spans.
Off,
/// Enables only error-level events and spans.
Error,
/// Enables warning and error events and spans.
Warn,
/// Enables informational, warning and error events and spans.
Info,
/// Enables debug and less verbose events and spans.
Debug,
/// Enables all KSP logging events and spans.
Trace,
}
/// Per-target filter override owned by Logging.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TargetFilter {
target_prefix: std::string::String,
level: crate::LogFilterLevel,
}
impl TargetFilter {
/// Creates a filter override for a KSP target prefix.
#[must_use]
pub fn new(target_prefix: impl std::convert::Into<std::string::String>, level: crate::LogFilterLevel) -> Self {
return Self { target_prefix: target_prefix.into(), level };
}
/// Returns the configured target prefix.
#[must_use]
pub fn target_prefix(&self) -> &str {
return self.target_prefix.as_str();
}
/// Returns the configured filter level.
#[must_use]
pub const fn level(&self) -> crate::LogFilterLevel {
return self.level;
}
}
/// Lifecycle events emitted for spans by the formatted subscriber.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum SpanEvents {
/// Does not synthesize span lifecycle events.
Off,
/// Emits span creation and closure events for timing-oriented diagnostics.
NewAndClose,
/// Emits all supported span lifecycle events.
Full,
}
/// Console stream selected for human-readable logs.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum ConsoleOutput {
/// Writes console logs to standard output.
Stdout,
/// Writes console logs to standard error.
Stderr,
}
/// Runtime settings for the optional console output.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct ConsoleSettings {
output: crate::ConsoleOutput,
}
impl ConsoleSettings {
/// Creates console settings targeting standard output.
#[must_use]
pub const fn stdout() -> Self {
return Self { output: crate::ConsoleOutput::Stdout };
}
/// Creates console settings targeting standard error.
#[must_use]
pub const fn stderr() -> Self {
return Self { output: crate::ConsoleOutput::Stderr };
}
/// Returns the selected console stream.
#[must_use]
pub const fn output(&self) -> crate::ConsoleOutput {
return self.output;
}
}
/// Rotation cadence for the optional file output.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum FileRotation {
/// Keeps a single non-rotating file.
Never,
/// Rotates the file every hour.
Hourly,
/// Rotates the file every day.
Daily,
}
/// Runtime settings for the optional file output.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FileSettings {
directory: std::path::PathBuf,
file_name_prefix: std::string::String,
rotation: crate::FileRotation,
}
impl FileSettings {
/// Creates file output settings.
#[must_use]
pub fn new(
directory: impl std::convert::Into<std::path::PathBuf>,
file_name_prefix: impl std::convert::Into<std::string::String>,
rotation: crate::FileRotation,
) -> Self {
return Self { directory: directory.into(), file_name_prefix: file_name_prefix.into(), rotation };
}
/// Returns the directory containing log files.
#[must_use]
pub fn directory(&self) -> &std::path::Path {
return self.directory.as_path();
}
/// Returns the file-name prefix passed to the file appender.
#[must_use]
pub fn file_name_prefix(&self) -> &str {
return self.file_name_prefix.as_str();
}
/// Returns the selected file rotation cadence.
#[must_use]
pub const fn rotation(&self) -> crate::FileRotation {
return self.rotation;
}
}
/// Complete runtime settings consumed by `ksp-logging-lib` initialization and reload.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LoggingSettings {
default_filter: crate::LogFilterLevel,
target_filters: std::vec::Vec<crate::TargetFilter>,
span_events: crate::SpanEvents,
console: std::option::Option<crate::ConsoleSettings>,
file: std::option::Option<crate::FileSettings>,
}
impl LoggingSettings {
/// Creates explicit Logging settings without any 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>,
) -> Self {
return Self { default_filter, target_filters: std::vec::Vec::new(), span_events, console, file };
}
/// Adds one target-prefix override 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.
#[must_use]
pub const fn default_filter(&self) -> crate::LogFilterLevel {
return self.default_filter;
}
/// Returns target-prefix overrides in insertion order.
#[must_use]
pub fn target_filters(&self) -> &[crate::TargetFilter] {
return self.target_filters.as_slice();
}
/// Returns the selected span lifecycle event policy.
#[must_use]
pub const fn span_events(&self) -> crate::SpanEvents {
return self.span_events;
}
/// Returns console settings when console output is enabled.
#[must_use]
pub fn console(&self) -> std::option::Option<&crate::ConsoleSettings> {
return self.console.as_ref();
}
/// Returns file settings when file output is enabled.
#[must_use]
pub fn file(&self) -> std::option::Option<&crate::FileSettings> {
return self.file.as_ref();
}
/// Validates backend-independent invariants of the runtime settings.
pub fn validate(&self) -> ksp_core_lib::Result<()> {
for target_filter in &self.target_filters {
if target_filter.target_prefix().is_empty() {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "target filter prefix must not be empty")
.with_context("field", "target_filters.target_prefix"),
);
}
if !target_filter.target_prefix().starts_with("ksp-") {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "target filter prefix must identify a KSP-owned target")
.with_context("field", "target_filters.target_prefix")
.with_context("target_prefix", target_filter.target_prefix()),
);
}
}
if let std::option::Option::Some(file) = self.file.as_ref() {
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", "file.file_name_prefix"),
);
}
}
return std::result::Result::Ok(());
}
}
#[cfg(test)]
#[path = "../unit_tests/settings.rs"]
mod tests;

View File

@@ -0,0 +1,42 @@
// file: crates/ksp-logging-lib/src/span.rs
// version: 1
/// KSP-owned handle to a tracing span.
#[derive(Clone, Debug)]
pub struct Span {
inner: tracing::Span,
}
impl Span {
/// Runs synchronous work while this span is entered.
pub fn in_scope<T>(&self, operation: impl std::ops::FnOnce() -> T) -> T {
return self.inner.in_scope(operation);
}
#[doc(hidden)]
/// Constructs the KSP span wrapper for macro expansion support.
#[must_use]
pub fn __from_tracing(inner: tracing::Span) -> Self {
return Self { inner };
}
/// Consumes this wrapper and returns the internal tracing span.
pub(crate) fn into_tracing(self) -> tracing::Span {
return self.inner;
}
}
/// Instruments an asynchronous future with a KSP span.
///
/// The span is entered whenever the future is polled and exited whenever it yields, so no enter guard is held across an `.await` point.
#[must_use]
pub fn instrument<F>(span: crate::Span, future: F) -> impl std::future::Future<Output = F::Output>
where
F: std::future::Future,
{
return tracing::Instrument::instrument(future, span.into_tracing());
}
#[cfg(test)]
#[path = "../unit_tests/span.rs"]
mod tests;

View File

@@ -0,0 +1,157 @@
// file: crates/ksp-logging-lib/tests/callsite.rs
// version: 1
const TEST_TARGET: &str = "ksp-logging-lib";
#[derive(Clone, Debug, Eq, PartialEq)]
struct CapturedMetadata {
target: std::string::String,
file: std::option::Option<std::string::String>,
module_path: std::option::Option<std::string::String>,
line: std::option::Option<u32>,
is_event: bool,
is_span: bool,
}
impl CapturedMetadata {
fn from_metadata(metadata: &tracing::Metadata<'_>) -> Self {
return Self {
target: metadata.target().to_owned(),
file: metadata.file().map(str::to_owned),
module_path: metadata.module_path().map(str::to_owned),
line: metadata.line(),
is_event: metadata.is_event(),
is_span: metadata.is_span(),
};
}
}
#[derive(Clone)]
struct CaptureSubscriber {
captured: std::sync::Arc<std::sync::Mutex<std::vec::Vec<CapturedMetadata>>>,
enters: std::sync::Arc<std::sync::atomic::AtomicU64>,
exits: std::sync::Arc<std::sync::atomic::AtomicU64>,
next_id: std::sync::Arc<std::sync::atomic::AtomicU64>,
}
impl CaptureSubscriber {
fn new(
captured: std::sync::Arc<std::sync::Mutex<std::vec::Vec<CapturedMetadata>>>,
enters: std::sync::Arc<std::sync::atomic::AtomicU64>,
exits: std::sync::Arc<std::sync::atomic::AtomicU64>,
) -> Self {
return Self { captured, enters, exits, next_id: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(1)) };
}
fn capture(&self, metadata: &tracing::Metadata<'_>) {
let lock = self.captured.lock();
if let std::result::Result::Ok(mut values) = lock {
values.push(CapturedMetadata::from_metadata(metadata));
}
}
}
impl tracing::Subscriber for CaptureSubscriber {
fn enabled(&self, _metadata: &tracing::Metadata<'_>) -> bool {
return true;
}
fn new_span(&self, span: &tracing::span::Attributes<'_>) -> tracing::span::Id {
self.capture(span.metadata());
let id = self.next_id.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
return tracing::span::Id::from_u64(id);
}
fn record(&self, _span: &tracing::span::Id, _values: &tracing::span::Record<'_>) {
return;
}
fn record_follows_from(&self, _span: &tracing::span::Id, _follows: &tracing::span::Id) {
return;
}
fn event(&self, event: &tracing::Event<'_>) {
self.capture(event.metadata());
return;
}
fn enter(&self, _span: &tracing::span::Id) {
self.enters.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
return;
}
fn exit(&self, _span: &tracing::span::Id) {
self.exits.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
return;
}
}
fn captured_values(captured: &std::sync::Arc<std::sync::Mutex<std::vec::Vec<CapturedMetadata>>>) -> std::vec::Vec<CapturedMetadata> {
let lock = captured.lock();
return match lock {
std::result::Result::Ok(values) => values.clone(),
std::result::Result::Err(error) => error.into_inner().clone(),
};
}
#[test]
fn event_macro_preserves_consumer_callsite() {
let captured = std::sync::Arc::new(std::sync::Mutex::new(std::vec::Vec::new()));
let enters = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
let exits = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
let subscriber = CaptureSubscriber::new(captured.clone(), enters, exits);
let expected_line = line!() + 2;
tracing::subscriber::with_default(subscriber, || {
ksp_logging_lib::info!(target: TEST_TARGET, domain = "logging", "callsite event");
return;
});
let values = captured_values(&captured);
assert_eq!(values.len(), 1);
assert_eq!(values[0].target, TEST_TARGET);
assert_eq!(values[0].file.as_deref(), std::option::Option::Some(file!()));
assert_eq!(values[0].module_path.as_deref(), std::option::Option::Some(module_path!()));
assert_eq!(values[0].line, std::option::Option::Some(expected_line));
assert!(values[0].is_event);
assert!(!values[0].is_span);
}
#[test]
fn span_macro_preserves_consumer_callsite() {
let captured = std::sync::Arc::new(std::sync::Mutex::new(std::vec::Vec::new()));
let enters = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
let exits = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
let subscriber = CaptureSubscriber::new(captured.clone(), enters, exits);
let expected_line = line!() + 2;
tracing::subscriber::with_default(subscriber, || {
let _span = ksp_logging_lib::trace_span!(target: TEST_TARGET, "callsite_span", component = "test");
return;
});
let values = captured_values(&captured);
assert_eq!(values.len(), 1);
assert_eq!(values[0].target, TEST_TARGET);
assert_eq!(values[0].file.as_deref(), std::option::Option::Some(file!()));
assert_eq!(values[0].module_path.as_deref(), std::option::Option::Some(module_path!()));
assert_eq!(values[0].line, std::option::Option::Some(expected_line));
assert!(!values[0].is_event);
assert!(values[0].is_span);
}
#[test]
fn async_instrumentation_enters_and_exits_span_during_poll() {
let captured = std::sync::Arc::new(std::sync::Mutex::new(std::vec::Vec::new()));
let enters = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
let exits = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
let subscriber = CaptureSubscriber::new(captured, std::sync::Arc::clone(&enters), std::sync::Arc::clone(&exits));
tracing::subscriber::with_default(subscriber, || {
let span = ksp_logging_lib::trace_span!(target: TEST_TARGET, "async_poll_span", domain = "logging");
let future = ksp_logging_lib::instrument(span, std::future::ready(42_u32));
let mut future = std::boxed::Box::pin(future);
let waker = std::task::Waker::noop();
let mut context = std::task::Context::from_waker(waker);
let poll = std::future::Future::poll(future.as_mut(), &mut context);
assert_eq!(poll, std::task::Poll::Ready(42_u32));
return;
});
assert_eq!(enters.load(std::sync::atomic::Ordering::Relaxed), 1);
assert_eq!(exits.load(std::sync::atomic::Ordering::Relaxed), 1);
}

View File

@@ -0,0 +1,52 @@
// file: crates/ksp-logging-lib/tests/public_api.rs
// version: 1
const TEST_TARGET: &str = "ksp-logging-lib";
#[test]
fn public_settings_surface_is_usable() {
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)),
)
.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));
}
#[test]
fn public_event_macros_are_usable() {
ksp_logging_lib::error!(target: TEST_TARGET, operation = "public_api", "error event");
ksp_logging_lib::warn!(target: TEST_TARGET, operation = "public_api", "warn event");
ksp_logging_lib::info!(target: TEST_TARGET, operation = "public_api", "info event");
ksp_logging_lib::debug!(target: TEST_TARGET, operation = "public_api", "debug event");
ksp_logging_lib::trace!(target: TEST_TARGET, operation = "public_api", "trace event");
}
#[test]
fn public_span_surface_is_usable_for_sync_and_async() {
let span = ksp_logging_lib::trace_span!(target: TEST_TARGET, "public_sync", domain = "logging");
let value = span.in_scope(|| -> u32 {
return 7;
});
assert_eq!(value, 7);
let async_span = ksp_logging_lib::debug_span!(target: TEST_TARGET, "public_async", component = "test");
let future = ksp_logging_lib::instrument(async_span, std::future::ready(9_u32));
let mut future = std::boxed::Box::pin(future);
let waker = std::task::Waker::noop();
let mut context = std::task::Context::from_waker(waker);
let poll = std::future::Future::poll(future.as_mut(), &mut context);
assert_eq!(poll, std::task::Poll::Ready(9_u32));
}
#[test]
fn all_span_levels_are_usable() {
let _error = ksp_logging_lib::error_span!(target: TEST_TARGET, "error_span");
let _warn = ksp_logging_lib::warn_span!(target: TEST_TARGET, "warn_span");
let _info = ksp_logging_lib::info_span!(target: TEST_TARGET, "info_span");
let _debug = ksp_logging_lib::debug_span!(target: TEST_TARGET, "debug_span");
let _trace = ksp_logging_lib::trace_span!(target: TEST_TARGET, "trace_span");
}

View File

@@ -0,0 +1,102 @@
// file: crates/ksp-logging-lib/unit_tests/settings.rs
// version: 1
#[test]
fn level_variants_are_distinct() {
assert_ne!(crate::LogFilterLevel::Off, crate::LogFilterLevel::Error);
assert_ne!(crate::LogFilterLevel::Error, crate::LogFilterLevel::Warn);
assert_ne!(crate::LogFilterLevel::Warn, crate::LogFilterLevel::Info);
assert_ne!(crate::LogFilterLevel::Info, crate::LogFilterLevel::Debug);
assert_ne!(crate::LogFilterLevel::Debug, crate::LogFilterLevel::Trace);
}
#[test]
fn target_filter_preserves_prefix_and_level() {
let filter = crate::TargetFilter::new("ksp-store-lib", crate::LogFilterLevel::Trace);
assert_eq!(filter.target_prefix(), "ksp-store-lib");
assert_eq!(filter.level(), crate::LogFilterLevel::Trace);
}
#[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);
}
#[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");
assert_eq!(settings.rotation(), crate::FileRotation::Daily);
}
#[test]
fn logging_settings_preserve_explicit_values() {
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)),
)
.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));
}
#[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());
}
#[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(
crate::LogFilterLevel::Info,
crate::SpanEvents::Off,
std::option::Option::None,
std::option::Option::Some(crate::FileSettings::new("logs", "", crate::FileRotation::Never)),
);
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]
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);
assert!(settings.validate().is_ok());
}

View File

@@ -0,0 +1,22 @@
// file: crates/ksp-logging-lib/unit_tests/span.rs
// version: 1
#[test]
fn synchronous_scope_returns_operation_value() {
let span = crate::Span::__from_tracing(tracing::info_span!("unit_test_span"));
let value = span.in_scope(|| -> u32 {
return 42;
});
assert_eq!(value, 42);
}
#[test]
fn async_instrumentation_returns_future_output() {
let span = crate::Span::__from_tracing(tracing::info_span!("unit_test_async_span"));
let future = crate::instrument(span, std::future::ready(42_u32));
let mut future = std::boxed::Box::pin(future);
let waker = std::task::Waker::noop();
let mut context = std::task::Context::from_waker(waker);
let poll = std::future::Future::poll(future.as_mut(), &mut context);
assert_eq!(poll, std::task::Poll::Ready(42_u32));
}

270
deltas/0.1.2/pre.002.md Normal file
View File

@@ -0,0 +1,270 @@
<!-- file: deltas/0.1.2/pre.002.md -->
<!-- version: 1 -->
# Delta 0.1.2-pre.002
## Base requise
Livraison précédente validée :
```text
0.1.2-pre.001-fix.001
```
Cette tranche applique le plan corrigé de `pre.001` et ouvre le développement fonctionnel de `ksp-logging-lib` sans encore installer le subscriber runtime.
## Objectif
Créer la première surface fonctionnelle de Logging :
- créer `crates/ksp-logging-lib` et l'ajouter au workspace ;
- dépendre de `ksp-core-lib` pour le contrat commun d'erreur ;
- ajouter uniquement `tracing` parmi les dépendances de la stack de logging ;
- définir les settings runtime propres à Logging, indépendants de Config ;
- exposer les cinq niveaux d'événements par macros KSP avec `target:` explicite ;
- exposer les cinq niveaux de spans KSP ;
- fournir une abstraction `Span` KSP pour les scopes synchrones ;
- fournir `instrument(span, future)` pour l'instrumentation async sans demander au consumer d'utiliser `tracing::Instrument` ;
- vérifier par tests la préservation du callsite événement/span et le cycle enter/exit d'une future instrumentée.
Le subscriber global, le takeover effectif, le filtering runtime, les sorties console/fichier non bloquantes, les guards et le hot reload restent réservés aux prereleases suivantes conformément au plan.
## Version Cargo
`workspace.package.version` passe de :
```text
0.1.2-pre.1
```
à :
```text
0.1.2-pre.2
```
L'identifiant de livraison reste :
```text
0.1.2-pre.002
```
Le header du `Cargo.toml` racine passe de version 26 à 27.
## Dépendances
`tracing` est ajouté à la racine sous `[workspace.dependencies]` :
```toml
tracing = { version = "^0.1", default-features = false, features = ["std"] }
```
`ksp-logging-lib` le consomme avec :
```toml
tracing.workspace = true
```
L'audit de la publication actuelle retient `tracing 0.1.44`. Les default features ne sont pas activées : `attributes` n'est pas nécessaire à cette tranche, car KSP n'utilise pas `#[instrument]`. La feature `std` suffit à la façade retenue et aux tests de subscriber local.
`tracing-subscriber` et `tracing-appender` ne sont pas ajoutés dans `pre.002` : ils ne sont pas encore consommés par du code runtime.
## Settings runtime
La surface publique introduit :
```text
LogFilterLevel
TargetFilter
SpanEvents
ConsoleOutput
ConsoleSettings
FileRotation
FileSettings
LoggingSettings
```
Ces types :
- appartiennent à `ksp-logging-lib` ;
- ne lisent aucun document Config ;
- ne consultent aucune variable d'environnement ;
- ne dépendent pas de `ksp-config-lib` ;
- utilisent des champs privés et une construction/getters explicites.
Une configuration sans console ni fichier est valide et représente un logging KSP désactivé. Les validations actuelles rejettent uniquement les ambiguïtés propres au contrat déjà fixé, notamment les préfixes de target vides/externes et un préfixe de fichier vide.
## Façade événements
Les macros crate-root suivantes sont introduites :
```text
ksp_logging_lib::error!
ksp_logging_lib::warn!
ksp_logging_lib::info!
ksp_logging_lib::debug!
ksp_logging_lib::trace!
```
Leur syntaxe KSP exige `target:` explicitement. Elles délèguent directement aux macros `tracing` au point d'expansion afin que les métadonnées `file`, `module_path` et `line` correspondent au callsite consumer et non à une fonction wrapper dans Logging.
Un bridge `tracing` public mais caché de la documentation est nécessaire à l'expansion des macros depuis les crates consommatrices. Il est réservé à l'implémentation des macros ; `DEP-LOG-009` interdit son usage direct comme API consumer.
## Spans synchrones et async
Les macros suivantes sont introduites :
```text
ksp_logging_lib::error_span!
ksp_logging_lib::warn_span!
ksp_logging_lib::info_span!
ksp_logging_lib::debug_span!
ksp_logging_lib::trace_span!
```
Elles exigent également `target:` explicitement et retournent `ksp_logging_lib::Span`.
Pour le synchrone :
```text
Span::in_scope(operation)
```
entre dans le span pendant le scope puis en sort à la fin du scope.
Pour l'async :
```text
ksp_logging_lib::instrument(span, future)
```
retourne une `Future` opaque instrumentée. Le span est entré pendant chaque poll de la future et quitté lorsque ce poll rend la main ; aucun enter guard KSP n'est destiné à être conservé à travers `.await`.
Cette surface prépare les diagnostics de durée `NEW/CLOSE`, `busy` et `idle` qui seront activés par le formatter/subscriber dans les tranches runtime suivantes.
## Erreurs
`ksp-logging-lib` utilise :
```text
ksp_core_lib::Result<T>
ksp_core_lib::Error
ksp_core_lib::ErrorCode
```
Le premier code propre à Logging est :
```text
logging.invalid_settings
```
Core ne reçoit aucune connaissance de Logging et aucune dépendance inverse n'est introduite.
## Tests ajoutés
### Unitaires
- distinction des niveaux ;
- construction/getters des target filters ;
- console stdout/stderr ;
- settings fichier/rotation ;
- conservation des settings explicites ;
- logging désactivé sans sink ;
- rejet des target prefixes vides ou externes ;
- rejet du préfixe fichier vide ;
- scope synchrone d'un span ;
- propagation du résultat d'une future instrumentée.
### Intégration
- surface publique des settings sans Config ;
- disponibilité des cinq macros événements ;
- disponibilité des cinq macros spans ;
- usage sync et async sans import consumer de `tracing::Span` ou `tracing::Instrument` ;
- préservation de `target`, `file`, `module_path` et `line` au callsite événement ;
- préservation de `target`, `file`, `module_path` et `line` au callsite span ;
- entrée puis sortie du span lors du poll d'une future instrumentée.
## Règles ajustées
`DEP-LOG-009` documente explicitement que le bridge `tracing` caché nécessaire aux macros est un détail d'implémentation de `ksp-logging-lib`, jamais une surface utilisable par une crate consommatrice.
Le plan `004-V0_1_2_LOGGING_FOUNDATION_PLAN.md` est synchronisé avec l'API effectivement retenue dans `pre.002` et avec la validité d'un logging entièrement désactivé.
## Fichiers ajoutés
- `crates/ksp-logging-lib/Cargo.toml`
- `crates/ksp-logging-lib/src/error.rs`
- `crates/ksp-logging-lib/src/lib.rs`
- `crates/ksp-logging-lib/src/macros.rs`
- `crates/ksp-logging-lib/src/settings.rs`
- `crates/ksp-logging-lib/src/span.rs`
- `crates/ksp-logging-lib/unit_tests/settings.rs`
- `crates/ksp-logging-lib/unit_tests/span.rs`
- `crates/ksp-logging-lib/tests/callsite.rs`
- `crates/ksp-logging-lib/tests/public_api.rs`
- `deltas/0.1.2/pre.002.md`
## Fichiers modifiés
- `Cargo.toml`
- `docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md`
- `docs/rules/RULES_DEPENDENCIES.md`
## Fichiers supprimés
Aucun.
## Validations exécutées
Validations statiques exécutées dans l'environnement de préparation :
- parsing TOML des manifests ;
- contrôle des headers `file:` / `version:` des fichiers livrés ;
- contrôle de l'absence de `Cargo.lock` dans le delta ;
- contrôle de l'absence de `tracing-subscriber` et `tracing-appender` dans les manifests ;
- contrôle de la centralisation de `tracing` sous `[workspace.dependencies]` ;
- contrôle que les usages directs de `tracing` restent bornés à `ksp-logging-lib` ;
- contrôle des patterns Rust interdits par les règles workspace dans le code production ajouté ;
- contrôle des liens Markdown locaux du plan modifié ;
- contrôle du contenu de l'archive selon `VER-ARCHIVE-004`.
## Validations non exécutées
L'environnement de préparation ne fournit pas `cargo`, `rustc` ou `rustfmt`. Les validations suivantes ne sont donc **pas** déclarées réussies :
```bash
cargo fmt --all
cargo check --workspace
cargo test --workspace
cargo clippy --workspace --all-targets
cargo tree -p ksp-logging-lib
cargo tree -p ksp-logging-lib -d
cargo tree -p ksp-logging-lib -e features
```
Elles doivent être exécutées sur le workspace de développement avant validation de la tranche. Toute erreur sera corrigée par le delta suivant conformément au workflow KSP.
## Décisions prises
- `tracing` est la seule dépendance de la stack ajoutée en `pre.002` ;
- les macros KSP exigent `target:` ;
- le callsite est préservé par expansion de macro et testé ;
- l'abstraction publique de span est `ksp_logging_lib::Span` ;
- le synchrone utilise `Span::in_scope(...)` ;
- l'async utilise `instrument(span, future)` ;
- une configuration sans sink est valide et représente Logging désactivé ;
- aucune initialisation/subscriber global n'est introduit prématurément dans cette tranche.
## Questions ouvertes
Aucune question bloquante pour `pre.002`.
Restent à choisir/tester dans les tranches runtime suivantes :
- la composition interne reloadable la moins coûteuse ;
- l'API exacte d'observation des lignes abandonnées ;
- les détails finaux du formatter console/fichier ;
- la stratégie de swap des sinks garantissant le maintien de l'ancienne configuration si une reconfiguration échoue.
Après validation de cette tranche, la prochaine étape est `0.1.2-pre.003` : subscriber, takeover, filtering, console initiale et fondation du hot reload.

View File

@@ -1,13 +1,13 @@
<!-- file: docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md -->
<!-- version: 2 -->
<!-- version: 3 -->
# Plan KSP 0.1.2 — Logging foundation
## Statut
Plan actif de `0.1.2`, établi par `0.1.2-pre.001` et corrigé par `0.1.2-pre.001-fix.001`.
Plan actif de `0.1.2`, établi par `0.1.2-pre.001`, corrigé par `0.1.2-pre.001-fix.001` puis concrétisé par la première surface fonctionnelle de `0.1.2-pre.002`.
`pre.001` et son fix restent une tranche de brainstorming, audit et planification. Aucun développement fonctionnel de `ksp-logging-lib` n'est commencé avant validation de ce plan corrigé.
`pre.002` crée `ksp-logging-lib`, ses settings runtime, sa façade d'événements/spans et son instrumentation async. Le subscriber runtime, le takeover effectif, les sorties et le hot reload restent réservés aux tranches suivantes.
## Base auditée
@@ -93,13 +93,13 @@ tracing-appender 0.2.5 MSRV annoncé : rustc 1.63+
Ces exigences restent inférieures au MSRV déjà imposé indirectement par la génération Solana retenue dans `0.1.1` (`solana-pubkey 4.3.0` / workspace Solana SDK auditée à Rust 1.89.0). Logging ne relève donc pas le plancher observé du workspace.
Les versions doivent néanmoins être revérifiées au moment exact de leur ajout effectif au manifest.
Les versions sont revérifiées au moment exact de leur ajout effectif au manifest. Pour `pre.002`, `tracing` reste à `0.1.44`, publié dans la génération `^0.1`; `tracing-subscriber` et `tracing-appender` ne sont pas encore ajoutés.
### `tracing`
`tracing 0.1.44` active par défaut `attributes` et `std` ; la feature `attributes` tire `tracing-attributes`.
La première surface KSP n'utilise ni `#[instrument]` ni autre macro attribut procédurale. La dépendance candidate est donc :
La première surface KSP n'utilise ni `#[instrument]` ni autre macro attribut procédurale. `pre.002` retient donc :
```toml
tracing = { version = "^0.1", default-features = false, features = ["std"] }
@@ -147,9 +147,9 @@ tracing-appender = { version = "^0.2", default-features = false }
### Politique d'ajout au manifest
Aucune de ces trois dépendances n'est ajoutée par `pre.001`, car aucun code de `ksp-logging-lib` ne les consomme encore.
`pre.002` ajoute uniquement `tracing` sous `[workspace.dependencies]`, car la façade événements/spans et l'instrumentation async l'utilisent réellement. `crates/ksp-logging-lib/Cargo.toml` l'hérite avec `tracing.workspace = true`.
Elles seront ajoutées sous `[workspace.dependencies]` uniquement dans la prerelease qui les utilise effectivement, puis héritées dans `crates/ksp-logging-lib/Cargo.toml` avec `.workspace = true`.
`tracing-subscriber` et `tracing-appender` restent absents jusqu'aux prereleases qui utilisent effectivement leurs APIs.
Après chaque ajout réel :
@@ -429,7 +429,7 @@ FileRotation
Aucun `Default` implicite de `LoggingSettings` n'est requis dans la première surface.
Le caller choisit explicitement son niveau KSP global et ses sorties. Le niveau global des targets externes reste `Off` par politique et n'est pas rendu configurable dans cette première surface.
Le caller choisit explicitement son niveau KSP global et ses sorties. Une configuration sans console ni fichier est valide et représente un logging KSP désactivé. Le niveau global des targets externes reste `Off` par politique et n'est pas rendu configurable dans cette première surface.
`ConsoleSettings` pourra offrir des constructeurs explicites `stdout()` / `stderr()` si cela simplifie l'API sans ambiguïté.
@@ -437,7 +437,6 @@ Le caller choisit explicitement son niveau KSP global et ses sorties. Le niveau
`initialize()` et `reinitialize()` doivent refuser au minimum :
- une configuration sans aucune sortie active ;
- un préfixe de target vide ;
- un override de target qui ne correspond pas à la convention KSP retenue ;
- un préfixe de fichier vide si le backend retenu ne peut pas le traiter sans ambiguïté.
@@ -708,7 +707,7 @@ La feature `tracing-log` de `tracing-subscriber` n'est pas activée.
Cette décision participe au takeover : KSP ne cherche pas à aspirer automatiquement le bruit de dépendances instrumentées avec la crate `log`. Une information utile est réémise explicitement par la crate KSP propriétaire sous son propre target.
## API publique candidate
## API publique
La façade crate-root visée à la fin de la release est :
@@ -728,7 +727,8 @@ ksp_logging_lib::trace_span!
ksp_logging_lib::LogFilterLevel
ksp_logging_lib::TargetFilter
ksp_logging_lib::SpanEvents
ksp_logging_lib::<KSP span abstraction/helpers>
ksp_logging_lib::Span
ksp_logging_lib::instrument
ksp_logging_lib::ConsoleOutput
ksp_logging_lib::ConsoleSettings
ksp_logging_lib::FileRotation
@@ -741,9 +741,9 @@ ksp_logging_lib::reinitialize
ksp_logging_lib::<ErrorCode constants owned by Logging>
```
Les noms exacts de l'abstraction de span et de l'API d'instrumentation async sont figés pendant l'implémentation après tests de callsite/type leakage.
`pre.002` fixe l'abstraction de span publique à `ksp_logging_lib::Span`. `Span::in_scope(...)` couvre les scopes synchrones et `ksp_logging_lib::instrument(span, future)` retourne une `Future` opaque instrumentée sans demander au consumer d'importer `tracing::Span` ou `tracing::Instrument`.
Les modules d'implémentation restent privés. Les types externes `tracing*` ne deviennent pas le contrat public KSP.
Les macros utilisent un bridge `tracing` caché de la documentation uniquement pour permettre leur expansion depuis une crate consommatrice tout en conservant le callsite réel. Ce bridge est un détail d'implémentation réservé aux macros et ne constitue pas une API KSP consommable. Les modules d'implémentation restent privés.
Arborescence candidate :
@@ -905,6 +905,8 @@ Objectifs :
### `0.1.2-pre.002` — crate + settings + façade événements/spans
Statut : implémenté dans la tranche `pre.002`, sous réserve des validations Cargo à exécuter dans l'environnement de développement.
Objectifs :
- créer `crates/ksp-logging-lib` et l'ajouter au workspace ;
@@ -1013,17 +1015,17 @@ La release peut être stabilisée lorsque :
- les validations workspace et audits présents sont propres ;
- la documentation finale et le prompt `0.1.3` sont prêts.
## Questions ouvertes après `pre.001-fix.001`
## Questions ouvertes après `pre.002`
Aucune question architecturale bloquante ne justifie du développement fonctionnel dans `pre.001`.
Les deux questions d'API propres à `pre.002` sont résolues :
Restent à confirmer par implémentation/tests sans remettre en cause le contrat :
1. les macros KSP délèguent aux macros `tracing` au point d'appel via un bridge interne caché et exigent un `target:` explicite ;
2. la surface span publique est `Span::in_scope(...)` pour le synchrone et `instrument(span, future)` pour l'async, avec type de future retourné opaque.
1. le mécanisme Rust exact des macros KSP événements/spans qui préserve le callsite et masque les types `tracing` ;
2. la forme exacte de l'abstraction KSP permettant d'instrumenter proprement les futures async ;
3. la composition interne la moins coûteuse pour le hot reload (reload de filters/layers ciblés ou routing dynamique KSP), tout en conservant un seul subscriber global ;
4. l'API publique exacte d'observation des dropped lines ;
5. le détail visuel exact du formatter humain, sans transformer sa ponctuation en contrat public.
Restent à confirmer par les prereleases runtime sans remettre en cause ce contrat :
1. la composition interne la moins coûteuse pour le hot reload (reload de filters/layers ciblés ou routing dynamique KSP), tout en conservant un seul subscriber global ;
2. l'API publique exacte d'observation des dropped lines ;
3. le détail visuel exact du formatter humain, sans transformer sa ponctuation en contrat public.
La prochaine action après validation de ce plan est `0.1.2-pre.002`, pas l'ouverture de Config.
La prochaine action après validation de `pre.002` est `0.1.2-pre.003` : subscriber, takeover, filtering, console initiale et fondation du hot reload.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/rules/RULES_DEPENDENCIES.md -->
<!-- version: 9 -->
<!-- version: 10 -->
# Règles des dépendances KSP
@@ -51,6 +51,7 @@ Elles complètent les règles Rust générales et le graphe de `docs/architectur
- **DEP-LOG-006** — Le subscriber KSP rend silencieux par défaut les targets externes ; une information tierce utile est réémise explicitement par la crate KSP propriétaire sous son propre target au lieu de renommer/réécrire l'événement tiers.
- **DEP-LOG-007** — Une application/framework peut exceptionnellement intégrer directement un plugin/dépendance tracing imposé par son framework, notamment Tauri, sans créer une seconde politique de logging parallèle à `ksp-logging-lib`; les événements KSP restent émis via la façade KSP.
- **DEP-LOG-008** — `ksp-logging-lib` possède ses settings runtime et son hot reload ; `ksp-config-lib` peut plus tard construire ces settings et demander une reconfiguration sans créer de dépendance inverse Logging -> Config.
- **DEP-LOG-009** — Tout bridge `tracing` public mais caché de la documentation rendu techniquement nécessaire par lexpansion des macros de `ksp-logging-lib` est un détail dimplémentation réservé à ces macros ; une crate consommatrice ne lutilise jamais directement et reste limitée à la façade KSP documentée.
## Program / Execution