rm-lisa 0.3.2

A logging library for rem-verse, with support for inputs, tasks, and more.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
//! The logging library for Rem-Verse.
//!
//! Some Key Features:
//!
//! - Multiple Logging Formats, that are built for many individuals.
//!   - Colored for pretty ascii color & erasing interfaces.
//!   - Plaintext for a simple line based format ideal for braille displays,
//!     and log files.
//!   - JSON for structured logging, with quick parsing/slicing/dicing support.
//! - Task Status, and Logging.
//!   - Queue up many background tasks, and have log lines prefixed with task
//!     names, and status updates.
//! - Shell Interface Building with tab-complete support.
//! - Full customization at runtime of data to include by users.
//!
//! # Getting started
//!
//! To get started, you should simply initialize the super console at the start
//! of the program, and keep a guard dropping it when you want to ensure all
//! logs are flushed (e.g. at program exit.). This does assume your program is
//! running in a tokio runtime.
//!
//! ```rust,no_run
//! use rm_lisa::initialize_logging;
//!
//! async fn my_main() {
//!   let console = initialize_logging("my-application-name").expect("Failed to initialize logging!");
//!   // ... do my stuff ...
//!   console.flush().await;
//! }
//! ```
//!
//! Once you've initialized the super console, all you need to do is use
//! tracing like normal:
//!
//! ```rust,no_run
//! use tracing::info;
//!
//! info!("my cool log line!");
//! ```
//!
//! You can also specify a series of fields to customize your log message. Like:
//!
//! - `lisa.force_combine_fields`: force combine the metadata, and message to render
//!   (e.g. render without the gutter).
//! - `lisa.hide_fields_for_humans`: hide any fields that probably aren't useful for
//!   humans. (e.g. `id`).
//! - `lisa.subsystem`: the 'name' of the the system that logged this line.
//!   Renders in the first part of the line instead of application name.
//! - `lisa.stdout`: for this log line going to STDOUT instead of STDERR.
//! - `lisa.decorate`: render the application name/log level on text rendering.
//!
//! These messages can be set on individual log messages themselves or set for
//! everything in a scope:
//!
//! ```rust,no_run
//! use tracing::{Instrument, error_span, info};
//!
//! async fn my_cool_function() {
//!   async {
//!     info!("Hello with details from span!");
//!   }.instrument(
//!     // we want our span attached to every message, making it error ensures that happens.
//!     error_span!("my_span", lisa.subsystem="my_cool_task", lisa.stdout=true)
//!   );
//! }
//! ```
//!
//! It is also recommended to include an `id` set to a unique string per log
//! line. As when a user requests JSON output, without an ID it can be hard to
//! know what schema the log will be following (What fields will be presenet,
//! etc.). Having an `id` field can be used for that, and will be hidden on
//! color/text renderers automatically as they are not useful there.

pub mod display;
pub mod errors;
pub mod input;
pub mod tasks;
#[cfg(any(
	target_os = "linux",
	target_os = "android",
	target_os = "macos",
	target_os = "ios",
	target_os = "freebsd",
	target_os = "openbsd",
	target_os = "netbsd",
	target_os = "dragonfly",
	target_os = "solaris",
	target_os = "illumos",
	target_os = "aix",
	target_os = "haiku",
))]
pub mod termios;

use crate::{
	display::{SuperConsole, renderers::ConsoleOutputFeatures, tracing::TracableSuperConsole},
	errors::LisaError,
};
use std::{
	io::{Stderr as StdStderr, Stdout as StdStdout, Write as IoWrite},
	sync::{
		Arc,
		atomic::{AtomicBool, Ordering as AtomicOrdering},
	},
};
use tracing_subscriber::{EnvFilter, prelude::*, registry as subscriber_registry};

/// If we have already registered a logger.
static HAS_REGISTERED_LOGGER: AtomicBool = AtomicBool::new(false);

/// Initialize the Lisa logger, this will error if we run into any errors
/// actually setting up everything that needs to be set-up.
///
/// ## Errors
///
/// If we cannot determine which [`crate::display::renderers::ConsoleRenderer`]
/// to utilize.
pub fn initialize_logging(
	app_name: &'static str,
) -> Result<Arc<SuperConsole<StdStdout, StdStderr>>, LisaError> {
	if HAS_REGISTERED_LOGGER.swap(true, AtomicOrdering::SeqCst) {
		return Err(LisaError::AlreadyRegistered);
	}

	let console = Arc::new(SuperConsole::new(app_name)?);
	register_panic_hook(&console);

	let filter_layer = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
	let registry = subscriber_registry().with(filter_layer);
	registry
		.with(TracableSuperConsole::new(console.clone()))
		.init();

	Ok(console)
}

