zellij_utils/
logging.rs

1//! Zellij logging utility functions.
2
3use std::{
4    fs,
5    io::{self, prelude::*},
6    os::unix::io::RawFd,
7    path::{Path, PathBuf},
8};
9
10use log::LevelFilter;
11
12use log4rs::append::rolling_file::{
13    policy::compound::{
14        roll::fixed_window::FixedWindowRoller, trigger::size::SizeTrigger, CompoundPolicy,
15    },
16    RollingFileAppender,
17};
18use log4rs::config::{Appender, Config, Logger, Root};
19use log4rs::encode::pattern::PatternEncoder;
20
21use crate::consts::{ZELLIJ_TMP_DIR, ZELLIJ_TMP_LOG_DIR, ZELLIJ_TMP_LOG_FILE};
22use crate::shared::set_permissions;
23
24const LOG_MAX_BYTES: u64 = 1024 * 1024 * 16; // 16 MiB per log
25
26pub fn configure_logger() {
27    atomic_create_dir(&*ZELLIJ_TMP_DIR).unwrap();
28    atomic_create_dir(&*ZELLIJ_TMP_LOG_DIR).unwrap();
29    atomic_create_file(&*ZELLIJ_TMP_LOG_FILE).unwrap();
30
31    let trigger = SizeTrigger::new(LOG_MAX_BYTES);
32    let roller = FixedWindowRoller::builder()
33        .build(
34            ZELLIJ_TMP_LOG_DIR
35                .join("zellij.log.old.{}")
36                .to_str()
37                .unwrap(),
38            1,
39        )
40        .unwrap();
41
42    // {n} means platform dependent newline
43    // module is padded to exactly 25 bytes and thread is padded to be between 10 and 15 bytes.
44    let file_pattern = "{highlight({level:<6})} |{module:<25.25}| {date(%Y-%m-%d %H:%M:%S.%3f)} [{thread:<10.15}] [{file}:{line}]: {message} {n}";
45
46    // default zellij appender, should be used across most of the codebase.
47    let log_file = RollingFileAppender::builder()
48        .encoder(Box::new(PatternEncoder::new(file_pattern)))
49        .build(
50            &*ZELLIJ_TMP_LOG_FILE,
51            Box::new(CompoundPolicy::new(
52                Box::new(trigger),
53                Box::new(roller.clone()),
54            )),
55        )
56        .unwrap();
57
58    // plugin appender. To be used in logging_pipe to forward stderr output from plugins. We do some formatting
59    // in logging_pipe to print plugin name as 'module' and plugin_id instead of thread.
60    let log_plugin = RollingFileAppender::builder()
61        .encoder(Box::new(PatternEncoder::new(
62            "{highlight({level:<6})} {message} {n}",
63        )))
64        .build(
65            &*ZELLIJ_TMP_LOG_FILE,
66            Box::new(CompoundPolicy::new(Box::new(trigger), Box::new(roller))),
67        )
68        .unwrap();
69
70    // Set the default logging level to "info" and log it to zellij.log file
71    // Decrease verbosity for `wasmtime_wasi` module because it has a lot of useless info logs
72    // For `zellij_server::logging_pipe`, we use custom format as we use logging macros to forward stderr output from plugins
73    let config = Config::builder()
74        .appender(Appender::builder().build("logFile", Box::new(log_file)))
75        .appender(Appender::builder().build("logPlugin", Box::new(log_plugin)))
76        // reduce the verbosity of isahc, otherwise it logs on every failed web request
77        .logger(
78            Logger::builder()
79                .appender("logFile")
80                .build("isahc", LevelFilter::Error),
81        )
82        .logger(
83            Logger::builder()
84                .appender("logPlugin")
85                .build("wasmtime_wasi", LevelFilter::Warn),
86        )
87        .logger(
88            Logger::builder()
89                .appender("logPlugin")
90                .additive(false)
91                .build("zellij_server::logging_pipe", LevelFilter::Trace),
92        )
93        .build(Root::builder().appender("logFile").build(LevelFilter::Info))
94        .unwrap();
95
96    let _ = log4rs::init_config(config).unwrap();
97}
98
99pub fn atomic_create_file(file_name: &Path) -> io::Result<()> {
100    let _ = fs::OpenOptions::new()
101        .append(true)
102        .create(true)
103        .open(file_name)?;
104    set_permissions(file_name, 0o600)
105}
106
107pub fn atomic_create_dir(dir_name: &Path) -> io::Result<()> {
108    let result = if let Err(e) = fs::create_dir(dir_name) {
109        if e.kind() == std::io::ErrorKind::AlreadyExists {
110            Ok(())
111        } else {
112            Err(e)
113        }
114    } else {
115        Ok(())
116    };
117    if result.is_ok() {
118        set_permissions(dir_name, 0o700)?;
119    }
120    result
121}
122
123pub fn debug_to_file(message: &[u8], pid: RawFd) -> io::Result<()> {
124    let mut path = PathBuf::new();
125    path.push(&*ZELLIJ_TMP_LOG_DIR);
126    path.push(format!("zellij-{}.log", pid));
127
128    let mut file = fs::OpenOptions::new()
129        .append(true)
130        .create(true)
131        .open(&path)?;
132    set_permissions(&path, 0o600)?;
133    file.write_all(message)
134}