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