multiline_logger/
lib.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// TODO: it's very possible to make this stable-able:
3// - ptr_metadata: remove if log#666 gets resolved
4// - panic_payload_as_str: just inline the implementation
5#![feature(ptr_metadata, panic_payload_as_str)]
6//! [![Repository](https://img.shields.io/badge/repository-GitHub-brightgreen.svg)](https://github.com/1e1001/rsutil/tree/main/multiline-logger)
7//! [![Crates.io](https://img.shields.io/crates/v/multiline-logger)](https://crates.io/crates/multiline-logger)
8//! [![docs.rs](https://img.shields.io/docsrs/multiline-logger)](https://docs.rs/multiline-logger)
9//! [![MIT OR Apache-2.0](https://img.shields.io/crates/l/multiline-logger)](https://github.com/1e1001/rsutil/blob/main/multiline-logger/README.md#License)
10//!
11//! Fancy lightweight debug output:
12//! - Not excessively dynamic but still configurable
13//! - Logs messages and crashes
14//! - Looks very nice (in my opinion)
15//!
16//! | Platform | Console output | File output | Backtraces |
17//! |-:|-|:-:|-|
18//! | Native | `stderr` (colored!) | ✓ | `backtrace` feature |
19//! | Web | web `console` (colored!) | ✗ | `backtrace` feature |
20//!
21//! Get started by creating a [`Settings`] and calling [`init`].
22//!
23//! [`init`]: Settings::init
24
25use 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
34/// For convenience :)
35pub 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
52/// Settings for the logger
53pub struct Settings {
54	/// A human-readable name for the application
55	pub title: &'static str,
56	/// List of module-prefix filters to match against,
57	/// earlier filters get priority
58	pub filters: &'static [(&'static str, LevelFilter)],
59	/// Optional file path to output to (desktop only)
60	pub file_out: Option<&'static Path>,
61	/// Set to `true` to output to an appropriate console
62	pub console_out: bool,
63	/// Enables the formatted panic hook, and calls the supplied function.
64	/// Use `|_| ()` if you don't have anything to run
65	pub panic_hook: Option<fn(Panic)>,
66}
67
68impl Settings {
69	/// Initializes the logger
70	///
71	/// # Panics
72	/// will panic if initialization fails in any way
73	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			// set the hook before installing the logger,
88			// to show panic messages if logger initialization breaks
89			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
111// TODO: remove this once log#666 gets resolved
112fn as_dyn_ref(logger: *const Logger) -> *const dyn Log {
113	// split into one function to always attach the same metadata
114	logger as *const dyn Log
115}
116fn upcast_log(logger: &'static Logger) -> &'static dyn Log {
117	// SAFETY: as_dyn_ref returns a reference to the same object as passed in
118	unsafe { &*as_dyn_ref(logger) }
119}
120fn downcast_log(log: &'static dyn Log) -> Option<&'static Logger> {
121	// horribly cursed implementation to fetch a reference to the installed logger
122	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		// SAFETY: v-tables match so it's probably ours!
126		unsafe { &*logger_ptr.cast::<Logger>() }
127	})
128}
129
130// logger context
131struct 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/// Name / Id of a thread
186#[derive(Debug)]
187pub enum ThreadName<'data> {
188	/// Thread has a name
189	Name(&'data str),
190	/// Thread is ID only
191	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
211/// ```text
212/// {BY}== title - date ==
213/// ```
214struct Header {
215	title: &'static str,
216	date: Date,
217}
218
219/// ```text
220/// {BY}= date =
221/// {BB}h:m:s.ms {BG}module:line{BM} thread
222/// {L}level {0}message
223/// {L}    | {0}message
224/// ```
225struct 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/// System-agnostic backtrace type
236#[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	// TODO: platform-specific converters
251	// TODO: some way to get color printing
252	// TODO: impl Display via some fucked up io::Write → fmt::Write adapter
253	// for now I just implemented all I need
254	/// Print backtrace to a writer
255	/// # Errors
256	/// if the writer errors, or backtrace error
257	pub fn write<W: Write>(&self, writer: W) -> io::Result<()> {
258		System::backtrace_write(&self.data, writer)
259	}
260	/// Get the backtrace as a string
261	pub fn as_string(&self) -> String { System::backtrace_string(&self.data) }
262}
263
264/// Panic handler information. This structure will change with updates!
265/// ```text
266/// {BR}== title - {BM}thread{BR} Panic ==
267/// {0}message
268/// {BG}→ location
269/// {0}backtrace```
270pub struct Panic<'data> {
271	/// Panicking thread
272	pub thread: ThreadName<'data>,
273	/// Panic text
274	pub message: Option<&'data str>,
275	/// Panic location
276	pub location: Option<Location<'data>>,
277	/// Application title
278	pub title: &'data str,
279	/// Log file path, if you want to show it to the user
280	pub path: Option<&'data Path>,
281	/// Backtrace (or not)
282	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}