core_lib/log/
logger.rs

1use std::{io::SeekFrom, path::PathBuf, sync::Arc};
2
3use anyhow::Ok;
4use chrono::Local;
5use dirs::home_dir;
6use tokio::{
7    fs::{File, remove_file},
8    io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt},
9    sync::Mutex,
10};
11
12#[derive(Debug, Clone)]
13pub struct Logger {
14    pub file: Arc<Mutex<tokio::fs::File>>,
15    path: String,
16    color_enable: bool,
17}
18
19const RESET: &str = "\x1b[0m";
20const BG_BLUE: &str = "\x1b[44m"; // info
21const BG_ORANGE: &str = "\x1b[48;5;208m"; // warning 
22const BG_RED: &str = "\x1b[41m";
23const BG_GREEN: &str = "\x1b[42m"; // job start 
24const BG_MAGENTA: &str = "\x1b[45m"; // job end 
25const FG_BOLD_WHITE: &str = "\x1b[97;1m";
26
27pub enum LogLevel {
28    Info,
29    Warning,
30    Error,
31}
32
33impl Logger {
34    pub async fn new(path: &std::path::Path) -> anyhow::Result<Self> {
35        let file = tokio::fs::OpenOptions::new()
36            .append(true)
37            .create(true)
38            .open(path)
39            .await?;
40        let no_color = std::env::var("FLEET_NO_COLOR").ok().as_deref() == Some("1");
41        Ok(Self {
42            file: Arc::new(Mutex::new(file)),
43            path: String::from(path.to_str().unwrap_or("")),
44            color_enable: !no_color,
45        })
46    }
47
48    pub fn path_by_id(id: &str) -> PathBuf {
49        let home = home_dir().unwrap();
50
51        let log_dir = home.join(".fleet").join("logs");
52        log_dir.join(id.to_string() + ".log")
53    }
54
55    pub fn rm_logs_by_id(id: &str) -> anyhow::Result<()> {
56        let path = Logger::path_by_id(id);
57
58        if path.exists() {
59            std::fs::remove_file(path)?;
60        }
61        Ok(())
62    }
63
64    pub async fn fetchn(id: &str, n: usize) -> anyhow::Result<Vec<String>> {
65        let path = Logger::path_by_id(id);
66
67        // Vérifier que le fichier existe
68        if !tokio::fs::try_exists(&path).await? {
69            return Err(anyhow::anyhow!("Failed to find log file"));
70        }
71
72        let mut file = File::open(&path).await?;
73        let metadata = file.metadata().await?;
74        let file_size = metadata.len();
75
76        let mut buffer = vec![0; 8192];
77        let mut collected = Vec::new();
78        let mut carry = String::new();
79
80        let mut pos = file_size as i64;
81
82        while pos > 0 && collected.len() < n {
83            let read_size = buffer.len().min(pos as usize);
84            pos -= read_size as i64;
85
86            file.seek(SeekFrom::Start(pos as u64)).await?;
87
88            file.read_exact(&mut buffer[..read_size]).await?;
89
90            let chunk = String::from_utf8_lossy(&buffer[..read_size]);
91
92            let combined = format!("{chunk}{carry}");
93            let mut parts: Vec<&str> = combined.split('\n').collect();
94
95            carry = parts.remove(0).to_string();
96
97            for line in parts.into_iter().rev() {
98                if !line.is_empty() {
99                    collected.push(line.to_string());
100                    if collected.len() >= n {
101                        break;
102                    }
103                }
104            }
105        }
106
107        if !carry.is_empty() && collected.len() < n {
108            collected.push(carry);
109        }
110
111        collected.reverse();
112
113        Ok(collected)
114    }
115
116    pub fn placeholder() -> Logger {
117        Logger {
118            file: Arc::new(Mutex::new(tokio::fs::File::from_std(
119                std::fs::File::create("/dev/null").unwrap(),
120            ))),
121            path: String::new(),
122            color_enable: false,
123        }
124    }
125
126    fn paint_level(level: &str) -> String {
127        match level {
128            "INFO" => format!("{BG_BLUE}{FG_BOLD_WHITE} {level} {RESET}"),
129            "WARNING" => format!("{BG_ORANGE}{FG_BOLD_WHITE} {level} {RESET}"),
130            "ERROR" => format!("{BG_RED}{FG_BOLD_WHITE} {level} {RESET}"),
131            "JOB START" => format!("{BG_GREEN}{FG_BOLD_WHITE} {level} {RESET}"),
132            "JOB END" => format!("{BG_MAGENTA}{FG_BOLD_WHITE} {level} {RESET}"),
133            _ => level.to_string(),
134        }
135    }
136
137    pub async fn log(&self, level: &str, msg: &str) -> anyhow::Result<()> {
138        let mut f = self.file.lock().await;
139        let now = Local::now();
140        let line = format!(
141            "[{}] {}: {}\n",
142            now.format("%Y-%m-%d %H:%M:%S"),
143            Logger::paint_level(level),
144            msg
145        );
146        f.write_all(line.as_bytes()).await?;
147        f.flush().await?;
148        Ok(())
149    }
150
151    pub async fn info(&self, msg: &str) -> anyhow::Result<()> {
152        self.log("INFO", msg).await
153    }
154
155    pub async fn warning(&self, msg: &str) -> anyhow::Result<()> {
156        self.log("WARNING", msg).await
157    }
158
159    pub async fn error(&self, msg: &str) -> anyhow::Result<()> {
160        self.log("ERROR", msg).await
161    }
162
163    pub async fn job_start(&self, msg: &str) -> anyhow::Result<()> {
164        self.log("JOB START", msg).await
165    }
166
167    pub async fn job_end(&self, msg: &str) -> anyhow::Result<()> {
168        self.log("JOB END", msg).await
169    }
170
171    pub async fn clean(&self) -> anyhow::Result<()> {
172        let log_path = self.get_path()?;
173        remove_file(&log_path)
174            .await
175            .map_err(|e| anyhow::anyhow!("Failed to remove log_file {log_path} : {e}"))?;
176        Ok(())
177    }
178
179    pub fn get_path(&self) -> Result<String, anyhow::Error> {
180        if self.path.is_empty() {
181            Err(anyhow::anyhow!("Failed to find log path"))
182        } else {
183            Ok(self.path.clone())
184        }
185    }
186
187    pub fn write(msg: &str, level: LogLevel) {
188        let now = Local::now();
189        let log = |s: &str| {
190            let line = format!(
191                "[{}] {}: {}\n",
192                now.format("%Y-%m-%d %H:%M:%S"),
193                Logger::paint_level(s),
194                msg
195            );
196            println!("{line}");
197        };
198
199        match level {
200            LogLevel::Info => {
201                log("INFO");
202            }
203            LogLevel::Warning => {
204                log("WARNING");
205            }
206            LogLevel::Error => {
207                log("ERROR");
208            }
209        }
210    }
211}