Skip to main content

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