1use std::path::PathBuf;
2
3use dirs::home_dir;
4use tokio::{
5 fs::{File, remove_file},
6 io::{AsyncReadExt, AsyncSeekExt},
7};
8
9#[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 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}