torrust_tracker/console/ci/e2e/
logs_parser.rs

1//! Utilities to parse Torrust Tracker logs.
2use regex::Regex;
3use serde::{Deserialize, Serialize};
4
5use crate::servers::health_check_api::HEALTH_CHECK_API_LOG_TARGET;
6use crate::servers::http::HTTP_TRACKER_LOG_TARGET;
7use crate::servers::logging::STARTED_ON;
8use crate::servers::udp::UDP_TRACKER_LOG_TARGET;
9
10const INFO_THRESHOLD: &str = "INFO";
11
12#[derive(Serialize, Deserialize, Debug, Default)]
13pub struct RunningServices {
14    pub udp_trackers: Vec<String>,
15    pub http_trackers: Vec<String>,
16    pub health_checks: Vec<String>,
17}
18
19impl RunningServices {
20    /// It parses the tracker logs to extract the running services.
21    ///
22    /// For example, from this logs:
23    ///
24    /// ```text
25    /// Loading configuration from default configuration file: `./share/default/config/tracker.development.sqlite3.toml` ...
26    /// 2024-06-10T16:07:39.989540Z  INFO torrust_tracker::bootstrap::logging: Logging initialized
27    /// 2024-06-10T16:07:39.990205Z  INFO UDP TRACKER: Starting on: udp://0.0.0.0:6868
28    /// 2024-06-10T16:07:39.990215Z  INFO UDP TRACKER: Started on: udp://0.0.0.0:6868
29    /// 2024-06-10T16:07:39.990244Z  INFO UDP TRACKER: Starting on: udp://0.0.0.0:6969
30    /// 2024-06-10T16:07:39.990255Z  INFO UDP TRACKER: Started on: udp://0.0.0.0:6969
31    /// 2024-06-10T16:07:39.990261Z  INFO torrust_tracker::bootstrap::jobs: TLS not enabled
32    /// 2024-06-10T16:07:39.990303Z  INFO HTTP TRACKER: Starting on: http://0.0.0.0:7070
33    /// 2024-06-10T16:07:39.990439Z  INFO HTTP TRACKER: Started on: http://0.0.0.0:7070
34    /// 2024-06-10T16:07:39.990448Z  INFO torrust_tracker::bootstrap::jobs: TLS not enabled
35    /// 2024-06-10T16:07:39.990563Z  INFO API: Starting on http://127.0.0.1:1212
36    /// 2024-06-10T16:07:39.990565Z  INFO API: Started on http://127.0.0.1:1212
37    /// 2024-06-10T16:07:39.990577Z  INFO HEALTH CHECK API: Starting on: http://127.0.0.1:1313
38    /// 2024-06-10T16:07:39.990638Z  INFO HEALTH CHECK API: Started on: http://127.0.0.1:1313
39    /// ```
40    ///
41    /// It would extract these services:
42    ///
43    /// ```json
44    /// {
45    ///   "udp_trackers": [
46    ///     "127.0.0.1:6969"
47    ///    ],
48    ///    "http_trackers": [
49    ///      "http://127.0.0.1:7070"
50    ///    ],
51    ///    "health_checks": [
52    ///      "http://127.0.0.1:1313/health_check"
53    ///    ]
54    /// }
55    /// ```
56    ///
57    /// NOTICE: Using colors in the console output could affect this method
58    /// due to the hidden control chars.
59    ///
60    /// # Panics
61    ///
62    /// Will panic is the regular expression to parse the services can't be compiled.
63    #[must_use]
64    pub fn parse_from_logs(logs: &str) -> Self {
65        let mut udp_trackers: Vec<String> = Vec::new();
66        let mut http_trackers: Vec<String> = Vec::new();
67        let mut health_checks: Vec<String> = Vec::new();
68
69        let udp_re = Regex::new(&format!("{STARTED_ON}: {}", r"udp://([0-9.]+:[0-9]+)")).unwrap();
70        let http_re = Regex::new(&format!("{STARTED_ON}: {}", r"(https?://[0-9.]+:[0-9]+)")).unwrap(); // DevSkim: ignore DS137138
71        let health_re = Regex::new(&format!("{STARTED_ON}: {}", r"(https?://[0-9.]+:[0-9]+)")).unwrap(); // DevSkim: ignore DS137138
72        let ansi_escape_re = Regex::new(r"\x1b\[[0-9;]*m").unwrap();
73
74        for line in logs.lines() {
75            let clean_line = ansi_escape_re.replace_all(line, "");
76
77            if !line.contains(INFO_THRESHOLD) {
78                continue;
79            };
80
81            if line.contains(UDP_TRACKER_LOG_TARGET) {
82                if let Some(captures) = udp_re.captures(&clean_line) {
83                    let address = Self::replace_wildcard_ip_with_localhost(&captures[1]);
84                    udp_trackers.push(address);
85                }
86            } else if line.contains(HTTP_TRACKER_LOG_TARGET) {
87                if let Some(captures) = http_re.captures(&clean_line) {
88                    let address = Self::replace_wildcard_ip_with_localhost(&captures[1]);
89                    http_trackers.push(address);
90                }
91            } else if line.contains(HEALTH_CHECK_API_LOG_TARGET) {
92                if let Some(captures) = health_re.captures(&clean_line) {
93                    let address = format!("{}/health_check", Self::replace_wildcard_ip_with_localhost(&captures[1]));
94                    health_checks.push(address);
95                }
96            }
97        }
98
99        Self {
100            udp_trackers,
101            http_trackers,
102            health_checks,
103        }
104    }
105
106    fn replace_wildcard_ip_with_localhost(address: &str) -> String {
107        address.replace("0.0.0.0", "127.0.0.1")
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn it_should_parse_from_logs_with_valid_logs() {
117        let logs = r"
118            Loading configuration from default configuration file: `./share/default/config/tracker.development.sqlite3.toml` ...
119            2024-06-10T16:07:39.989540Z  INFO torrust_tracker::bootstrap::logging: Logging initialized
120            2024-06-10T16:07:39.990244Z  INFO UDP TRACKER: Starting on: udp://0.0.0.0:6969
121            2024-06-10T16:07:39.990255Z  INFO UDP TRACKER: Started on: udp://0.0.0.0:6969
122            2024-06-10T16:07:39.990261Z  INFO torrust_tracker::bootstrap::jobs: TLS not enabled
123            2024-06-10T16:07:39.990303Z  INFO HTTP TRACKER: Starting on: http://0.0.0.0:7070
124            2024-06-10T16:07:39.990439Z  INFO HTTP TRACKER: Started on: http://0.0.0.0:7070
125            2024-06-10T16:07:39.990448Z  INFO torrust_tracker::bootstrap::jobs: TLS not enabled
126            2024-06-10T16:07:39.990563Z  INFO API: Starting on http://127.0.0.1:1212
127            2024-06-10T16:07:39.990565Z  INFO API: Started on http://127.0.0.1:1212
128            2024-06-10T16:07:39.990577Z  INFO HEALTH CHECK API: Starting on: http://127.0.0.1:1313
129            2024-06-10T16:07:39.990638Z  INFO HEALTH CHECK API: Started on: http://127.0.0.1:1313
130            ";
131
132        let running_services = RunningServices::parse_from_logs(logs);
133
134        assert_eq!(running_services.udp_trackers, vec!["127.0.0.1:6969"]);
135        assert_eq!(running_services.http_trackers, vec!["http://127.0.0.1:7070"]);
136        assert_eq!(running_services.health_checks, vec!["http://127.0.0.1:1313/health_check"]);
137    }
138
139    #[test]
140    fn it_should_support_colored_output() {
141        let logs = "\x1b[2m2024-06-14T14:40:13.028824Z\x1b[0m  \x1b[33mINFO\x1b[0m \x1b[2mUDP TRACKER\x1b[0m: \x1b[37mStarted on: udp://0.0.0.0:6969\x1b[0m";
142
143        let running_services = RunningServices::parse_from_logs(logs);
144
145        assert_eq!(running_services.udp_trackers, vec!["127.0.0.1:6969"]);
146    }
147
148    #[test]
149    fn it_should_ignore_logs_with_no_matching_lines() {
150        let logs = "[Other Service][INFO] Started on: 0.0.0.0:7070";
151
152        let running_services = RunningServices::parse_from_logs(logs);
153
154        assert!(running_services.udp_trackers.is_empty());
155        assert!(running_services.http_trackers.is_empty());
156        assert!(running_services.health_checks.is_empty());
157    }
158
159    #[test]
160    fn it_should_parse_multiple_services() {
161        let logs = "
162            2024-06-10T16:07:39.990205Z  INFO UDP TRACKER: Starting on: udp://0.0.0.0:6868
163            2024-06-10T16:07:39.990215Z  INFO UDP TRACKER: Started on: udp://0.0.0.0:6868
164
165            2024-06-10T16:07:39.990244Z  INFO UDP TRACKER: Starting on: udp://0.0.0.0:6969
166            2024-06-10T16:07:39.990255Z  INFO UDP TRACKER: Started on: udp://0.0.0.0:6969
167        ";
168
169        let running_services = RunningServices::parse_from_logs(logs);
170
171        assert_eq!(running_services.udp_trackers, vec!["127.0.0.1:6868", "127.0.0.1:6969"]);
172    }
173
174    #[test]
175    fn it_should_replace_wildcard_ip_with_localhost() {
176        let address = "0.0.0.0:8080";
177        assert_eq!(RunningServices::replace_wildcard_ip_with_localhost(address), "127.0.0.1:8080");
178    }
179}