Skip to main content

systemprompt_cli/shared/
disk_logs.rs

1//! On-disk log-file discovery and tailing shared by the agent and MCP `logs`
2//! commands.
3//!
4//! Log files follow the `<prefix>-<name>.log` convention; lookup also accepts
5//! bare `<name>.log` paths and substring matches over the discovered set.
6//!
7//! Copyright (c) systemprompt.io — Business Source License 1.1.
8//! See <https://systemprompt.io> for licensing details.
9
10use std::path::{Path, PathBuf};
11
12use anyhow::{Context, Result, anyhow};
13
14pub fn list_log_files(logs_dir: &Path, prefix: &str) -> Result<Vec<String>> {
15    let mut files = std::fs::read_dir(logs_dir)
16        .context("Failed to read logs directory")?
17        .filter_map(Result::ok)
18        .filter_map(|entry| {
19            let path = entry.path();
20            path.file_name()
21                .and_then(|n| n.to_str())
22                .filter(|name| {
23                    name.starts_with(prefix)
24                        && path
25                            .extension()
26                            .is_some_and(|ext| ext.eq_ignore_ascii_case("log"))
27                })
28                .map(String::from)
29        })
30        .collect::<Vec<_>>();
31
32    files.sort();
33    Ok(files)
34}
35
36pub fn find_log_file(logs_dir: &Path, prefix: &str, name: &str) -> Result<PathBuf> {
37    let exact_path = logs_dir.join(format!("{name}.log"));
38    if exact_path.exists() {
39        return Ok(exact_path);
40    }
41
42    let prefixed_path = logs_dir.join(format!("{prefix}{name}.log"));
43    if prefixed_path.exists() {
44        return Ok(prefixed_path);
45    }
46
47    let log_files = list_log_files(logs_dir, prefix)?;
48    log_files
49        .iter()
50        .find(|file| file.contains(name))
51        .map(|file| logs_dir.join(file))
52        .ok_or_else(|| {
53            anyhow!(
54                "Log file not found for '{}'. Available: {:?}",
55                name,
56                log_files
57            )
58        })
59}
60
61pub fn read_log_lines(
62    log_file: &Path,
63    lines: usize,
64    filter: impl Fn(&str) -> bool,
65) -> Result<Vec<String>> {
66    use std::io::{BufRead, BufReader};
67
68    let file = std::fs::File::open(log_file)
69        .with_context(|| format!("Failed to open log file: {}", log_file.display()))?;
70
71    let reader = BufReader::new(file);
72    let filtered: Vec<String> = reader
73        .lines()
74        .collect::<Result<Vec<_>, _>>()
75        .context("Failed to read log lines")?
76        .into_iter()
77        .filter(|line| filter(line))
78        .collect();
79
80    let start = filtered.len().saturating_sub(lines);
81    Ok(filtered[start..].to_vec())
82}
83
84#[must_use]
85pub fn display_names(log_files: &[String], prefix: &str) -> Vec<String> {
86    log_files
87        .iter()
88        .map(|f| {
89            f.strip_prefix(prefix)
90                .unwrap_or(f)
91                .strip_suffix(".log")
92                .unwrap_or(f)
93                .to_owned()
94        })
95        .collect()
96}