From 7d480080526e67dc40a1cf4fafdd343c517f41ad Mon Sep 17 00:00:00 2001 From: KodrAus Date: Tue, 21 May 2024 07:45:11 +1000 Subject: [PATCH] document Runtime --- core/src/runtime.rs | 347 +++++++++++++++++++++++++++++++++-------- core/src/timestamp.rs | 4 +- core/src/well_known.rs | 4 +- 3 files changed, 283 insertions(+), 72 deletions(-) diff --git a/core/src/runtime.rs b/core/src/runtime.rs index 1cc2b6e..e10eb19 100644 --- a/core/src/runtime.rs +++ b/core/src/runtime.rs @@ -9,9 +9,11 @@ Runtimes combine components into a fully encapsulated diagnostic pipeline. Each - A [`Clock`] to timestamp events. - A [`Rng`] to generate correlation ids for events. -Runtimes are fully isolated and may be short-lived. +Runtimes are fully isolated and may be short-lived. A [`Runtime`] can be treated generically, or erased behind an [`AmbientSlot`] for global sharing. This module defines two global runtimes; the [`shared()`] runtime, and the [`internal()`] runtime. Applications should emit their events through the [`shared()`] runtime. Code running within a runtime itself, such as an implementation of [`Emitter`] should emit their events through the [`internal()`] runtime. -Applications should emit their events through the [`shared()`] runtime. Code running within a runtime itself, such as an implementation of [`Emitter`] should emit their events through the [`internal()`] runtime. +The [`internal()`] runtime can only be initialized with components that also satisfy internal versions of their regular traits. These marker traits require a component not produce any diagnostics of its own, and so are safe to use by another runtime. If components in the [`internal()`] runtime could produce their own diagnostics then it could cause loops and stack overflows. + +If an application is initializing both the [`shared()`] and [`internal()`] runtimes, then it should initialize the [`internal()`] runtime _first_. */ use crate::{ @@ -24,26 +26,61 @@ static SHARED: AmbientSlot = AmbientSlot::new(); #[cfg(feature = "implicit_rt")] static INTERNAL: AmbientInternalSlot = AmbientInternalSlot::new(); +/** +The global shared runtime for applications to use. + +This runtime needs to be initialized through its [`shared_slot()`], otherwise it will use [`Empty`] implementations of its components. +*/ #[cfg(feature = "implicit_rt")] pub fn shared() -> &'static AmbientRuntime<'static> { SHARED.get() } +/** +The initialization slot for the [`shared()`] runtime. +*/ #[cfg(feature = "implicit_rt")] pub fn shared_slot() -> &'static AmbientSlot { &SHARED } +/** +The internal runtime for other runtime components to use. + +Applications should use the [`shared()`] runtime instead of this one. + +This runtime can be initialized through its [`internal_slot()`] to enable diagnostics on the regular diagnostics runtime itself. +*/ #[cfg(feature = "implicit_rt")] pub fn internal() -> &'static AmbientRuntime<'static> { INTERNAL.get() } +/** +The initialization slot for the [`internal()`] runtime. + +This slot should be initialized _before_ the [`shared_slot()`] if it's in use. +*/ #[cfg(feature = "implicit_rt")] pub fn internal_slot() -> &'static AmbientInternalSlot { &INTERNAL } +/** +A diagnostic pipeline. + +Each runtime includes the following components: + +- An [`Emitter`] to receive diagnostic events. +- A [`Filter`] to limit the volume of diagnostic events. +- A [`Ctxt`] to capture and attach ambient state to events. +- A [`Clock`] to timestamp events. +- A [`Rng`] to generate correlation ids for events. + +The components of a runtime can be accessed directly through methods. A runtime can be treated like a builder to set its components, or initialized with them all directly. + +In statics, you can also use the [`AmbientSlot`] type to hold a type-erased runtime. It's also reasonable to store a fully generic runtime in a static too. +*/ #[derive(Debug, Clone, Copy)] pub struct Runtime { pub(crate) emitter: TEmitter, @@ -60,6 +97,9 @@ impl Default for Runtime { } impl Runtime { + /** + Create a new, empty runtime. + */ pub const fn new() -> Runtime { Runtime { emitter: Empty, @@ -72,6 +112,9 @@ impl Runtime { } impl Runtime { + /** + Create a new runtime with the given components. + */ pub const fn build( emitter: TEmitter, filter: TFilter, @@ -88,14 +131,23 @@ impl Runtime &TEmitter { &self.emitter } + /** + Set the [`Emitter`]. + */ pub fn with_emitter(self, emitter: U) -> Runtime { self.map_emitter(|_| emitter) } + /** + Map the current [`Emitter`] to a new value. + */ pub fn map_emitter( self, emitter: impl FnOnce(TEmitter) -> U, @@ -109,14 +161,23 @@ impl Runtime &TFilter { &self.filter } + /** + Set the [`Filter`]. + */ pub fn with_filter(self, filter: U) -> Runtime { self.map_filter(|_| filter) } + /** + Map the current [`Filter`] to a new value. + */ pub fn map_filter( self, filter: impl FnOnce(TFilter) -> U, @@ -130,14 +191,23 @@ impl Runtime &TCtxt { &self.ctxt } + /** + Set the [`Ctxt`]. + */ pub fn with_ctxt(self, ctxt: U) -> Runtime { self.map_ctxt(|_| ctxt) } + /** + Map the current [`Ctxt`] to a new value. + */ pub fn map_ctxt( self, ctxt: impl FnOnce(TCtxt) -> U, @@ -151,14 +221,23 @@ impl Runtime &TClock { &self.clock } + /** + Set the [`Clock`]. + */ pub fn with_clock(self, clock: U) -> Runtime { self.map_clock(|_| clock) } + /** + Map the current [`Clock`] to a new value. + */ pub fn map_clock( self, clock: impl FnOnce(TClock) -> U, @@ -172,14 +251,23 @@ impl Runtime &TRng { &self.rng } + /** + Set the [`Rng`]. + */ pub fn with_rng(self, id_gen: U) -> Runtime { self.map_rng(|_| id_gen) } + /** + Map the current [`Rng`] to a new value. + */ pub fn map_rng( self, id_gen: impl FnOnce(TRng) -> U, @@ -197,6 +285,18 @@ impl Runtime Runtime { + /** + Emit a diagnostic event through the runtime. + + This method uses the components of the runtime to process the event. It will: + + 1. Attempt to assign an extent to the event using [`Clock::now`] if the event doesn't already have one. + 2. Add [`Ctxt::Current`] to the event properties. + 3. Ensure the event passes [`Filter::matches`]. + 4. Emit the event through [`Emitter::emit`]. + + You can bypass any of these steps by emitting the event directly through the runtime's [`Emitter`]. + */ pub fn emit(&self, evt: E) { self.ctxt.with_current(|ctxt| { let evt = evt.to_event(); @@ -217,73 +317,9 @@ impl } } -pub struct AssertInternal(pub T); - -impl Emitter for AssertInternal { - fn emit(&self, evt: E) { - self.0.emit(evt) - } - - fn blocking_flush(&self, timeout: core::time::Duration) -> bool { - self.0.blocking_flush(timeout) - } -} - -impl Filter for AssertInternal { - fn matches(&self, evt: E) -> bool { - self.0.matches(evt) - } -} - -impl Ctxt for AssertInternal { - type Current = T::Current; - type Frame = T::Frame; - - fn open_root(&self, props: P) -> Self::Frame { - self.0.open_root(props) - } - - fn open_push(&self, props: P) -> Self::Frame { - self.0.open_push(props) - } - - fn enter(&self, local: &mut Self::Frame) { - self.0.enter(local) - } - - fn with_current R>(&self, with: F) -> R { - self.0.with_current(with) - } - - fn exit(&self, local: &mut Self::Frame) { - self.0.exit(local) - } - - fn close(&self, frame: Self::Frame) { - self.0.close(frame) - } -} - -impl Clock for AssertInternal { - fn now(&self) -> Option { - self.0.now() - } -} - -impl Rng for AssertInternal { - fn fill>(&self, arr: A) -> Option { - self.0.fill(arr) - } - - fn gen_u64(&self) -> Option { - self.0.gen_u64() - } - - fn gen_u128(&self) -> Option { - self.0.gen_u128() - } -} - +/** +A marker trait for an [`Emitter`] that does not emit any diagnostics of its own. +*/ pub trait InternalEmitter: Emitter {} impl InternalEmitter for AssertInternal {} @@ -298,6 +334,9 @@ impl<'a, T: ?Sized + InternalEmitter> InternalEmitter for alloc::boxed::Box { #[cfg(feature = "alloc")] impl<'a, T: ?Sized + InternalEmitter> InternalEmitter for alloc::sync::Arc {} +/** +A marker trait for a [`Filter`] that does not emit any diagnostics of its own. +*/ pub trait InternalFilter: Filter {} impl InternalFilter for AssertInternal {} @@ -319,6 +358,9 @@ impl<'a, T: ?Sized + InternalFilter> InternalFilter for alloc::boxed::Box {} #[cfg(feature = "alloc")] impl<'a, T: ?Sized + InternalFilter> InternalFilter for alloc::sync::Arc {} +/** +A marker trait for a [`Ctxt`] that does not emit any diagnostics of its own. +*/ pub trait InternalCtxt: Ctxt {} impl InternalCtxt for AssertInternal {} @@ -331,6 +373,9 @@ impl<'a, T: ?Sized + InternalCtxt> InternalCtxt for alloc::boxed::Box {} #[cfg(feature = "alloc")] impl<'a, T: ?Sized + InternalCtxt> InternalCtxt for alloc::sync::Arc {} +/** +A marker trait for a [`Clock`] that does not emit any diagnostics of its own. +*/ pub trait InternalClock: Clock {} impl InternalClock for AssertInternal {} @@ -343,6 +388,9 @@ impl<'a, T: ?Sized + InternalClock> InternalClock for alloc::boxed::Box {} #[cfg(feature = "alloc")] impl<'a, T: ?Sized + InternalClock> InternalClock for alloc::sync::Arc {} +/** +A marker trait for an [`Rng`] that does not emit any diagnostics of its own. +*/ pub trait InternalRng: Rng {} impl InternalRng for AssertInternal {} @@ -355,6 +403,76 @@ impl<'a, T: ?Sized + InternalRng> InternalRng for alloc::boxed::Box {} #[cfg(feature = "alloc")] impl<'a, T: ?Sized + InternalRng> InternalRng for alloc::sync::Arc {} +/** +Assert that a given component does not emit any diagnostics of its own. +*/ +pub struct AssertInternal(pub T); + +impl Emitter for AssertInternal { + fn emit(&self, evt: E) { + self.0.emit(evt) + } + + fn blocking_flush(&self, timeout: core::time::Duration) -> bool { + self.0.blocking_flush(timeout) + } +} + +impl Filter for AssertInternal { + fn matches(&self, evt: E) -> bool { + self.0.matches(evt) + } +} + +impl Ctxt for AssertInternal { + type Current = T::Current; + type Frame = T::Frame; + + fn open_root(&self, props: P) -> Self::Frame { + self.0.open_root(props) + } + + fn open_push(&self, props: P) -> Self::Frame { + self.0.open_push(props) + } + + fn enter(&self, local: &mut Self::Frame) { + self.0.enter(local) + } + + fn with_current R>(&self, with: F) -> R { + self.0.with_current(with) + } + + fn exit(&self, local: &mut Self::Frame) { + self.0.exit(local) + } + + fn close(&self, frame: Self::Frame) { + self.0.close(frame) + } +} + +impl Clock for AssertInternal { + fn now(&self) -> Option { + self.0.now() + } +} + +impl Rng for AssertInternal { + fn fill>(&self, arr: A) -> Option { + self.0.fill(arr) + } + + fn gen_u64(&self) -> Option { + self.0.gen_u64() + } + + fn gen_u128(&self) -> Option { + self.0.gen_u128() + } +} + #[cfg(feature = "std")] mod std_support { use core::any::Any; @@ -367,6 +485,9 @@ mod std_support { use super::*; + /** + A type-erased [`Emitter`] for an [`AmbientSlot`]. + */ pub type AmbientEmitter<'a> = &'a (dyn ErasedEmitter + Send + Sync + 'static); trait AnyEmitter: Any + ErasedEmitter + Send + Sync + 'static { @@ -384,6 +505,9 @@ mod std_support { } } + /** + A type-erased [`Filter`] for an [`AmbientSlot`]. + */ pub type AmbientFilter<'a> = &'a (dyn ErasedFilter + Send + Sync + 'static); trait AnyFilter: Any + ErasedFilter + Send + Sync + 'static { @@ -401,6 +525,9 @@ mod std_support { } } + /** + A type-erased [`Ctxt`] for an [`AmbientSlot`]. + */ pub type AmbientCtxt<'a> = &'a (dyn ErasedCtxt + Send + Sync + 'static); trait AnyCtxt: Any + ErasedCtxt + Send + Sync + 'static { @@ -418,6 +545,9 @@ mod std_support { } } + /** + A type-erased [`Clock`] for an [`AmbientSlot`]. + */ pub type AmbientClock<'a> = &'a (dyn ErasedClock + Send + Sync + 'static); trait AnyClock: Any + ErasedClock + Send + Sync + 'static { @@ -435,6 +565,9 @@ mod std_support { } } + /** + A type-erased [`Rng`] for an [`AmbientSlot`]. + */ pub type AmbientRng<'a> = &'a (dyn ErasedRng + Send + Sync + 'static); trait AnyRng: Any + ErasedRng + Send + Sync + 'static { @@ -452,8 +585,16 @@ mod std_support { } } + /** + A type-erased slot for a globally shared [`Runtime`]. + + The slot is suitable to store directly in a static; it coordinates its own initialization using a [`OnceLock`]. + */ pub struct AmbientSlot(OnceLock); + /** + A type-erased slot for the [`internal()`] runtime. + */ #[cfg(feature = "implicit_rt")] pub struct AmbientInternalSlot(AmbientSlot); @@ -478,6 +619,9 @@ mod std_support { *const (dyn ErasedRng + Send + Sync), >; + /** + A type-erased [`Runtime`]. + */ pub type AmbientRuntime<'a> = Runtime< AmbientEmitter<'a>, AmbientFilter<'a>, @@ -490,14 +634,25 @@ mod std_support { unsafe impl Sync for AmbientSync where AmbientSyncValue: Sync {} impl AmbientSlot { + /** + Create a new, empty slot. + */ pub const fn new() -> Self { AmbientSlot(OnceLock::new()) } + /** + Whether the slot has been initialized with a runtime. + */ pub fn is_enabled(&self) -> bool { self.0.get().is_some() } + /** + Try initialize the slot with the given components. + + If the slot has not already been initialized then the components will be installed and a reference to the resulting [`Runtime`] will be returned. If the slot has already been initialized by another caller then this method will discard the components and return `None`. + */ pub fn init( &self, pipeline: Runtime, @@ -544,6 +699,9 @@ mod std_support { )) } + /** + Get the underlying [`Runtime`], or a [`Runtime::default`] if it hasn't been initialized yet. + */ pub fn get(&self) -> &AmbientRuntime { const EMPTY_AMBIENT_RUNTIME: AmbientRuntime = Runtime::build( &Empty as &(dyn ErasedEmitter + Send + Sync + 'static), @@ -568,10 +726,20 @@ mod std_support { AmbientInternalSlot(AmbientSlot(OnceLock::new())) } + /** + Whether the [`internal()`] runtime has been initialized. + + Components can use this method to decide whether to do work related to diagnostic capturing. + */ pub fn is_enabled(&self) -> bool { self.0.is_enabled() } + /** + Initialize the [`internal()`] runtime with the given components. + + The components must satisfy additional trait bounds compared to a regular [`AmbientSlot`]. Each component must also implement a marker trait that promises they don't produce any diagnostics of their own. + */ pub fn init( &self, pipeline: Runtime, @@ -587,6 +755,9 @@ mod std_support { self.0.init(pipeline) } + /** + Get the underlying [`Runtime`], or a [`Runtime::default`] if it hasn't been initialized yet. + */ pub fn get(&self) -> &AmbientRuntime { self.0.get() } @@ -600,20 +771,34 @@ pub use self::std_support::*; mod no_std_support { use super::*; + /** + A slot for a shared runtime. + + Without the `std` feature enabled, this slot cannot be initialized. + */ pub struct AmbientSlot {} #[cfg(feature = "implicit_rt")] pub struct AmbientInternalSlot(AmbientSlot); impl AmbientSlot { + /** + Create a new, empty slot. + */ pub const fn new() -> Self { AmbientSlot {} } + /** + When the `std` feature is not enabled this method always returns `false`. + */ pub fn is_enabled(&self) -> bool { false } + /** + When the `std` feature is not enabled this method always returns an empty runtime. + */ pub fn get(&self) -> &AmbientRuntime { const EMPTY_AMBIENT_RUNTIME: AmbientRuntime = Runtime::build(&Empty, &Empty, &Empty, &Empty, &Empty); @@ -628,21 +813,45 @@ mod no_std_support { AmbientInternalSlot(AmbientSlot::new()) } + /** + When the `std` feature is not enabled this method always returns `false`. + */ pub fn is_enabled(&self) -> bool { false } + /** + When the `std` feature is not enabled this method always returns an empty runtime. + */ pub fn get(&self) -> &AmbientRuntime { self.0.get() } } + /** + When the `std` feature is not enabled this is always [`Empty`]. + */ pub type AmbientEmitter<'a> = &'a Empty; + /** + When the `std` feature is not enabled this is always [`Empty`]. + */ pub type AmbientFilter<'a> = &'a Empty; + /** + When the `std` feature is not enabled this is always [`Empty`]. + */ pub type AmbientCtxt<'a> = &'a Empty; + /** + When the `std` feature is not enabled this is always [`Empty`]. + */ pub type AmbientClock<'a> = &'a Empty; + /** + When the `std` feature is not enabled this is always [`Empty`]. + */ pub type AmbientRng<'a> = &'a Empty; + /** + When the `std` feature is not enabled this is always [`Runtime::default`]. + */ pub type AmbientRuntime<'a> = Runtime< AmbientEmitter<'a>, AmbientFilter<'a>, diff --git a/core/src/timestamp.rs b/core/src/timestamp.rs index ea7c077..ebe3b97 100644 --- a/core/src/timestamp.rs +++ b/core/src/timestamp.rs @@ -99,7 +99,7 @@ impl Timestamp { /** Try create a timestamp from time since the Unix epoch. - If the `unix_time` is within [`MIN`]..=[`MAX`] then this method will return `Some`. Otherwise it will return `None`. + If the `unix_time` is within [`Timestamp::MIN`]..=[`Timestamp::MAX`] then this method will return `Some`. Otherwise it will return `None`. */ pub fn from_unix(unix_time: Duration) -> Option { if unix_time >= MIN && unix_time <= MAX { @@ -138,7 +138,7 @@ impl Timestamp { /** Try get a timestamp from its individual date and time parts. - If the resulting timestamp is within [`MIN`]..=[`MAX`] then this method will return `Some`. Otherwise it will return `None`. + If the resulting timestamp is within [`Timestamp::MIN`]..=[`Timestamp::MAX`] then this method will return `Some`. Otherwise it will return `None`. If any field of `parts` would overflow its maximum value, such as `days: 32`, then it will wrap into the next unit. */ diff --git a/core/src/well_known.rs b/core/src/well_known.rs index 3eb9f36..ddf22d1 100644 --- a/core/src/well_known.rs +++ b/core/src/well_known.rs @@ -1,4 +1,6 @@ -// TODO: Treat `_` props as "silent" or reserved? +/*! +Extensions to the diagnostic module using well-known properties. +*/ // Event pub const KEY_MODULE: &'static str = "module";