//! 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"
},
);
}
}
}
}