endpoint_libs/libs/
log_reader.rs

1use core::str::FromStr;
2use eyre::ContextCompat;
3use lazy_static::lazy_static;
4use regex::Regex;
5use rev_lines::RevLines;
6use tracing::warn;
7
8// Define a struct to hold the parts of the log entry
9
10#[derive(Debug, Clone)]
11pub struct LogEntry {
12    pub datetime: i64,
13    pub level: String,
14    pub thread: String,
15    pub path: String,
16    pub line_number: usize,
17    pub message: String,
18}
19
20lazy_static! {
21    static ref CONTROL_SEQUENCE_PATTERN: Regex = Regex::new("\x1b\\[[0-9;]*m").unwrap();
22}
23
24impl FromStr for LogEntry {
25    type Err = eyre::Error;
26    fn from_str(s: &str) -> Result<Self, Self::Err> {
27        let no_control_sequence = CONTROL_SEQUENCE_PATTERN.replace_all(s, "");
28        let mut split = no_control_sequence.split_whitespace();
29        let datetime = split
30            .next()
31            .and_then(|x| chrono::DateTime::parse_from_rfc3339(x).ok())
32            .context("no datetime")?;
33        let level: tracing::Level = split
34            .next()
35            .and_then(|x| x.parse().ok())
36            .context("no level")?;
37        let thread = split.next().context("no thread")?;
38        let path = split
39            .next()
40            .map(|x| x[..x.len() - 1].to_string())
41            .context("no path")?;
42
43        let line_number = split
44            .next()
45            .and_then(|x| x[..x.len() - 1].parse().ok())
46            .unwrap_or(0);
47        let message = split.collect::<Vec<&str>>().join(" ");
48        Ok(LogEntry {
49            datetime: datetime.timestamp_millis(),
50            level: level.to_string(),
51            thread: thread.to_string(),
52            path,
53            line_number,
54            message,
55        })
56    }
57}
58
59pub async fn get_log_entries(
60    path: impl AsRef<std::path::Path>,
61    limit: usize,
62) -> eyre::Result<Vec<LogEntry>> {
63    // Specify the path to your log file
64    let file = std::fs::File::open(path.as_ref())?;
65    let lines = RevLines::new(file);
66    // get all entries first
67    let mut entries = tokio::task::spawn_blocking(move || {
68        let mut entries = vec![];
69        for line in lines {
70            if entries.len() >= limit {
71                break;
72            }
73            let line = match line {
74                Ok(line) => line,
75                Err(error) => {
76                    warn!("Error reading line: {:?}", error);
77                    entries.push(LogEntry {
78                        datetime: 0,
79                        level: "".to_string(),
80                        thread: "".to_string(),
81                        path: "".to_string(),
82                        line_number: 0,
83                        message: error.to_string(),
84                    });
85                    break;
86                }
87            };
88            let entry = LogEntry::from_str(&line);
89            match entry {
90                Ok(entry) => entries.push(entry),
91                Err(_) => entries.push(LogEntry {
92                    datetime: 0,
93                    level: "".to_string(),
94                    thread: "".to_string(),
95                    path: "".to_string(),
96                    line_number: 0,
97                    message: line,
98                }),
99            }
100        }
101        entries
102    })
103    .await?;
104    if entries.is_empty() {
105        entries.push(LogEntry {
106            datetime: 0,
107            level: "".to_string(),
108            thread: "".to_string(),
109            path: "".to_string(),
110            line_number: 0,
111            message: format!("No entries found in {}", path.as_ref().display()),
112        });
113    }
114    Ok(entries)
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    #[test]
121    fn log_entry_from_line() {
122        let line = "2024-05-18T14:26:36.709390Z  WARN                 main trading_be: 290: terminated diff 0_table_limit_1 initially";
123        let entry = LogEntry::from_str(line).unwrap();
124        // assert_eq!(entry.datetime, "2024-02-09 18:10:46");
125        assert_eq!(entry.level, "WARN");
126        assert_eq!(entry.thread, "main");
127        assert_eq!(entry.path, "trading_be");
128        assert_eq!(entry.line_number, 290);
129        assert_eq!(entry.message, "terminated diff 0_table_limit_1 initially");
130    }
131    #[test]
132    fn log_entry_from_line_control_sequence() {
133        let line = "\u{1b}[2m2024-06-07T12:25:06.735143Z\u{1b}[0m \u{1b}[32m INFO\u{1b}[0m main \u{1b}[2mtrading_be::strategy::data_factory\u{1b}[0m\u{1b}[2m:\u{1b}[0m \u{1b}[2m110:\u{1b}[0m terminated diff 0_table_limit_1 initially";
134        let entry = LogEntry::from_str(line).unwrap();
135        // assert_eq!(entry.datetime, "2024-02-09 18:10:46");
136        assert_eq!(entry.level, "INFO");
137        assert_eq!(entry.thread, "main");
138        assert_eq!(entry.path, "trading_be::strategy::data_factory");
139        assert_eq!(entry.line_number, 110);
140        assert_eq!(entry.message, "terminated diff 0_table_limit_1 initially");
141    }
142
143    #[tokio::test]
144    async fn test_get_log_entries() {
145        use std::io::Write;
146        use tempfile::NamedTempFile;
147
148        let mut temp_file = NamedTempFile::new().unwrap();
149        writeln!(
150            temp_file,
151            "2025-03-19T12:34:56Z INFO thread-1 src/main.rs:42 Application started"
152        )
153        .unwrap();
154        writeln!(
155            temp_file,
156            "2025-03-19T12:35:01Z ERROR thread-2 src/lib.rs:128 Failed to connect"
157        )
158        .unwrap();
159
160        let entries = get_log_entries(temp_file.path(), 10).await.unwrap();
161        assert_eq!(entries.len(), 2);
162        assert_eq!(entries[0].level, "ERROR");
163        assert_eq!(entries[1].level, "INFO");
164    }
165}