Skip to main content

pharos_server/
logging.rs

1//! Process-wide stdout logging, configured by the binary at startup.
2
3use std::{
4    fmt::{self, Write as _},
5    io::{self, Write as _},
6    sync::atomic::{AtomicBool, Ordering},
7    time::{SystemTime, UNIX_EPOCH},
8};
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11pub enum LogFormat {
12    Text,
13    Json,
14}
15
16static JSON: AtomicBool = AtomicBool::new(false);
17static DEBUG: AtomicBool = AtomicBool::new(false);
18pub const PLUGIN_METADATA: &str = "logging/plugin";
19/// Marks an Error already logged by the errors Plugin.
20pub const ERROR_LOGGED_METADATA: &str = "logging/errors-logged";
21
22/// Select the format used by subsequent log entries.
23pub fn init(format: LogFormat) {
24    JSON.store(format == LogFormat::Json, Ordering::Relaxed);
25}
26
27/// Enable or disable process-wide debug logging.
28pub fn set_debug_enabled(enabled: bool) {
29    DEBUG.store(enabled, Ordering::Relaxed);
30}
31
32/// Return whether debug entries should be emitted.
33#[must_use]
34pub fn debug_enabled() -> bool {
35    DEBUG.load(Ordering::Relaxed)
36}
37
38/// Write one entry to stdout.
39pub fn log(level: &str, plugin: &str, message: &str) {
40    let format = if JSON.load(Ordering::Relaxed) {
41        LogFormat::Json
42    } else {
43        LogFormat::Text
44    };
45    let line = format_line(format, SystemTime::now(), level, plugin, message);
46    let _ = io::stdout().lock().write_all(line.as_bytes());
47}
48
49/// Write a bounded debug entry without allocating on the Query path.
50pub fn log_debug(plugin: &str, message: fmt::Arguments<'_>) {
51    let mut text = StackText::<2048>::new();
52    if text.write_fmt(message).is_err() {
53        return;
54    }
55    let mut line = StackText::<16384>::new();
56    let format = if JSON.load(Ordering::Relaxed) {
57        LogFormat::Json
58    } else {
59        LogFormat::Text
60    };
61    write_line(
62        &mut line,
63        format,
64        SystemTime::now(),
65        "DEBUG",
66        plugin,
67        text.as_str(),
68    );
69    let _ = io::stdout().lock().write_all(line.as_str().as_bytes());
70}
71
72struct StackText<const N: usize> {
73    bytes: [u8; N],
74    len: usize,
75}
76
77impl<const N: usize> StackText<N> {
78    fn new() -> Self {
79        Self {
80            bytes: [0; N],
81            len: 0,
82        }
83    }
84    fn as_str(&self) -> &str {
85        std::str::from_utf8(&self.bytes[..self.len]).expect("formatted UTF-8")
86    }
87}
88
89impl<const N: usize> fmt::Write for StackText<N> {
90    fn write_str(&mut self, value: &str) -> fmt::Result {
91        let end = self
92            .len
93            .checked_add(value.len())
94            .filter(|end| *end <= N)
95            .ok_or(fmt::Error)?;
96        self.bytes[self.len..end].copy_from_slice(value.as_bytes());
97        self.len = end;
98        Ok(())
99    }
100}
101
102/// Format one entry with a supplied timestamp.
103pub fn format_line(
104    format: LogFormat,
105    time: SystemTime,
106    level: &str,
107    plugin: &str,
108    message: &str,
109) -> String {
110    let mut line = String::with_capacity(128 + 6 * (level.len() + plugin.len() + message.len()));
111    write_line(&mut line, format, time, level, plugin, message);
112    line
113}
114
115fn write_line(
116    line: &mut impl fmt::Write,
117    format: LogFormat,
118    time: SystemTime,
119    level: &str,
120    plugin: &str,
121    message: &str,
122) {
123    match format {
124        LogFormat::Text => {
125            write_timestamp(line, time);
126            let _ = write!(line, " {level} ");
127            if !plugin.is_empty() {
128                let _ = write!(line, "{plugin}: ");
129            }
130            let _ = writeln!(line, "{message}");
131        }
132        LogFormat::Json => {
133            let _ = line.write_str("{\"timestamp\":\"");
134            write_timestamp(line, time);
135            let _ = line.write_str("\",\"level\":");
136            json_string(line, level);
137            let _ = line.write_str(",\"plugin\":");
138            json_string(line, plugin);
139            let _ = line.write_str(",\"message\":");
140            json_string(line, message);
141            let _ = line.write_str("}\n");
142        }
143    }
144}
145
146fn json_string(out: &mut impl fmt::Write, value: &str) {
147    let _ = out.write_char('"');
148    for c in value.chars() {
149        match c {
150            '"' => {
151                let _ = out.write_str("\\\"");
152            }
153            '\\' => {
154                let _ = out.write_str("\\\\");
155            }
156            '\n' => {
157                let _ = out.write_str("\\n");
158            }
159            '\r' => {
160                let _ = out.write_str("\\r");
161            }
162            '\t' => {
163                let _ = out.write_str("\\t");
164            }
165            c if c <= '\u{1f}' => {
166                let _ = write!(out, "\\u{:04x}", c as u32);
167            }
168            c => {
169                let _ = out.write_char(c);
170            }
171        }
172    }
173    let _ = out.write_char('"');
174}
175
176fn write_timestamp(out: &mut impl fmt::Write, time: SystemTime) {
177    let seconds = match time.duration_since(UNIX_EPOCH) {
178        Ok(duration) => duration.as_secs() as i64,
179        Err(error) => {
180            -(error.duration().as_secs() as i64) - i64::from(error.duration().subsec_nanos() != 0)
181        }
182    };
183    let days = seconds.div_euclid(86_400);
184    let day_seconds = seconds.rem_euclid(86_400);
185    // Gregorian civil date from days since 1970-01-01.
186    let z = days + 719_468;
187    let era = z.div_euclid(146_097);
188    let doe = z - era * 146_097;
189    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
190    let mut year = yoe + era * 400;
191    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
192    let mp = (5 * doy + 2) / 153;
193    let day = doy - (153 * mp + 2) / 5 + 1;
194    let month = mp + if mp < 10 { 3 } else { -9 };
195    year += i64::from(month <= 2);
196    let _ = write!(
197        out,
198        "{year:04}-{month:02}-{day:02}T{:02}:{:02}:{:02}Z",
199        day_seconds / 3600,
200        day_seconds % 3600 / 60,
201        day_seconds % 60
202    );
203}