v0.1.2-pre.002
This commit is contained in:
15
crates/ksp-logging-lib/Cargo.toml
Normal file
15
crates/ksp-logging-lib/Cargo.toml
Normal 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
|
||||
5
crates/ksp-logging-lib/src/error.rs
Normal file
5
crates/ksp-logging-lib/src/error.rs
Normal 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");
|
||||
42
crates/ksp-logging-lib/src/lib.rs
Normal file
42
crates/ksp-logging-lib/src/lib.rs
Normal 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;
|
||||
97
crates/ksp-logging-lib/src/macros.rs
Normal file
97
crates/ksp-logging-lib/src/macros.rs
Normal 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)+))
|
||||
}};
|
||||
}
|
||||
233
crates/ksp-logging-lib/src/settings.rs
Normal file
233
crates/ksp-logging-lib/src/settings.rs
Normal 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;
|
||||
42
crates/ksp-logging-lib/src/span.rs
Normal file
42
crates/ksp-logging-lib/src/span.rs
Normal 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;
|
||||
157
crates/ksp-logging-lib/tests/callsite.rs
Normal file
157
crates/ksp-logging-lib/tests/callsite.rs
Normal 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);
|
||||
}
|
||||
52
crates/ksp-logging-lib/tests/public_api.rs
Normal file
52
crates/ksp-logging-lib/tests/public_api.rs
Normal 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");
|
||||
}
|
||||
102
crates/ksp-logging-lib/unit_tests/settings.rs
Normal file
102
crates/ksp-logging-lib/unit_tests/settings.rs
Normal 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());
|
||||
}
|
||||
22
crates/ksp-logging-lib/unit_tests/span.rs
Normal file
22
crates/ksp-logging-lib/unit_tests/span.rs
Normal 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));
|
||||
}
|
||||
Reference in New Issue
Block a user