rm_lisa/
lib.rs

1//! The logging library for Rem-Verse.
2//!
3//! Some Key Features:
4//!
5//! - Multiple Logging Formats, that are built for many individuals.
6//!   - Colored for pretty ascii color & erasing interfaces.
7//!   - Plaintext for a simple line based format ideal for braille displays,
8//!     and log files.
9//!   - JSON for structured logging, with quick parsing/slicing/dicing support.
10//! - Task Status, and Logging.
11//!   - Queue up many background tasks, and have log lines prefixed with task
12//!     names, and status updates.
13//! - Shell Interface Building with tab-complete support.
14//! - Full customization at runtime of data to include by users.
15//!
16//! # Getting started
17//!
18//! To get started, you should simply initialize the super console at the start
19//! of the program, and keep a guard dropping it when you want to ensure all
20//! logs are flushed (e.g. at program exit.). This does assume your program is
21//! running in a tokio runtime.
22//!
23//! ```rust,no_run
24//! use rm_lisa::initialize_logging;
25//!
26//! async fn my_main() {
27//!   let console = initialize_logging("my-application-name").expect("Failed to initialize logging!");
28//!   // ... do my stuff ...
29//!   console.flush().await;
30//! }
31//! ```
32//!
33//! Once you've initialized the super console, all you need to do is use
34//! tracing like normal:
35//!
36//! ```rust,no_run
37//! use tracing::info;
38//!
39//! info!("my cool log line!");
40//! ```
41//!
42//! You can also specify a series of fields to customize your log message. Like:
43//!
44//! - `lisa.force_combine_fields`: force combine the metadata, and message to render
45//!   (e.g. render without the gutter).
46//! - `lisa.hide_fields_for_humans`: hide any fields that probably aren't useful for
47//!   humans. (e.g. `id`).
48//! - `lisa.subsystem`: the 'name' of the the system that logged this line.
49//!   Renders in the first part of the line instead of application name.
50//! - `lisa.stdout`: for this log line going to STDOUT instead of STDERR.
51//! - `lisa.decorate`: render the application name/log level on text rendering.
52//!
53//! These messages can be set on individual log messages themselves or set for
54//! everything in a scope:
55//!
56//! ```rust,no_run
57//! use tracing::{Instrument, error_span, info};
58//!
59//! async fn my_cool_function() {
60//!   async {
61//!     info!("Hello with details from span!");
62//!   }.instrument(
63//!     // we want our span attached to every message, making it error ensures that happens.
64//!     error_span!("my_span", lisa.subsystem="my_cool_task", lisa.stdout=true)
65//!   );
66//! }
67//! ```
68//!
69//! It is also recommended to include an `id` set to a unique string per log
70//! line. As when a user requests JSON output, without an ID it can be hard to
71//! know what schema the log will be following (What fields will be presenet,
72//! etc.). Having an `id` field can be used for that, and will be hidden on
73//! color/text renderers automatically as they are not useful there.
74
75pub mod display;
76pub mod errors;
77pub mod input;
78pub mod tasks;
79#[cfg(any(
80	target_os = "linux",
81	target_os = "android",
82	target_os = "macos",
83	target_os = "ios",
84	target_os = "freebsd",
85	target_os = "openbsd",
86	target_os = "netbsd",
87	target_os = "dragonfly",
88	target_os = "solaris",
89	target_os = "illumos",
90	target_os = "aix",
91	target_os = "haiku",
92))]
93pub mod termios;
94
95use crate::{
96	display::{SuperConsole, renderers::ConsoleOutputFeatures, tracing::TracableSuperConsole},
97	errors::LisaError,
98};
99use std::{
100	io::{Stderr as StdStderr, Stdout as StdStdout, Write as IoWrite},
101	sync::{
102		Arc,
103		atomic::{AtomicBool, Ordering as AtomicOrdering},
104	},
105};
106use tracing_subscriber::{EnvFilter, prelude::*, registry as subscriber_registry};
107
108/// If we have already registered a logger.
109static HAS_REGISTERED_LOGGER: AtomicBool = AtomicBool::new(false);
110
111/// Initialize the Lisa logger, this will error if we run into any errors
112/// actually setting up everything that needs to be set-up.
113///
114/// ## Errors
115///
116/// If we cannot determine which [`crate::display::renderers::ConsoleRenderer`]
117/// to utilize.
118pub fn initialize_logging(
119	app_name: &'static str,
120) -> Result<Arc<SuperConsole<StdStdout, StdStderr>>, LisaError> {
121	if HAS_REGISTERED_LOGGER.swap(true, AtomicOrdering::SeqCst) {
122		return Err(LisaError::AlreadyRegistered);
123	}
124
125	let console = Arc::new(SuperConsole::new(app_name)?);
126	register_panic_hook(&console);
127
128	let filter_layer = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
129	let registry = subscriber_registry().with(filter_layer);
130	registry
131		.with(TracableSuperConsole::new(console.clone()))
132		.init();
133
134	Ok(console)
135}
136
137/// Initialize the Lisa logger with a pre-configured super console.
138///
139/// ## Errors
140///
141/// If the logger has already been registered.
142pub fn initialize_with_console<
143	Stdout: IoWrite + ConsoleOutputFeatures + Send + 'static,
144	Stderr: IoWrite + ConsoleOutputFeatures + Send + 'static,
145>(
146	console: SuperConsole<Stdout, Stderr>,
147) -> Result<Arc<SuperConsole<Stdout, Stderr>>, LisaError> {
148	if HAS_REGISTERED_LOGGER.swap(true, AtomicOrdering::SeqCst) {
149		return Err(LisaError::AlreadyRegistered);
150	}
151	let arc_console = Arc::new(console);
152	register_panic_hook(&arc_console);
153
154	let filter_layer = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
155	let registry = subscriber_registry().with(filter_layer);
156	registry
157		.with(TracableSuperConsole::new(arc_console.clone()))
158		.init();
159
160	Ok(arc_console)
161}
162
163/// Create an environment variable prefix given an application name.
164///
165/// This will turn an app name into a prefix based off the following:
166///
167/// - Iterate through each character:
168///   - If the character is ascii alphanumeric: uppercase it and append
169///     it to the string.
170///   - If the character is anything else append '_'.
171/// - Append one final '_' if the string doesn't edit with it already.
172///
173/// Some examples:
174///
175/// ```
176/// use rm_lisa::app_name_to_prefix;
177///
178/// assert_eq!(app_name_to_prefix("sprig"), "SPRIG");
179/// assert_eq!(app_name_to_prefix("cafe-sdk"), "CAFE_SDK");
180/// assert_eq!(app_name_to_prefix("Café"), "CAF_");
181/// ```
182#[must_use]
183pub fn app_name_to_prefix(app_name: &'static str) -> String {
184	let mut buffer = String::with_capacity(app_name.len() + 1);
185
186	for character in app_name.chars() {
187		if character.is_ascii_alphanumeric() {
188			buffer.push(character.to_ascii_uppercase());
189		} else {
190			buffer.push('_');
191		}
192	}
193
194	buffer
195}
196
197fn register_panic_hook<
198	Stdout: IoWrite + ConsoleOutputFeatures + Send + 'static,
199	Stderr: IoWrite + ConsoleOutputFeatures + Send + 'static,
200>(
201	console: &Arc<SuperConsole<Stdout, Stderr>>,
202) {
203	let weak = Arc::downgrade(console);
204
205	let previously_registered_hook = std::panic::take_hook();
206	std::panic::set_hook(Box::new(move |arg| {
207		if let Some(c) = weak.upgrade() {
208			_ = c.flush();
209		}
210		previously_registered_hook(arg);
211	}));
212}
213
214#[cfg(test)]
215mod unit_tests {
216	use super::*;
217	use crate::display::renderers::JSONConsoleRenderer;
218	use escargot::CargoBuild;
219	use std::process::Stdio;
220
221	#[tokio::test]
222	pub async fn initializing_only_works_once() {
223		initialize_logging("librs_unit_tests").expect("First initialization should always work!");
224
225		assert!(
226			initialize_logging("any").is_err(),
227			"Initializing logging multiple times doesn't work!",
228		);
229		assert!(
230			initialize_with_console(
231				SuperConsole::new_preselected_renderers(
232					"librs_unit_tests",
233					Box::new(JSONConsoleRenderer::new()),
234					Box::new(JSONConsoleRenderer::new()),
235				)
236				.expect("Failed to create new_preselected_renderers")
237			)
238			.is_err(),
239			"Initializing logging multiple times doesn't work!",
240		);
241		assert!(
242			initialize_logging("test").is_err(),
243			"Initializing logging multiple times doesn't work!",
244		);
245	}
246
247	#[test]
248	pub fn test_simple_golden() {
249		let runner = CargoBuild::new()
250			.example("simple")
251			.run()
252			.expect("Failed to get runner for simple example!");
253
254		{
255			let output = runner
256				.command()
257				.env("SIMPLE_LOG_FORMAT", "color")
258				.env("SIMPLE_FORCE_TERM_WIDTH", "100")
259				.stdout(Stdio::piped())
260				.stderr(Stdio::piped())
261				.spawn()
262				.expect("Failed to spawn and run simple example in color mode!")
263				.wait_with_output()
264				.expect("Failed to get output from simple example!");
265			assert!(
266				output.status.success(),
267				"Simple output example did not complete successfully!\n\nStdout: {}\n\nStderr: {}",
268				String::from_utf8_lossy(&output.stdout),
269				String::from_utf8_lossy(&output.stderr),
270			);
271
272			assert_eq!(
273				String::from_utf8_lossy(&output.stdout).to_string(),
274				"".to_owned(),
275				"Standard out for simple example color did not match!",
276			);
277			assert_eq!(
278				String::from_utf8_lossy(&output.stderr).to_string(),
279				"\u{1b}[31msimple\u{1b}[39m/\u{1b}[1m\u{1b}[37mINFO \u{1b}[39m\u{1b}[0m|Hello from simple!                                                |                    \n  \u{1b}[92msdio\u{1b}[39m/\u{1b}[1m\u{1b}[37mINFO \u{1b}[39m\u{1b}[0m|Hello from other task!                                            |extra.data=hello w \n            |                                                                  |orld               \n\u{1b}[31msimple\u{1b}[39m/\u{1b}[1m\u{1b}[37mINFO \u{1b}[39m\u{1b}[0m|This will be rendered in \u{1b}[31mC\u{1b}[39m\u{1b}[91mO\u{1b}[39m\u{1b}[33mL\u{1b}[39m\u{1b}[32mO\u{1b}[39m\u{1b}[34mR\u{1b}[39m\u{1b}[35m!\u{1b}[39m if supported! # \u{1b}[31mG\u{1b}[39m\u{1b}[37mA\u{1b}[39m\u{1b}[35mY\u{1b}[39mPlay           |                    \n",
280			);
281		}
282
283		{
284			let output = runner
285				.command()
286				.env("SIMPLE_LOG_FORMAT", "text")
287				.stdout(Stdio::piped())
288				.stderr(Stdio::piped())
289				.spawn()
290				.expect("Failed to spawn and run simple example in color mode!")
291				.wait_with_output()
292				.expect("Failed to get output from simple example!");
293			assert!(
294				output.status.success(),
295				"Simple output example did not complete successfully!\n\nStdout: {}\n\nStderr: {}",
296				String::from_utf8_lossy(&output.stdout),
297				String::from_utf8_lossy(&output.stderr),
298			);
299
300			assert_eq!(
301				String::from_utf8_lossy(&output.stdout).to_string(),
302				"".to_owned(),
303				"Standard out for simple example text did not match!",
304			);
305
306			// Can't match exactly because of timestamps.
307			// Validate everything is printed though.
308			let stderr = String::from_utf8_lossy(&output.stderr).to_string();
309			assert!(
310				!stderr.contains('\u{1b}'),
311				"Standard error for simple example text rendered with an ANSI escape sequence! It should never!",
312			);
313			assert!(
314				stderr.contains("simple/INFO|Hello from simple!||"),
315				"Standard error for simple text did not have hello message",
316			);
317			assert!(
318				stderr.contains("sdio/INFO|Hello from other task!|extra.data=hello world|"),
319				"Standard error for simple text did not have sdio hello message",
320			);
321			assert!(
322				stderr.contains(
323					"simple/INFO|This will be rendered in COLOR! if supported! # GAYPlay||"
324				),
325				"Standard error for simple text did not have simple text color message",
326			);
327		}
328
329		{
330			let output = runner
331				.command()
332				.env("SIMPLE_LOG_FORMAT", "json")
333				.stdout(Stdio::piped())
334				.stderr(Stdio::piped())
335				.spawn()
336				.expect("Failed to spawn and run simple example in color mode!")
337				.wait_with_output()
338				.expect("Failed to get output from simple example!");
339			assert!(
340				output.status.success(),
341				"Simple output example did not complete successfully!\n\nStdout: {}\n\nStderr: {}",
342				String::from_utf8_lossy(&output.stdout),
343				String::from_utf8_lossy(&output.stderr),
344			);
345
346			assert_eq!(
347				String::from_utf8_lossy(&output.stdout).to_string(),
348				"".to_owned(),
349				"Standard out for simple example text did not match!",
350			);
351
352			for (idx, line) in String::from_utf8_lossy(&output.stderr)
353				.to_string()
354				.lines()
355				.enumerate()
356			{
357				let data: serde_json::Map<String, serde_json::Value> = serde_json::from_str(&line)
358					.expect("Failed to parse log line in json mode as JSON!");
359
360				if idx != 1 {
361					assert!(
362						data.get("metadata")
363							.expect("Failed to get metadata key!")
364							.as_object()
365							.expect("Failed to get metadata as object!")
366							.is_empty(),
367						"Metadata is supposed to be empty for this log line but it wasn't!",
368					);
369
370					assert!(
371						data.get("lisa")
372							.expect("Failed to get lisa key!")
373							.as_object()
374							.expect("Failed to get lisa as object!")
375							.get("id")
376							.expect("Failed to get `lisa.id` key")
377							.is_null(),
378					);
379
380					assert!(
381						data.get("lisa")
382							.expect("Failed to get lisa key!")
383							.as_object()
384							.expect("Failed to get lisa as object!")
385							.get("subsystem")
386							.expect("Failed to get `lisa.subsystem` key")
387							.is_null(),
388					);
389				} else {
390					assert_eq!(
391						data.get("metadata")
392							.expect("Failed to get metadata key!")
393							.as_object()
394							.expect("Failed to get metadata as object!")
395							.get("extra.data")
396							.expect("Failed to get metadata `extra.data`")
397							.as_str()
398							.expect("Failed to get metadata extra data as string!"),
399						"hello world",
400					);
401
402					assert_eq!(
403						data.get("lisa")
404							.expect("Failed to get lisa key!")
405							.as_object()
406							.expect("Failed to get lisa as object!")
407							.get("id")
408							.expect("Failed to get metadata `lisa.id`")
409							.as_str()
410							.expect("Failed to get lisa id as string!"),
411						"lisa::simple::example",
412					);
413
414					assert_eq!(
415						data.get("lisa")
416							.expect("Failed to get lisa key!")
417							.as_object()
418							.expect("Failed to get lisa as object!")
419							.get("subsystem")
420							.expect("Failed to get metadata `lisa.subsystem`")
421							.as_str()
422							.expect("Failed to get lisa id as string!"),
423						"sdio",
424					);
425				}
426
427				assert_eq!(
428					data.get("msg")
429						.expect("Failed to get message attribute!")
430						.as_str()
431						.expect("Failed to get msg attribute as string!"),
432					if idx == 0 {
433						"Hello from simple!"
434					} else if idx == 1 {
435						"Hello from other task!"
436					} else {
437						"This will be rendered in \u{1b}[31mC\u{1b}[39m\u{1b}[91mO\u{1b}[39m\u{1b}[33mL\u{1b}[39m\u{1b}[32mO\u{1b}[39m\u{1b}[34mR\u{1b}[39m\u{1b}[35m!\u{1b}[39m if supported! # \u{1b}[31mG\u{1b}[39m\u{1b}[37mA\u{1b}[39m\u{1b}[35mY\u{1b}[39mPlay"
438					},
439				);
440			}
441		}
442	}
443}