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