Skip to main content

nap_core/server/
logging.rs

1// SPDX-FileCopyrightText: 2026 Digital Creations
2// SPDX-License-Identifier: MIT
3//! Persistent logging for NAP SDK and Lore server
4//!
5//! Provides structured logging to persistent files for diagnostics and support.
6
7use 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
14/// Initialize persistent logging for NAP SDK
15pub 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    // NAP SDK log file
20    let nap_log_path = logs_dir.join("nap.log");
21
22    // Lore server log file
23    let lore_log_path = logs_dir.join("loreserver.log");
24
25    // Initialize tracing subscriber with file output
26    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
52/// Initialize rolling log files with rotation
53pub 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    // Rolling file appender for NAP logs (daily rotation)
58    let nap_appender = RollingFileAppender::new(Rotation::DAILY, &logs_dir, "nap.log");
59
60    // Rolling file appender for Lore server logs (daily rotation)
61    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
77/// Get the path to the NAP log file
78pub fn nap_log_path(nap_home: &Path) -> std::path::PathBuf {
79    nap_home.join("logs").join("nap.log")
80}
81
82/// Get the path to the Lore server log file
83pub fn lore_log_path(nap_home: &Path) -> std::path::PathBuf {
84    nap_home.join("logs").join("loreserver.log")
85}
86
87/// Read recent log entries from a file
88pub 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
102/// Tail a log file (return last N lines)
103pub fn tail_log(log_path: &Path, line_count: usize) -> Result<Vec<String>> {
104    read_recent_logs(log_path, line_count)
105}
106
107/// Clear log files
108pub 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
130/// Get log file size in bytes
131pub 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
136/// Get total size of all log files
137pub 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/// Log file information
155#[derive(Debug, Clone)]
156pub struct LogFileInfo {
157    pub path: std::path::PathBuf,
158    pub size_bytes: u64,
159    pub exists: bool,
160}
161
162/// Get information about all log files
163pub 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}