1#![feature(ptr_metadata, panic_payload_as_str)]
6use std::io::{self, Write};
26use std::mem::replace;
27use std::num::NonZeroU32;
28use std::panic::Location;
29use std::path::Path;
30use std::sync::Mutex;
31use std::thread::{self, Thread, ThreadId};
32use std::{fmt, panic, ptr};
33
34pub use log;
36use log::{Level, LevelFilter, Log, set_logger, set_max_level};
37use sys_abstract::SystemImpl;
38use time::{Date, OffsetDateTime};
39
40mod sys_abstract;
41
42#[cfg(target_arch = "wasm32")]
43mod sys_web;
44#[cfg(target_arch = "wasm32")]
45use sys_web::System;
46
47#[cfg(not(target_arch = "wasm32"))]
48mod sys_native;
49#[cfg(not(target_arch = "wasm32"))]
50use sys_native::System;
51
52pub struct Settings {
54 pub title: &'static str,
56 pub filters: &'static [(&'static str, LevelFilter)],
59 pub file_out: Option<&'static Path>,
61 pub console_out: bool,
63 pub panic_hook: Option<fn(Panic)>,
66}
67
68impl Settings {
69 pub fn init(self) {
74 let Self {
75 title,
76 filters,
77 file_out,
78 console_out,
79 panic_hook,
80 } = self;
81 let max_level = filters
82 .iter()
83 .map(|&(_, level)| level)
84 .max()
85 .unwrap_or(LevelFilter::Off);
86 if let Some(handler) = panic_hook {
87 panic::set_hook(Box::new(panic_handler(handler)));
90 }
91 let date = now().date();
92 let logger = Logger {
93 title,
94 filters,
95 file_out: file_out.map(System::file_new),
96 console_out: console_out.then(System::console_new),
97 prev_day: Mutex::new(date.to_julian_day()),
98 };
99 let message = Header { title, date };
100 if let Some(out) = &logger.file_out {
101 System::file_p_header(out, &message);
102 }
103 if let Some(out) = &logger.console_out {
104 System::console_p_header(out, &message);
105 }
106 set_logger(upcast_log(Box::leak(Box::new(logger)))).expect("Failed to apply logger");
107 set_max_level(max_level);
108 }
109}
110
111fn as_dyn_ref(logger: *const Logger) -> *const dyn Log {
113 logger as *const dyn Log
115}
116fn upcast_log(logger: &'static Logger) -> &'static dyn Log {
117 unsafe { &*as_dyn_ref(logger) }
119}
120fn downcast_log(log: &'static dyn Log) -> Option<&'static Logger> {
121 let (logger_ptr, logger_meta) = (&raw const *log).to_raw_parts();
123 let (_, fake_logger_meta) = as_dyn_ref(ptr::null::<Logger>()).to_raw_parts();
124 (logger_meta == fake_logger_meta).then(|| {
125 unsafe { &*logger_ptr.cast::<Logger>() }
127 })
128}
129
130struct Logger {
132 title: &'static str,
133 filters: &'static [(&'static str, LevelFilter)],
134 file_out: Option<<System as SystemImpl>::File>,
135 console_out: Option<<System as SystemImpl>::Console>,
136 prev_day: Mutex<i32>,
137}
138
139impl Log for Logger {
140 fn enabled(&self, meta: &log::Metadata) -> bool {
141 for (name, level) in self.filters {
142 if meta.target().starts_with(name) {
143 return *level >= meta.level();
144 }
145 }
146 false
147 }
148 fn log(&self, record: &log::Record) {
149 if self.enabled(record.metadata()) {
150 let now = now();
151 let date = now.date();
152 let day = date.to_julian_day();
153 let date = match self.prev_day.lock() {
154 Ok(mut lock) => (replace(&mut *lock, day) != day).then_some(date),
155 Err(_) => None,
156 };
157 let thread = thread::current();
158 let message = Record {
159 date,
160 module: record.module_path().unwrap_or("?"),
161 line: NonZeroU32::new(record.line().unwrap_or(0)),
162 thread: ThreadName::new(&thread),
163 args: *record.args(),
164 hmsms: now.time().as_hms_milli(),
165 level: record.level(),
166 };
167 if let Some(out) = &self.file_out {
168 System::file_p_record(out, &message);
169 }
170 if let Some(out) = &self.console_out {
171 System::console_p_record(out, &message);
172 }
173 }
174 }
175 fn flush(&self) {
176 self.file_out.as_ref().map(System::file_flush);
177 self.console_out.as_ref().map(System::console_flush);
178 }
179}
180
181fn now() -> OffsetDateTime {
182 OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc())
183}
184
185#[derive(Debug)]
187pub enum ThreadName<'data> {
188 Name(&'data str),
190 Id(ThreadId),
192}
193impl<'data> ThreadName<'data> {
194 fn new(thread: &'data Thread) -> Self {
195 if let Some(name) = thread.name() {
196 Self::Name(name)
197 } else {
198 Self::Id(thread.id())
199 }
200 }
201}
202impl fmt::Display for ThreadName<'_> {
203 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
204 match self {
205 ThreadName::Name(name) => write!(f, "Thread {name:?}"),
206 ThreadName::Id(id) => write!(f, "{id:?}"),
207 }
208 }
209}
210
211struct Header {
215 title: &'static str,
216 date: Date,
217}
218
219struct Record<'data> {
226 date: Option<Date>,
227 module: &'data str,
228 line: Option<NonZeroU32>,
229 thread: ThreadName<'data>,
230 args: fmt::Arguments<'data>,
231 hmsms: (u8, u8, u8, u16),
232 level: Level,
233}
234
235#[derive(Debug)]
237pub struct Backtrace {
238 #[cfg(feature = "backtrace")]
239 data: <System as SystemImpl>::Backtrace,
240 #[cfg(not(feature = "backtrace"))]
241 data: (),
242}
243
244impl Backtrace {
245 fn capture() -> Self {
246 Self {
247 data: System::backtrace_new(),
248 }
249 }
250 pub fn write<W: Write>(&self, writer: W) -> io::Result<()> {
258 System::backtrace_write(&self.data, writer)
259 }
260 pub fn as_string(&self) -> String { System::backtrace_string(&self.data) }
262}
263
264pub struct Panic<'data> {
271 pub thread: ThreadName<'data>,
273 pub message: Option<&'data str>,
275 pub location: Option<Location<'data>>,
277 pub title: &'data str,
279 pub path: Option<&'data Path>,
281 pub trace: Backtrace,
283}
284
285impl fmt::Debug for Panic<'_> {
286 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287 f.debug_struct("Panic")
288 .field("thread", &self.thread)
289 .field("message", &self.message)
290 .field("location", &self.location)
291 .field("title", &self.title)
292 .field("path", &self.path)
293 .finish_non_exhaustive()
294 }
295}
296
297impl Panic<'_> {
298 fn message_str(&self) -> &str { self.message.unwrap_or("[non-string message]") }
299 fn location_display(&self) -> &dyn fmt::Display {
300 self.location.as_ref().map_or(&"[citation needed]", |v| v)
301 }
302}
303
304fn panic_handler(handler: fn(Panic)) -> impl Fn(&panic::PanicHookInfo) {
305 move |info: &panic::PanicHookInfo| {
306 let logger = downcast_log(log::logger());
307 let thread = thread::current();
308 let mut message = Panic {
309 thread: ThreadName::new(&thread),
310 message: info.payload_as_str(),
311 location: info.location().copied(),
312 title: "[unknown?]",
313 path: None,
314 trace: Backtrace::capture(),
315 };
316 if let Some(logger) = logger {
317 message.title = logger.title;
318 message.path = System::file_path(logger.file_out.as_ref());
319 if let Some(out) = &logger.file_out {
320 System::file_p_panic(out, &message);
321 }
322 if let Some(out) = &logger.console_out {
323 System::console_p_panic(out, &message);
324 }
325 handler(message);
326 } else {
327 System::fallback_p_panic(&message);
328 }
329 }
330}