Skip to main content

provide_telemetry/logger/
mod.rs

1// SPDX-FileCopyrightText: Copyright (C) 2026 provide.io llc
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-Comment: Part of provide-telemetry.
4//
5
6use std::collections::BTreeMap;
7use std::sync::{Arc, LazyLock, Mutex, OnceLock};
8
9use serde_json::Value;
10
11use crate::backpressure::{release, try_acquire, QueueTicket};
12use crate::config::TelemetryConfig;
13#[cfg(feature = "governance")]
14use crate::consent::should_allow;
15use crate::context::get_context;
16use crate::health::increment_emitted;
17use crate::runtime::get_runtime_config;
18use crate::sampling::{set_sampling_policy, should_sample, SamplingPolicy, Signal};
19use crate::tracer::get_trace_context;
20
21mod emit;
22mod levels;
23mod pretty;
24mod processors;
25
26use emit::{emit_if_console, emit_if_json, emit_if_otel, emit_if_pretty};
27pub use emit::{
28    enable_console_capture_for_tests, enable_json_capture_for_tests,
29    enable_pretty_capture_for_tests, take_console_capture, take_json_capture, take_pretty_capture,
30};
31use levels::{effective_level_threshold, level_order};
32use processors::process_event;
33
34#[cfg(feature = "governance")]
35#[inline(always)]
36fn consent_allows_logs(level: &str) -> bool {
37    should_allow("logs", Some(level))
38}
39
40#[cfg(not(feature = "governance"))]
41#[inline(always)]
42#[cfg_attr(test, mutants::skip)] // Dead under the default governance feature set, so false-return mutants are not meaningfully testable here.
43fn consent_allows_logs(_level: &str) -> bool {
44    true
45}
46
47const MAX_FALLBACK_EVENTS: usize = 1000;
48
49static LOGGING_CONFIG_OVERRIDE: LazyLock<Mutex<Option<crate::config::LoggingConfig>>> =
50    LazyLock::new(|| Mutex::new(None));
51
52#[cfg_attr(test, mutants::skip)] // Equivalent mutants only rewrite Vec::new() syntax.
53fn empty_events_mutex() -> Mutex<Vec<LogEvent>> {
54    Mutex::new(Vec::new())
55}
56
57/// Override the active logging configuration programmatically.
58///
59/// Takes precedence over both `setup_telemetry()` config and env vars.
60/// Useful when the caller wants to set level/format at startup in code
61/// rather than relying solely on environment variables.
62pub fn configure_logging(config: crate::config::LoggingConfig) {
63    *crate::_lock::lock(&LOGGING_CONFIG_OVERRIDE) = Some(config);
64}
65
66/// Clear the programmatic logging override (test helper).
67pub fn reset_logging_config_for_tests() {
68    *crate::_lock::lock(&LOGGING_CONFIG_OVERRIDE) = None;
69}
70
71/// Read the active logging config.
72/// Priority order: programmatic override, runtime config, then env/defaults.
73fn active_logging_config() -> crate::config::LoggingConfig {
74    let override_cfg = crate::_lock::lock(&LOGGING_CONFIG_OVERRIDE).clone();
75    if let Some(cfg) = override_cfg {
76        return cfg;
77    }
78    if let Some(cfg) = get_runtime_config() {
79        return cfg.logging.clone();
80    }
81    match TelemetryConfig::from_env() {
82        Ok(cfg) => cfg.logging,
83        Err(err) => {
84            eprintln!("provide_telemetry: logging config parse failed, using defaults: {err}");
85            crate::config::LoggingConfig::default()
86        }
87    }
88}
89
90fn runtime_identity_config() -> Option<TelemetryConfig> {
91    match get_runtime_config() {
92        Some(cfg) => Some(cfg),
93        None => TelemetryConfig::from_env().ok(),
94    }
95}
96
97fn apply_lazy_logger_policies_from_env() {
98    if get_runtime_config().is_some() {
99        return;
100    }
101    if std::env::var_os("PROVIDE_SAMPLING_LOGS_RATE").is_none() {
102        return;
103    }
104    let Ok(cfg) = TelemetryConfig::from_env() else {
105        return;
106    };
107    let _ = set_sampling_policy(
108        Signal::Logs,
109        SamplingPolicy {
110            default_rate: cfg.sampling.logs_rate,
111            overrides: BTreeMap::new(),
112        },
113    );
114}
115
116fn inject_identity_fields(context: &mut BTreeMap<String, Value>, cfg: TelemetryConfig) {
117    context
118        .entry("service".to_string())
119        .or_insert(Value::String(cfg.service_name));
120    context
121        .entry("env".to_string())
122        .or_insert(Value::String(cfg.environment));
123    context
124        .entry("version".to_string())
125        .or_insert(Value::String(cfg.version));
126}
127
128fn inject_runtime_identity_fields(context: &mut BTreeMap<String, Value>) {
129    let cfg = match runtime_identity_config() {
130        Some(cfg) => cfg,
131        None => return,
132    };
133    inject_identity_fields(context, cfg);
134}
135
136/// Shared emit path: run processors, emit, buffer.
137fn emit_event(mut event: LogEvent) {
138    process_event(&mut event);
139    emit_if_json(&event);
140    emit_if_pretty(&event);
141    emit_if_console(&event);
142    emit_if_otel(&event);
143    let mut buf = crate::_lock::lock(events());
144    if buf.len() >= MAX_FALLBACK_EVENTS {
145        return;
146    }
147    buf.push(event);
148}
149
150fn acquire_log_ticket(level: &str, target: &str, sample_key: Option<&str>) -> Option<QueueTicket> {
151    let config = active_logging_config();
152    if level_order(level) < effective_level_threshold(target, &config) {
153        return None;
154    }
155    if !consent_allows_logs(level) {
156        return None;
157    }
158    if !should_sample(Signal::Logs, sample_key).unwrap_or(true) {
159        return None;
160    }
161    try_acquire(Signal::Logs)
162}
163
164/// Like `log_event` but merges extra caller-supplied fields into the event context.
165fn log_event_with_fields(
166    level: &str,
167    target: &str,
168    message: &str,
169    extra: &BTreeMap<String, Value>,
170) {
171    let Some(ticket) = acquire_log_ticket(level, target, Some(message)) else {
172        return;
173    };
174    let mut event = new_event(target, level, message);
175    event.context.extend(extra.clone());
176    emit_event(event);
177    increment_emitted(Signal::Logs, 1);
178    release(ticket);
179}
180
181/// Shared core: gate, build, process, emit, count.
182fn log_event(level: &str, target: &str, message: &str) {
183    let Some(ticket) = acquire_log_ticket(level, target, Some(message)) else {
184        return;
185    };
186    emit_event(new_event(target, level, message));
187    increment_emitted(Signal::Logs, 1);
188    release(ticket);
189}
190
191/// Like `log_event` but attaches DARS metadata from an `Event`.
192fn log_event_with_event(level: &str, target: &str, ev: &crate::schema::Event) {
193    let Some(ticket) = acquire_log_ticket(level, target, Some(&ev.event)) else {
194        return;
195    };
196    let mut event = new_event(target, level, &ev.event);
197    event.event_metadata = Some(EventMetadata {
198        domain: ev.domain.clone(),
199        action: ev.action.clone(),
200        resource: ev.resource.clone(),
201        status: ev.status.clone(),
202    });
203    emit_event(event);
204    increment_emitted(Signal::Logs, 1);
205    release(ticket);
206}
207
208/// DARS metadata extracted from an `Event` when the caller uses the
209/// `_event` logger methods. `None` for plain string messages.
210#[derive(Clone, Debug, PartialEq, Eq)]
211pub struct EventMetadata {
212    pub domain: String,
213    pub action: String,
214    pub resource: Option<String>,
215    pub status: String,
216}
217
218#[derive(Clone, Debug, Default, PartialEq)]
219pub struct LogEvent {
220    pub level: String,
221    pub target: String,
222    pub message: String,
223    pub context: BTreeMap<String, Value>,
224    pub trace_id: Option<String>,
225    pub span_id: Option<String>,
226    pub event_metadata: Option<EventMetadata>,
227}
228
229#[derive(Clone, Debug, Default, PartialEq, Eq)]
230pub struct Logger {
231    target: String,
232}
233
234#[derive(Clone, Debug, Default, PartialEq, Eq)]
235pub struct NullLogger {
236    target: String,
237}
238
239#[derive(Clone, Debug, Default)]
240pub struct BufferLogger {
241    target: String,
242    events: Arc<Mutex<Vec<LogEvent>>>,
243}
244
245static EVENTS: OnceLock<Mutex<Vec<LogEvent>>> = OnceLock::new();
246
247fn events() -> &'static Mutex<Vec<LogEvent>> {
248    EVENTS.get_or_init(empty_events_mutex)
249}
250
251fn default_logger() -> Logger {
252    Logger::new(None)
253}
254
255pub static logger: LazyLock<Logger> = LazyLock::new(default_logger);
256
257fn logger_target(target: Option<&str>) -> String {
258    match target {
259        Some(target) => target.to_string(),
260        None => "provide.telemetry".to_string(),
261    }
262}
263
264fn new_event(target: &str, level: &str, message: &str) -> LogEvent {
265    let trace = get_trace_context();
266    let mut context = get_context();
267    inject_runtime_identity_fields(&mut context);
268    LogEvent {
269        level: level.to_string(),
270        target: target.to_string(),
271        message: message.to_string(),
272        context,
273        trace_id: trace.get("trace_id").and_then(Clone::clone),
274        span_id: trace.get("span_id").and_then(Clone::clone),
275        event_metadata: None,
276    }
277}
278
279impl Logger {
280    pub fn new(target: Option<&str>) -> Self {
281        Self {
282            target: logger_target(target),
283        }
284    }
285
286    pub fn target(&self) -> &str {
287        &self.target
288    }
289
290    pub fn debug(&self, message: &str) {
291        self.log("DEBUG", message);
292    }
293
294    pub fn info(&self, message: &str) {
295        self.log("INFO", message);
296    }
297
298    pub fn warn(&self, message: &str) {
299        self.log("WARN", message);
300    }
301
302    pub fn error(&self, message: &str) {
303        self.log("ERROR", message);
304    }
305
306    pub fn log(&self, level: &str, message: &str) {
307        log_event(level, &self.target, message);
308    }
309
310    /// Emit with extra step-local structured fields merged into the event context.
311    pub fn log_fields(&self, level: &str, message: &str, fields: &BTreeMap<String, Value>) {
312        log_event_with_fields(level, &self.target, message, fields);
313    }
314
315    pub fn debug_fields(&self, message: &str, fields: &BTreeMap<String, Value>) {
316        self.log_fields("DEBUG", message, fields);
317    }
318
319    pub fn info_fields(&self, message: &str, fields: &BTreeMap<String, Value>) {
320        self.log_fields("INFO", message, fields);
321    }
322
323    pub fn warn_fields(&self, message: &str, fields: &BTreeMap<String, Value>) {
324        self.log_fields("WARN", message, fields);
325    }
326
327    pub fn error_fields(&self, message: &str, fields: &BTreeMap<String, Value>) {
328        self.log_fields("ERROR", message, fields);
329    }
330
331    pub fn debug_event(&self, event: &crate::schema::Event) {
332        self.log_event("DEBUG", event);
333    }
334
335    pub fn info_event(&self, event: &crate::schema::Event) {
336        self.log_event("INFO", event);
337    }
338
339    pub fn warn_event(&self, event: &crate::schema::Event) {
340        self.log_event("WARN", event);
341    }
342
343    pub fn error_event(&self, event: &crate::schema::Event) {
344        self.log_event("ERROR", event);
345    }
346
347    pub fn log_event(&self, level: &str, event: &crate::schema::Event) {
348        log_event_with_event(level, &self.target, event);
349    }
350
351    pub fn drain_events_for_tests() -> Vec<LogEvent> {
352        std::mem::take(&mut *crate::_lock::lock(events()))
353    }
354}
355
356impl NullLogger {
357    pub fn new(target: Option<&str>) -> Self {
358        Self {
359            target: logger_target(target),
360        }
361    }
362
363    pub fn target(&self) -> &str {
364        &self.target
365    }
366
367    pub fn debug(&self, _message: &str) {}
368
369    pub fn info(&self, _message: &str) {}
370
371    pub fn warn(&self, _message: &str) {}
372
373    pub fn error(&self, _message: &str) {}
374}
375
376impl BufferLogger {
377    pub fn new(target: Option<&str>) -> Self {
378        Self {
379            target: logger_target(target),
380            events: Arc::new(Mutex::new(Vec::new())),
381        }
382    }
383
384    pub fn target(&self) -> &str {
385        &self.target
386    }
387
388    pub fn debug(&self, message: &str) {
389        self.log("DEBUG", message);
390    }
391
392    pub fn info(&self, message: &str) {
393        self.log("INFO", message);
394    }
395
396    pub fn warn(&self, message: &str) {
397        self.log("WARN", message);
398    }
399
400    pub fn error(&self, message: &str) {
401        self.log("ERROR", message);
402    }
403
404    pub fn log(&self, level: &str, message: &str) {
405        let config = active_logging_config();
406        if level_order(level) < effective_level_threshold(&self.target, &config) {
407            return;
408        }
409        let mut event = new_event(&self.target, level, message);
410        process_event(&mut event);
411        crate::_lock::lock(&self.events).push(event);
412    }
413
414    pub fn drain(&self) -> Vec<LogEvent> {
415        std::mem::take(&mut *crate::_lock::lock(&self.events))
416    }
417}
418
419pub fn get_logger(name: Option<&str>) -> Logger {
420    apply_lazy_logger_policies_from_env();
421    Logger::new(name)
422}
423
424pub fn null_logger(name: Option<&str>) -> NullLogger {
425    NullLogger::new(name)
426}
427
428pub fn buffer_logger(name: Option<&str>) -> BufferLogger {
429    BufferLogger::new(name)
430}
431
432impl log::Log for Logger {
433    fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
434        let config = active_logging_config();
435        let record_order: u8 = match metadata.level() {
436            log::Level::Error => 4,
437            log::Level::Warn => 3,
438            log::Level::Info => 2,
439            log::Level::Debug => 1,
440            log::Level::Trace => 0,
441        };
442        record_order >= effective_level_threshold(metadata.target(), &config)
443    }
444
445    fn log(&self, record: &log::Record<'_>) {
446        if !self.enabled(record.metadata()) {
447            return;
448        }
449        let level = match record.level() {
450            log::Level::Error => "ERROR",
451            log::Level::Warn => "WARN",
452            log::Level::Info => "INFO",
453            log::Level::Debug => "DEBUG",
454            log::Level::Trace => "TRACE",
455        };
456        log_event(level, record.target(), &record.args().to_string());
457    }
458
459    fn flush(&self) {}
460}
461
462/// Register the package-level logger as the global `log` crate backend.
463pub fn set_as_global_logger() -> Result<(), log::SetLoggerError> {
464    match log::set_logger(&*logger) {
465        Ok(()) => {
466            log::set_max_level(log::LevelFilter::Trace);
467            Ok(())
468        }
469        Err(err) => Err(err),
470    }
471}
472
473#[cfg(test)]
474mod tests;
475
476#[cfg(test)]
477#[path = "log_trait_tests.rs"]
478mod log_trait_tests;