Skip to main content

appcore_log/
dispatcher.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: dispatcher.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: unknown by dnettoRaw
7//    ##   ## ##   ##    U: working-tree by dnettoRaw
8//      ###########      S: 1.0.2-rc
9// =============================================================================
10
11//! Dispatcher ownership and bounded failure accounting.
12
13use crate::{LogClock, LogEvent, LogPolicy, LogSink, Sensitivity, Severity};
14use std::borrow::Cow;
15use std::sync::atomic::{AtomicU64, Ordering};
16use std::sync::Arc;
17
18/// Observable dispatcher counters; sink failures never recursively log.
19#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
20pub struct LogStats {
21    /// Events filtered before formatting.
22    pub filtered: u64,
23    /// Sink deliveries that failed.
24    pub sink_failures: u64,
25    /// Events dropped by bounded sinks.
26    pub dropped: u64,
27    /// Events rejected because a public event limit was exceeded.
28    pub invalid: u64,
29}
30
31/// Failure counter for one configured sink label.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct SinkStats {
34    /// Stable sink implementation label.
35    pub name: &'static str,
36    /// Controlled delivery failures for that sink.
37    pub failures: u64,
38}
39
40struct SinkSlot {
41    sink: Arc<dyn LogSink>,
42    failures: AtomicU64,
43}
44
45/// Thread-safe fan-out dispatcher; ordinary sinks receive only sanitized events.
46pub struct LogDispatcher {
47    policy: LogPolicy,
48    sinks: Vec<SinkSlot>,
49    filtered: AtomicU64,
50    sink_failures: AtomicU64,
51    dropped: AtomicU64,
52    invalid: AtomicU64,
53}
54
55impl LogDispatcher {
56    /// Creates a dispatcher with explicit filtering and sinks.
57    pub fn new(policy: LogPolicy, sinks: Vec<Arc<dyn LogSink>>) -> Self {
58        Self {
59            policy,
60            sinks: sinks
61                .into_iter()
62                .map(|sink| SinkSlot {
63                    sink,
64                    failures: AtomicU64::new(0),
65                })
66                .collect(),
67            filtered: AtomicU64::new(0),
68            sink_failures: AtomicU64::new(0),
69            dropped: AtomicU64::new(0),
70            invalid: AtomicU64::new(0),
71        }
72    }
73    /// Emits once. Critical failure accounting is retained even when a sink fails.
74    pub fn emit(&self, event: LogEvent) {
75        if self.sinks.is_empty() {
76            return;
77        }
78        if event.validate().is_err() {
79            self.invalid.fetch_add(1, Ordering::Relaxed);
80            return;
81        }
82        if !self.policy.allows(&event) {
83            self.filtered.fetch_add(1, Ordering::Relaxed);
84            return;
85        }
86        let severity = event.severity;
87        let sanitized = self.policy.sanitize_owned(event);
88        for slot in &self.sinks {
89            if self.policy.sensitivity == Sensitivity::Sensitive && !slot.sink.accepts_sensitive() {
90                self.sink_failures.fetch_add(1, Ordering::Relaxed);
91                slot.failures.fetch_add(1, Ordering::Relaxed);
92                continue;
93            }
94            if slot.sink.emit(&sanitized).is_err() {
95                self.sink_failures.fetch_add(1, Ordering::Relaxed);
96                slot.failures.fetch_add(1, Ordering::Relaxed);
97                if severity < Severity::Error {
98                    self.dropped.fetch_add(1, Ordering::Relaxed);
99                }
100            }
101        }
102    }
103
104    /// Creates a fluent event builder for a stable component and clock value.
105    pub fn event<'a>(
106        &'a self,
107        timestamp_ms: u64,
108        component: impl Into<Cow<'a, str>>,
109    ) -> LogBuilder<'a> {
110        LogBuilder {
111            dispatcher: self,
112            timestamp_ms,
113            component: component.into(),
114            verbosity: crate::Verbosity::V4,
115        }
116    }
117
118    /// Creates a fluent event builder using an injected clock.
119    pub fn event_now<'a>(
120        &'a self,
121        clock: &dyn LogClock,
122        component: impl Into<Cow<'a, str>>,
123    ) -> LogBuilder<'a> {
124        self.event(clock.now_ms(), component)
125    }
126
127    /// Reports whether a component and verbosity would reach at least one sink.
128    pub fn enabled(&self, component: &str, verbosity: crate::Verbosity) -> bool {
129        !self.sinks.is_empty() && self.policy.allows_component(component, verbosity)
130    }
131    /// Returns counters without taking global locks.
132    pub fn stats(&self) -> LogStats {
133        LogStats {
134            filtered: self.filtered.load(Ordering::Relaxed),
135            sink_failures: self.sink_failures.load(Ordering::Relaxed),
136            dropped: self.dropped.load(Ordering::Relaxed),
137            invalid: self.invalid.load(Ordering::Relaxed),
138        }
139    }
140
141    /// Returns bounded failure counters for each configured sink.
142    pub fn sink_stats(&self) -> Vec<SinkStats> {
143        self.sinks
144            .iter()
145            .map(|slot| SinkStats {
146                name: slot.sink.name(),
147                failures: slot.failures.load(Ordering::Relaxed),
148            })
149            .collect()
150    }
151}
152
153/// Fluent, component-scoped log emitter with an immutable per-event override.
154#[derive(Clone)]
155pub struct LogBuilder<'a> {
156    dispatcher: &'a LogDispatcher,
157    timestamp_ms: u64,
158    component: Cow<'a, str>,
159    verbosity: crate::Verbosity,
160}
161
162impl<'a> LogBuilder<'a> {
163    /// Replaces the stable component used for filtering and rendering.
164    #[must_use]
165    pub fn component(&self, component: impl Into<Cow<'a, str>>) -> Self {
166        Self {
167            component: component.into(),
168            ..self.clone()
169        }
170    }
171
172    /// Sets a V1–V9 threshold for this event; invalid values retain V4.
173    ///
174    /// Use [`Self::try_verbosity`] when invalid configuration must be reported.
175    #[must_use]
176    pub fn verbosity(&self, verbosity: u8) -> Self {
177        Self {
178            verbosity: crate::Verbosity::new(verbosity).unwrap_or(crate::Verbosity::V4),
179            ..self.clone()
180        }
181    }
182
183    /// Sets a V1–V9 threshold and returns invalid configuration explicitly.
184    pub fn try_verbosity(&self, verbosity: u8) -> Result<Self, crate::VerbosityError> {
185        let verbosity = crate::Verbosity::new(verbosity).ok_or(crate::VerbosityError)?;
186        Ok(Self {
187            verbosity,
188            ..self.clone()
189        })
190    }
191
192    /// Emits a trace event.
193    pub fn trace(&self, message: impl Into<String>) {
194        self.emit(Severity::Trace, message);
195    }
196
197    /// Emits a debug event.
198    pub fn debug(&self, message: impl Into<String>) {
199        self.emit(Severity::Debug, message);
200    }
201
202    /// Emits an informational event.
203    pub fn info(&self, message: impl Into<String>) {
204        self.emit(Severity::Info, message);
205    }
206
207    /// Emits a warning event.
208    pub fn warn(&self, message: impl Into<String>) {
209        self.emit(Severity::Warn, message);
210    }
211
212    /// Emits an error event.
213    pub fn error(&self, message: impl Into<String>) {
214        self.emit(Severity::Error, message);
215    }
216
217    /// Emits a critical event.
218    pub fn critical(&self, message: impl Into<String>) {
219        self.emit(Severity::Critical, message);
220    }
221
222    fn emit(&self, severity: Severity, message: impl Into<String>) {
223        if self.dispatcher.enabled(&self.component, self.verbosity) {
224            self.dispatcher.emit(LogEvent::new(
225                self.timestamp_ms,
226                severity,
227                self.verbosity,
228                self.component.to_string(),
229                message.into(),
230            ));
231        }
232    }
233}