Skip to main content

provide_telemetry/logger/
emit.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//! Log emission helpers: JSON and console output + test capture buffers.
6//!
7//! Split out from `logger/mod.rs` to keep each file under the 500-LOC cap.
8
9use std::sync::{LazyLock, Mutex};
10
11use serde_json::{json, Value};
12
13use super::pretty::format_pretty_line;
14use super::{active_logging_config, LogEvent};
15
16// ---------------------------------------------------------------------------
17// Test capture buffers
18// ---------------------------------------------------------------------------
19
20static JSON_CAPTURE: LazyLock<Mutex<Option<Vec<u8>>>> = LazyLock::new(|| Mutex::new(None));
21
22pub fn enable_json_capture_for_tests() {
23    *crate::_lock::lock(&JSON_CAPTURE) = Some(Vec::new());
24}
25
26pub fn take_json_capture() -> Vec<u8> {
27    crate::_lock::lock(&JSON_CAPTURE).take().unwrap_or_default()
28}
29
30static CONSOLE_CAPTURE: LazyLock<Mutex<Option<Vec<u8>>>> = LazyLock::new(|| Mutex::new(None));
31
32pub fn enable_console_capture_for_tests() {
33    *crate::_lock::lock(&CONSOLE_CAPTURE) = Some(Vec::new());
34}
35
36pub fn take_console_capture() -> Vec<u8> {
37    crate::_lock::lock(&CONSOLE_CAPTURE)
38        .take()
39        .unwrap_or_default()
40}
41
42static PRETTY_CAPTURE: LazyLock<Mutex<Option<Vec<u8>>>> = LazyLock::new(|| Mutex::new(None));
43
44pub fn enable_pretty_capture_for_tests() {
45    *crate::_lock::lock(&PRETTY_CAPTURE) = Some(Vec::new());
46}
47
48pub fn take_pretty_capture() -> Vec<u8> {
49    crate::_lock::lock(&PRETTY_CAPTURE)
50        .take()
51        .unwrap_or_default()
52}
53
54// ---------------------------------------------------------------------------
55// Timestamp
56// ---------------------------------------------------------------------------
57
58pub(crate) fn now_iso8601() -> String {
59    use std::time::{SystemTime, UNIX_EPOCH};
60    let d = SystemTime::now()
61        .duration_since(UNIX_EPOCH)
62        .unwrap_or_default();
63    iso8601_from_unix_parts(d.as_secs(), d.subsec_millis())
64}
65
66fn iso8601_from_unix_parts(ts: u64, ms: u32) -> String {
67    let z: i64 = (ts / 86_400) as i64 + 719_468;
68    let era: i64 = z / 146_097;
69    let doe: i64 = z - era * 146_097;
70    let yoe: i64 = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
71    let y: i64 = yoe + era * 400;
72    let doy: i64 = doe - (365 * yoe + yoe / 4 - yoe / 100);
73    let mp: i64 = (5 * doy + 2) / 153;
74    let day: i64 = doy - (153 * mp + 2) / 5 + 1;
75    let month: i64 = if mp < 10 { mp + 3 } else { mp - 9 };
76    let year: i64 = if month <= 2 { y + 1 } else { y };
77    let sod = ts % 86_400;
78    let hour = sod / 3_600;
79    let min = (sod % 3_600) / 60;
80    let sec = sod % 60;
81    format!("{year:04}-{month:02}-{day:02}T{hour:02}:{min:02}:{sec:02}.{ms:03}Z")
82}
83
84// ---------------------------------------------------------------------------
85// JSON emit
86// ---------------------------------------------------------------------------
87
88fn emit_json_line(event: &LogEvent, include_timestamp: bool) {
89    let mut record = if include_timestamp {
90        json!({
91            "message": event.message,
92            "level": event.level,
93            "timestamp": now_iso8601(),
94        })
95    } else {
96        json!({
97            "message": event.message,
98            "level": event.level,
99        })
100    };
101    let obj = record.as_object_mut().expect("json object");
102    for (k, v) in &event.context {
103        obj.insert(k.clone(), v.clone());
104    }
105    if let Some(tid) = &event.trace_id {
106        obj.insert("trace_id".to_string(), Value::String(tid.clone()));
107    }
108    if let Some(sid) = &event.span_id {
109        obj.insert("span_id".to_string(), Value::String(sid.clone()));
110    }
111    obj.insert(
112        "logger_name".to_string(),
113        Value::String(event.target.clone()),
114    );
115    let line = serde_json::to_string(obj).unwrap_or_default();
116    let mut capture = crate::_lock::lock(&JSON_CAPTURE);
117    if let Some(buf) = capture.as_mut() {
118        buf.extend_from_slice(line.as_bytes());
119        buf.push(b'\n');
120    } else {
121        eprintln!("{line}");
122    }
123}
124
125pub(super) fn emit_if_json(event: &LogEvent) {
126    let logging = active_logging_config();
127    if logging.fmt.eq_ignore_ascii_case("json") {
128        emit_json_line(event, logging.include_timestamp);
129    }
130}
131
132// ---------------------------------------------------------------------------
133// Console emit
134// ---------------------------------------------------------------------------
135
136fn format_console_line(event: &LogEvent, include_timestamp: bool) -> String {
137    let mut s = String::new();
138    if include_timestamp {
139        s.push_str(&now_iso8601());
140        s.push_str("  ");
141    }
142    s.push_str(&format!("{:<5}", event.level));
143    s.push_str("  ");
144    s.push_str(&event.target);
145    s.push_str("  ");
146    s.push_str(&event.message);
147    for (k, v) in &event.context {
148        s.push_str(&format!("  {k}={v}"));
149    }
150    s
151}
152
153pub(super) fn emit_if_console(event: &LogEvent) {
154    let logging = active_logging_config();
155    if logging.fmt.eq_ignore_ascii_case("json") || logging.fmt.eq_ignore_ascii_case("pretty") {
156        return;
157    }
158    let line = format_console_line(event, logging.include_timestamp);
159    let mut capture = crate::_lock::lock(&CONSOLE_CAPTURE);
160    if let Some(buf) = capture.as_mut() {
161        buf.extend_from_slice(line.as_bytes());
162        buf.push(b'\n');
163    } else {
164        eprintln!("{line}");
165    }
166}
167
168pub(super) fn emit_if_pretty(event: &LogEvent) {
169    let logging = active_logging_config();
170    if !logging.fmt.eq_ignore_ascii_case("pretty") {
171        return;
172    }
173    let line = format_pretty_line(event, &logging);
174    let mut capture = crate::_lock::lock(&PRETTY_CAPTURE);
175    if let Some(buf) = capture.as_mut() {
176        buf.extend_from_slice(line.as_bytes());
177        buf.push(b'\n');
178    } else {
179        eprintln!("{line}");
180    }
181}
182
183#[cfg(feature = "otel")]
184pub(super) fn emit_if_otel(event: &LogEvent) {
185    crate::otel::logs::emit_log(event);
186}
187
188#[cfg(not(feature = "otel"))]
189pub(super) fn emit_if_otel(_event: &LogEvent) {}
190
191#[cfg(test)]
192#[path = "emit_tests.rs"]
193mod tests;