Skip to main content

greentic_operator/
operator_log.rs

1use std::{
2    fs::{File, OpenOptions},
3    io,
4    io::Write,
5    path::{Path, PathBuf},
6    sync::Mutex,
7};
8
9use anyhow::Context;
10use chrono::Utc;
11use std::sync::OnceLock;
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
14pub enum Level {
15    Trace,
16    Debug,
17    Info,
18    Warn,
19    Error,
20}
21
22struct Logger {
23    writer: Mutex<File>,
24    min_level: Level,
25}
26
27static LOGGER: OnceLock<Logger> = OnceLock::new();
28
29pub fn init(log_dir: PathBuf, min_level: Level) -> anyhow::Result<PathBuf> {
30    let fallback = std::env::current_dir()
31        .unwrap_or_else(|_| PathBuf::from("."))
32        .join("logs");
33
34    let mut candidates = vec![log_dir.clone()];
35    if fallback != log_dir {
36        candidates.push(fallback.clone());
37    }
38
39    let mut last_error: Option<(PathBuf, io::Error)> = None;
40    for candidate in candidates {
41        match try_open_operator_log(&candidate) {
42            Ok(file) => {
43                let logger = Logger {
44                    writer: Mutex::new(file),
45                    min_level,
46                };
47                if LOGGER.set(logger).is_err() {
48                    anyhow::bail!("operator logger already initialized");
49                }
50                if candidate != log_dir {
51                    eprintln!(
52                        "unable to write operator.log at {}; falling back to {}",
53                        log_dir.display(),
54                        candidate.display()
55                    );
56                }
57                return Ok(candidate);
58            }
59            Err(err) => {
60                last_error = Some((candidate, err));
61            }
62        }
63    }
64
65    if let Some((path, err)) = last_error {
66        Err(anyhow::anyhow!(
67            "unable to open operator log at {}: {}",
68            path.display(),
69            err
70        ))
71    } else {
72        anyhow::bail!("unable to initialize operator log")
73    }
74}
75
76fn try_open_operator_log(log_dir: &Path) -> io::Result<File> {
77    std::fs::create_dir_all(log_dir)?;
78    let operator_path = log_dir.join("operator.log");
79    OpenOptions::new()
80        .create(true)
81        .append(true)
82        .open(&operator_path)
83}
84
85pub fn log(level: Level, target: &str, message: String) {
86    let logger = match LOGGER.get() {
87        Some(logger) => logger,
88        None => return,
89    };
90    if level < logger.min_level {
91        return;
92    }
93    let mut writer = match logger.writer.lock() {
94        Ok(writer) => writer,
95        Err(_) => return,
96    };
97    let timestamp = Utc::now().to_rfc3339();
98    if writeln!(
99        *writer,
100        "{timestamp} [{level:?}] {target} - {message}",
101        level = level,
102        target = target,
103        message = message
104    )
105    .is_err()
106    {
107        let _ = writer.flush();
108    }
109}
110
111pub fn service_log_path(log_dir: &Path, service: &str) -> PathBuf {
112    log_dir.join(format!("{service}.log"))
113}
114
115pub fn reserve_service_log(log_dir: &Path, service: &str) -> anyhow::Result<PathBuf> {
116    let path = service_log_path(log_dir, service);
117    if let Some(parent) = path.parent() {
118        std::fs::create_dir_all(parent)?;
119    }
120    OpenOptions::new()
121        .create(true)
122        .append(true)
123        .open(&path)
124        .with_context(|| format!("unable to open {} log file at {}", service, path.display()))?;
125    Ok(path)
126}
127
128pub fn trace(target: &str, message: impl AsRef<str>) {
129    log(Level::Trace, target, message.as_ref().to_string());
130}
131
132pub fn debug(target: &str, message: impl AsRef<str>) {
133    log(Level::Debug, target, message.as_ref().to_string());
134}
135
136pub fn info(target: &str, message: impl AsRef<str>) {
137    log(Level::Info, target, message.as_ref().to_string());
138}
139
140pub fn warn(target: &str, message: impl AsRef<str>) {
141    log(Level::Warn, target, message.as_ref().to_string());
142}
143
144pub fn error(target: &str, message: impl AsRef<str>) {
145    log(Level::Error, target, message.as_ref().to_string());
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use std::fs;
152    use tempfile::tempdir;
153
154    #[test]
155    fn writes_operator_log() -> anyhow::Result<()> {
156        let dir = tempdir()?;
157        let _ = init(dir.path().to_path_buf(), Level::Info)?;
158        info("tests::writes_operator_log", "hello world");
159        let contents = fs::read_to_string(dir.path().join("operator.log"))?;
160        assert!(contents.contains("hello world"));
161        Ok(())
162    }
163}