/// Initialize the Lisa logger with a pre-configured super console.
///
/// ## Errors
///
/// If the logger has already been registered.
pub fn initialize_with_console<
	Stdout: IoWrite + ConsoleOutputFeatures + Send + 'static,
	Stderr: IoWrite + ConsoleOutputFeatures + Send + 'static,
>(
	console: SuperConsole<Stdout, Stderr>,
) -> Result<Arc<SuperConsole<Stdout, Stderr>>, LisaError> {
	if HAS_REGISTERED_LOGGER.swap(true, AtomicOrdering::SeqCst) {
		return Err(LisaError::AlreadyRegistered);
	}
	let arc_console = Arc::new(console);
	register_panic_hook(&arc_console);

	let filter_layer = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
	let registry = subscriber_registry().with(filter_layer);
	registry
		.with(TracableSuperConsole::new(arc_console.clone()))
		.init();

	Ok(arc_console)
}

/// Create an environment variable prefix given an application name.
///
/// This will turn an app name into a prefix based off the following:
///
/// - Iterate through each character:
///   - If the character is ascii alphanumeric: uppercase it and append
///     it to the string.
///   - If the character is anything else append '_'.
/// - Append one final '_' if the string doesn't edit with it already.
///
/// Some examples:
///
/// ```
/// use rm_lisa::app_name_to_prefix;
///
/// assert_eq!(app_name_to_prefix("sprig"), "SPRIG");
/// assert_eq!(app_name_to_prefix("cafe-sdk"), "CAFE_SDK");
/// assert_eq!(app_name_to_prefix("Café"), "CAF_");
/// ```
#[must_use]
pub fn app_name_to_prefix(app_name: &'static str) -> String {
	let mut buffer = String::with_capacity(app_name.len() + 1);

	for character in app_name.chars() {
		if character.is_ascii_alphanumeric() {
			buffer.push(character.to_ascii_uppercase());
		} else {
			buffer.push('_');
		}
	}

	buffer
}

fn register_panic_hook<
	Stdout: IoWrite + ConsoleOutputFeatures + Send + 'static,
	Stderr: IoWrite + ConsoleOutputFeatures + Send + 'static,
>(
	console: &Arc<SuperConsole<Stdout, Stderr>>,
) {
	let weak = Arc::downgrade(console);

	let previously_registered_hook = std::panic::take_hook();
	std::panic::set_hook(Box::new(move |arg| {
		if let Some(c) = weak.upgrade() {
			_ = c.flush();
		}
		previously_registered_hook(arg);
	}));
}

#[cfg(test)]
mod unit_tests {
	use super::*;
	use crate::display::renderers::JSONConsoleRenderer;
	use escargot::CargoBuild;
	use std::process::Stdio;

	#[tokio::test]
	pub async fn initializing_only_works_once() {
		initialize_logging("librs_unit_tests").expect("First initialization should always work!");

		assert!(
			initialize_logging("any").is_err(),
			"Initializing logging multiple times doesn't work!",
		);
		assert!(
			initialize_with_console(
				SuperConsole::new_preselected_renderers(
					"librs_unit_tests",
					Box::new(JSONConsoleRenderer::new()),
					Box::new(JSONConsoleRenderer::new()),
				)
				.expect("Failed to create new_preselected_renderers")
			)
			.is_err(),
			"Initializing logging multiple times doesn't work!",
		);
		assert!(
			initialize_logging("test").is_err(),
			"Initializing logging multiple times doesn't work!",
		);
	}

