1use 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";
19pub const ERROR_LOGGED_METADATA: &str = "logging/errors-logged";
21
22pub fn init(format: LogFormat) {
24 JSON.store(format == LogFormat::Json, Ordering::Relaxed);
25}
26
27pub fn set_debug_enabled(enabled: bool) {
29 DEBUG.store(enabled, Ordering::Relaxed);
30}
31
32#[must_use]
34pub fn debug_enabled() -> bool {
35 DEBUG.load(Ordering::Relaxed)
36}
37
38pub 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
49pub 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
102pub 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 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}