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"; const BG_ORANGE: &str = "\x1b[48;5;208m"; const BG_RED: &str = "\x1b[41m";
23const BG_GREEN: &str = "\x1b[42m"; const BG_MAGENTA: &str = "\x1b[45m"; const 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 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}