1pub 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
108static HAS_REGISTERED_LOGGER: AtomicBool = AtomicBool::new(false);
110
111pub 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
137pub 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#[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 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}