Skip to main content

core_lib/log/
logger.rs

1use std::path::PathBuf;
2
3use dirs::home_dir;
4use tokio::{
5    fs::{File, remove_file},
6    io::{AsyncReadExt, AsyncSeekExt},
7};
8
9/// Per-watch logger, kept as a thin façade over `tracing`.
10///
11/// The file-based API (`new`, `fetchn`, `path_by_id`, ...) is retained because
12/// the dashboard (`fleet stats`) and the tests read the historical
13/// `~/.fleet/logs/{id}.log` files directly. Writing happens in
14/// `crate::log::file_layer::FileLayer`: each method emits a `tracing` event
15/// carrying the `log_path`, and the layer appends the formatted line to that
16/// file.
17#[derive(Debug, Clone)]
18pub struct Logger {
19    path: String,
20}
21
22impl Logger {
23    pub async fn new(path: &std::path::Path) -> anyhow::Result<Self> {
24        let _file = tokio::fs::OpenOptions::new()
25            .append(true)
26            .create(true)
27            .open(path)
28            .await?;
29        crate::log::tracing::init_tracing();
30        Ok(Self {
31            path: String::from(path.to_str().unwrap_or("")),
32        })
33    }
34
35    pub fn path_by_id(id: &str) -> PathBuf {
36        let home = home_dir().unwrap();
37
38        let log_dir = home.join(".fleet").join("logs");
39        log_dir.join(id.to_string() + ".log")
40    }
41
42    pub fn rm_logs_by_id(id: &str) -> anyhow::Result<()> {
43        let path = Logger::path_by_id(id);
44
45        if path.exists() {
46            std::fs::remove_file(path)?;
47        }
48        Ok(())
49    }
50
51    pub async fn fetchn(id: &str, n: usize) -> anyhow::Result<Vec<String>> {
52        let path = Logger::path_by_id(id);
53
54        // Vérifier que le fichier existe
55        if !tokio::fs::try_exists(&path).await? {
56            return Err(anyhow::anyhow!("Failed to find log file"));
57        }
58
59        let mut file = File::open(&path).await?;
60        let metadata = file.metadata().await?;
61        let file_size = metadata.len();
62
63        let mut buffer = vec![0; 8192];
64        let mut collected = Vec::new();
65        let mut carry = String::new();
66
67        let mut pos = file_size as i64;
68
69        while pos > 0 && collected.len() < n {
70            let read_size = buffer.len().min(pos as usize);
71            pos -= read_size as i64;
72
73            file.seek(std::io::SeekFrom::Start(pos as u64)).await?;
74
75            file.read_exact(&mut buffer[..read_size]).await?;
76
77            let chunk = String::from_utf8_lossy(&buffer[..read_size]);
78
79            let combined = format!("{chunk}{carry}");
80            let mut parts: Vec<&str> = combined.split('\n').collect();
81
82            carry = parts.remove(0).to_string();
83
84            for line in parts.into_iter().rev() {
85                if !line.is_empty() {
86                    collected.push(line.to_string());
87                    if collected.len() >= n {
88                        break;
89                    }
90                }
91            }
92        }
93
94        if !carry.is_empty() && collected.len() < n {
95            collected.push(carry);
96        }
97
98        collected.reverse();
99
100        Ok(collected)
101    }
102
103    pub fn placeholder() -> Logger {
104        Logger {
105            path: String::new(),
106        }
107    }
108
109    pub async fn info(&self, msg: &str) -> anyhow::Result<()> {
110        tracing::info!(target: "fleet", log_path = %self.path, message = %msg);
111        Ok(())
112    }
113
114    pub async fn warning(&self, msg: &str) -> anyhow::Result<()> {
115        tracing::warn!(target: "fleet", log_path = %self.path, message = %msg);
116        Ok(())
117    }
118
119    pub async fn error(&self, msg: &str) -> anyhow::Result<()> {
120        tracing::error!(target: "fleet", log_path = %self.path, message = %msg);
121        Ok(())
122    }
123
124    pub async fn job_start(&self, msg: &str) -> anyhow::Result<()> {
125        tracing::event!(
126            target: "fleet",
127            tracing::Level::INFO,
128            log_path = %self.path,
129            kind = "JOB START",
130            message = %msg
131        );
132        Ok(())
133    }
134
135    pub async fn job_end(&self, msg: &str) -> anyhow::Result<()> {
136        tracing::event!(
137            target: "fleet",
138            tracing::Level::INFO,
139            log_path = %self.path,
140            kind = "JOB END",
141            message = %msg
142        );
143        Ok(())
144    }
145
146    pub async fn clean(&self) -> anyhow::Result<()> {
147        let log_path = self.get_path()?;
148        remove_file(&log_path)
149            .await
150            .map_err(|e| anyhow::anyhow!("Failed to remove log_file {log_path} : {e}"))?;
151        Ok(())
152    }
153
154    pub fn get_path(&self) -> Result<String, anyhow::Error> {
155        if self.path.is_empty() {
156            Err(anyhow::anyhow!("Failed to find log path"))
157        } else {
158            Ok(self.path.clone())
159        }
160    }
161}