	#[test]
	pub fn test_simple_golden() {
		let runner = CargoBuild::new()
			.example("simple")
			.run()
			.expect("Failed to get runner for simple example!");

		{
			let output = runner
				.command()
				.env("SIMPLE_LOG_FORMAT", "color")
				.env("SIMPLE_FORCE_TERM_WIDTH", "100")
				.stdout(Stdio::piped())
				.stderr(Stdio::piped())
				.spawn()
				.expect("Failed to spawn and run simple example in color mode!")
				.wait_with_output()
				.expect("Failed to get output from simple example!");
			assert!(
				output.status.success(),
				"Simple output example did not complete successfully!\n\nStdout: {}\n\nStderr: {}",
				String::from_utf8_lossy(&output.stdout),
				String::from_utf8_lossy(&output.stderr),
			);

			assert_eq!(
				String::from_utf8_lossy(&output.stdout).to_string(),
				"".to_owned(),
				"Standard out for simple example color did not match!",
			);
			assert_eq!(
				String::from_utf8_lossy(&output.stderr).to_string(),
				"\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",
			);
		}

		{
			let output = runner
				.command()
				.env("SIMPLE_LOG_FORMAT", "text")
				.stdout(Stdio::piped())
				.stderr(Stdio::piped())
				.spawn()
				.expect("Failed to spawn and run simple example in color mode!")
				.wait_with_output()
				.expect("Failed to get output from simple example!");
			assert!(
				output.status.success(),
				"Simple output example did not complete successfully!\n\nStdout: {}\n\nStderr: {}",
				String::from_utf8_lossy(&output.stdout),
				String::from_utf8_lossy(&output.stderr),
			);

			assert_eq!(
				String::from_utf8_lossy(&output.stdout).to_string(),
				"".to_owned(),
				"Standard out for simple example text did not match!",
			);

			// Can't match exactly because of timestamps.
			// Validate everything is printed though.
			let stderr = String::from_utf8_lossy(&output.stderr).to_string();
			assert!(
				!stderr.contains('\u{1b}'),
				"Standard error for simple example text rendered with an ANSI escape sequence! It should never!",
			);
			assert!(
				stderr.contains("simple/INFO|Hello from simple!||"),
				"Standard error for simple text did not have hello message",
			);
			assert!(
				stderr.contains("sdio/INFO|Hello from other task!|extra.data=hello world|"),
				"Standard error for simple text did not have sdio hello message",
			);
			assert!(
				stderr.contains(
					"simple/INFO|This will be rendered in COLOR! if supported! # GAYPlay||"
				),
				"Standard error for simple text did not have simple text color message",
			);
		}

		{
			let output = runner
				.command()
				.env("SIMPLE_LOG_FORMAT", "json")
				.stdout(Stdio::piped())
				.stderr(Stdio::piped())
				.spawn()
				.expect("Failed to spawn and run simple example in color mode!")
				.wait_with_output()
				.expect("Failed to get output from simple example!");
			assert!(
				output.status.success(),
				"Simple output example did not complete successfully!\n\nStdout: {}\n\nStderr: {}",
				String::from_utf8_lossy(&output.stdout),
				String::from_utf8_lossy(&output.stderr),
			);

			assert_eq!(
				String::from_utf8_lossy(&output.stdout).to_string(),
				"".to_owned(),
				"Standard out for simple example text did not match!",
			);

			for (idx, line) in String::from_utf8_lossy(&output.stderr)
				.to_string()
				.lines()
				.enumerate()
			{
				let data: serde_json::Map<String, serde_json::Value> = serde_json::from_str(&line)
					.expect("Failed to parse log line in json mode as JSON!");

				if idx != 1 {
					assert!(
						data.get("metadata")
							.expect("Failed to get metadata key!")
							.as_object()
							.expect("Failed to get metadata as object!")
							.is_empty(),
						"Metadata is supposed to be empty for this log line but it wasn't!",
					);

					assert!(
						data.get("lisa")
							.expect("Failed to get lisa key!")
							.as_object()
							.expect("Failed to get lisa as object!")
							.get("id")
							.expect("Failed to get `lisa.id` key")
							.is_null(),
					);

					assert!(
						data.get("lisa")
							.expect("Failed to get lisa key!")
							.as_object()
							.expect("Failed to get lisa as object!")
							.get("subsystem")
							.expect("Failed to get `lisa.subsystem` key")
							.is_null(),
					);
				} else {
					assert_eq!(
						data.get("metadata")
							.expect("Failed to get metadata key!")
							.as_object()
							.expect("Failed to get metadata as object!")
							.get("extra.data")
							.expect("Failed to get metadata `extra.data`")
							.as_str()
							.expect("Failed to get metadata extra data as string!"),
						"hello world",
					);

					assert_eq!(
						data.get("lisa")
							.expect("Failed to get lisa key!")
							.as_object()
							.expect("Failed to get lisa as object!")
							.get("id")
							.expect("Failed to get metadata `lisa.id`")
							.as_str()
							.expect("Failed to get lisa id as string!"),
						"lisa::simple::example",
					);

					assert_eq!(
						data.get("lisa")
							.expect("Failed to get lisa key!")
							.as_object()
							.expect("Failed to get lisa as object!")
							.get("subsystem")
							.expect("Failed to get metadata `lisa.subsystem`")
							.as_str()
							.expect("Failed to get lisa id as string!"),
						"sdio",
					);
				}

				assert_eq!(
					data.get("msg")
						.expect("Failed to get message attribute!")
						.as_str()
						.expect("Failed to get msg attribute as string!"),
					if idx == 0 {
						"Hello from simple!"
					} else if idx == 1 {
						"Hello from other task!"
					} else {
						"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"
					},
				);
			}
		}
	}
}