nap_core/server/
logging.rs1use anyhow::{Context, Result};
8use std::fs::OpenOptions;
9use std::path::Path;
10
11use tracing_appender::rolling::{RollingFileAppender, Rotation};
12use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt};
13
14pub fn init_persistent_logging(nap_home: &Path) -> Result<()> {
16 let logs_dir = nap_home.join("logs");
17 std::fs::create_dir_all(&logs_dir).context("Failed to create logs directory")?;
18
19 let nap_log_path = logs_dir.join("nap.log");
21
22 let lore_log_path = logs_dir.join("loreserver.log");
24
25 let nap_file = OpenOptions::new()
27 .create(true)
28 .append(true)
29 .open(&nap_log_path)
30 .context("Failed to open NAP log file")?;
31
32 let _lore_file = OpenOptions::new()
33 .create(true)
34 .append(true)
35 .open(&lore_log_path)
36 .context("Failed to open Lore server log file")?;
37
38 let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
39
40 tracing_subscriber::registry()
41 .with(env_filter)
42 .with(fmt::layer().with_writer(nap_file))
43 .init();
44
45 tracing::info!("Persistent logging initialized");
46 tracing::info!("NAP log: {}", nap_log_path.display());
47 tracing::info!("Lore server log: {}", lore_log_path.display());
48
49 Ok(())
50}
51
52pub fn init_rolling_logging(nap_home: &Path) -> Result<()> {
54 let logs_dir = nap_home.join("logs");
55 std::fs::create_dir_all(&logs_dir).context("Failed to create logs directory")?;
56
57 let nap_appender = RollingFileAppender::new(Rotation::DAILY, &logs_dir, "nap.log");
59
60 let lore_appender = RollingFileAppender::new(Rotation::DAILY, &logs_dir, "loreserver.log");
62
63 let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
64
65 tracing_subscriber::registry()
66 .with(env_filter)
67 .with(fmt::layer().with_writer(nap_appender))
68 .with(fmt::layer().with_writer(lore_appender))
69 .init();
70
71 tracing::info!("Rolling logging initialized");
72 tracing::info!("Logs directory: {}", logs_dir.display());
73
74 Ok(())
75}
76
77pub fn nap_log_path(nap_home: &Path) -> std::path::PathBuf {
79 nap_home.join("logs").join("nap.log")
80}
81
82pub fn lore_log_path(nap_home: &Path) -> std::path::PathBuf {
84 nap_home.join("logs").join("loreserver.log")
85}
86
87pub fn read_recent_logs(log_path: &Path, line_count: usize) -> Result<Vec<String>> {
89 let content = std::fs::read_to_string(log_path).context("Failed to read log file")?;
90
91 let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
92
93 let recent_lines = if lines.len() > line_count {
94 lines[lines.len() - line_count..].to_vec()
95 } else {
96 lines
97 };
98
99 Ok(recent_lines)
100}
101
102pub fn tail_log(log_path: &Path, line_count: usize) -> Result<Vec<String>> {
104 read_recent_logs(log_path, line_count)
105}
106
107pub fn clear_logs(nap_home: &Path) -> Result<()> {
109 let logs_dir = nap_home.join("logs");
110
111 if !logs_dir.exists() {
112 return Ok(());
113 }
114
115 let nap_log = nap_log_path(nap_home);
116 let lore_log = lore_log_path(nap_home);
117
118 if nap_log.exists() {
119 std::fs::write(&nap_log, "").context("Failed to clear NAP log")?;
120 }
121
122 if lore_log.exists() {
123 std::fs::write(&lore_log, "").context("Failed to clear Lore server log")?;
124 }
125
126 tracing::info!("Logs cleared");
127 Ok(())
128}
129
130pub fn log_file_size(log_path: &Path) -> Result<u64> {
132 let metadata = std::fs::metadata(log_path).context("Failed to get log file metadata")?;
133 Ok(metadata.len())
134}
135
136pub fn total_log_size(nap_home: &Path) -> Result<u64> {
138 let nap_log = nap_log_path(nap_home);
139 let lore_log = lore_log_path(nap_home);
140
141 let mut total = 0u64;
142
143 if nap_log.exists() {
144 total += log_file_size(&nap_log)?;
145 }
146
147 if lore_log.exists() {
148 total += log_file_size(&lore_log)?;
149 }
150
151 Ok(total)
152}
153
154#[derive(Debug, Clone)]
156pub struct LogFileInfo {
157 pub path: std::path::PathBuf,
158 pub size_bytes: u64,
159 pub exists: bool,
160}
161
162pub fn log_files_info(nap_home: &Path) -> Result<Vec<LogFileInfo>> {
164 let nap_log = nap_log_path(nap_home);
165 let lore_log = lore_log_path(nap_home);
166
167 let mut files = vec![];
168
169 files.push(LogFileInfo {
170 path: nap_log.clone(),
171 size_bytes: if nap_log.exists() {
172 log_file_size(&nap_log)?
173 } else {
174 0
175 },
176 exists: nap_log.exists(),
177 });
178
179 files.push(LogFileInfo {
180 path: lore_log.clone(),
181 size_bytes: if lore_log.exists() {
182 log_file_size(&lore_log)?
183 } else {
184 0
185 },
186 exists: lore_log.exists(),
187 });
188
189 Ok(files)
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use tempfile::TempDir;
196
197 #[test]
198 fn test_log_paths() {
199 let temp_dir = TempDir::new().unwrap();
200 let nap_log = nap_log_path(temp_dir.path());
201 let lore_log = lore_log_path(temp_dir.path());
202
203 assert_eq!(nap_log, temp_dir.path().join("logs").join("nap.log"));
204 assert_eq!(
205 lore_log,
206 temp_dir.path().join("logs").join("loreserver.log")
207 );
208 }
209
210 #[test]
211 fn test_log_file_size() -> Result<()> {
212 let temp_dir = TempDir::new().unwrap();
213 let log_path = temp_dir.path().join("test.log");
214
215 std::fs::write(&log_path, "test content")?;
216
217 let size = log_file_size(&log_path)?;
218 assert_eq!(size, 12);
219
220 Ok(())
221 }
222
223 #[test]
224 fn test_read_recent_logs() -> Result<()> {
225 let temp_dir = TempDir::new().unwrap();
226 let log_path = temp_dir.path().join("test.log");
227
228 let content = "line1\nline2\nline3\nline4\nline5";
229 std::fs::write(&log_path, content)?;
230
231 let recent = read_recent_logs(&log_path, 2)?;
232 assert_eq!(recent.len(), 2);
233 assert_eq!(recent[0], "line4");
234 assert_eq!(recent[1], "line5");
235
236 Ok(())
237 }
238
239 #[test]
240 fn test_tail_log() -> Result<()> {
241 let temp_dir = TempDir::new().unwrap();
242 let log_path = temp_dir.path().join("test.log");
243
244 let content = "line1\nline2\nline3";
245 std::fs::write(&log_path, content)?;
246
247 let tail = tail_log(&log_path, 2)?;
248 assert_eq!(tail.len(), 2);
249 assert_eq!(tail[0], "line2");
250 assert_eq!(tail[1], "line3");
251
252 Ok(())
253 }
254
255 #[test]
256 fn test_clear_logs() -> Result<()> {
257 let temp_dir = TempDir::new().unwrap();
258 let logs_dir = temp_dir.path().join("logs");
259 std::fs::create_dir_all(&logs_dir)?;
260
261 let nap_log = nap_log_path(temp_dir.path());
262 std::fs::write(&nap_log, "some content")?;
263
264 clear_logs(temp_dir.path())?;
265
266 let content = std::fs::read_to_string(&nap_log)?;
267 assert_eq!(content, "");
268
269 Ok(())
270 }
271
272 #[test]
273 fn test_log_files_info() -> Result<()> {
274 let temp_dir = TempDir::new().unwrap();
275 let logs_dir = temp_dir.path().join("logs");
276 std::fs::create_dir_all(&logs_dir)?;
277
278 let nap_log = nap_log_path(temp_dir.path());
279 std::fs::write(&nap_log, "test content")?;
280
281 let info = log_files_info(temp_dir.path())?;
282 assert_eq!(info.len(), 2);
283 assert!(info[0].exists);
284 assert_eq!(info[0].size_bytes, 12);
285
286 Ok(())
287 }
288}