Skip to main content

rust_web_server/log/
mod.rs

1use std::net::SocketAddr;
2use std::thread;
3use file_ext::FileExt;
4use crate::entry_point::command_line_args::CommandLineArgument;
5use crate::request::Request;
6use crate::response::Response;
7
8#[cfg(test)]
9mod tests;
10
11/// Logging helpers for access logs and server info.
12///
13/// [`Log::combined`] produces standard Combined Log Format lines compatible with
14/// GoAccess, AWStats, and similar tools. All three server code paths (HTTP/1.1,
15/// HTTP/2, HTTP/3) use it.
16pub struct Log;
17
18impl Log {
19    pub fn request_response(request: &Request, response: &Response, peer_addr: &SocketAddr) -> String {
20        let mut request_headers = "".to_string();
21        for header in &request.headers {
22            if &header.name.chars().count() > &0 {
23                request_headers = [
24                    request_headers,
25                    "\n  ".to_string(),
26                    header.name.to_string(),
27                    ": ".to_string(),
28                    header.value.to_string()
29                ].join("");
30            }
31        }
32
33        let mut response_headers = "".to_string();
34        for header in &response.headers {
35            if &header.name.chars().count() > &0 {
36                response_headers = [
37                    response_headers,
38                    "\n  ".to_string(),
39                    header.name.to_string(),
40                    ": ".to_string(),
41                    header.value.to_string()
42                ].join("");
43            }
44        }
45
46        let mut response_body_length = 0;
47        let mut response_body_parts_number = 0;
48        for content_range in &response.content_range_list {
49            let boxed_parse = content_range.size.parse::<i32>();
50            if boxed_parse.is_ok() {
51                response_body_length += boxed_parse.unwrap();
52                response_body_parts_number += 1;
53            }
54        }
55
56        let current_thread = thread::current();
57        let thread_id = current_thread.name().unwrap();
58
59        let log_request_response = format!("\n\nRequest (thread id: {} peer address is {}):\n  {} {} {}  {}\n  Body: {} byte(s) total (including default initialization vector)\nEnd of Request\nResponse:\n  {} {} {}\n\n  Body: {} part(s), {} byte(s) total\nEnd of Response",
60                                           thread_id,
61                                           peer_addr,
62                                           &request.http_version,
63                                           &request.method,
64                                           &request.request_uri,
65                                           request_headers,
66                                           request.body.len(),
67
68                                           &response.status_code,
69                                           &response.reason_phrase,
70                                           response_headers,
71                                           response_body_parts_number,
72                                           response_body_length);
73
74        log_request_response
75    }
76
77    pub fn usage_information() -> String {
78        let mut log = "Usage:\n\n".to_string();
79        let command_line_arg_list = CommandLineArgument::get_command_line_arg_list();
80        for arg in command_line_arg_list {
81            let argument_info = format!("  {} environment variable\n  -{} or --{} as command line line argument\n  {}\n\n", arg.environment_variable, arg.short_form, arg.long_form, arg._hint.unwrap());
82            log = [log, argument_info].join("");
83        }
84        log = [log, "End of usage section\n\n".to_string()].join("");
85        log
86    }
87
88    pub fn info(name: &str) -> String {
89        let mut log = format!("{}\n", name).to_string();
90        const VERSION: &str = env!("CARGO_PKG_VERSION");
91        const AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
92        const REPOSITORY: &str = env!("CARGO_PKG_REPOSITORY");
93        const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION");
94        const RUST_VERSION: &str = env!("CARGO_PKG_RUST_VERSION");
95        const LICENSE: &str = env!("CARGO_PKG_LICENSE");
96        let boxed_user = FileExt::get_current_user();
97        if boxed_user.is_err() {
98            let message = boxed_user.as_ref().err().unwrap();
99            eprintln!("{}", message)
100        }
101        let user: String = boxed_user.unwrap();
102
103        let boxed_working_directory = FileExt::get_static_filepath("");
104        if boxed_working_directory.is_err() {
105            let message = boxed_working_directory.as_ref().err().unwrap();
106            eprintln!("{}", message)
107        }
108
109        let working_directory: String = boxed_working_directory.unwrap();
110
111        let version = format!("Version:           {}\n", VERSION);
112        log = [log, version].join("");
113
114        let authors = format!("Authors:           {}\n", AUTHORS);
115        log = [log, authors].join("");
116
117        let repository = format!("Repository:        {}\n", REPOSITORY);
118        log = [log, repository].join("");
119
120        let description = format!("Desciption:        {}\n", DESCRIPTION);
121        log = [log, description].join("");
122
123        let rust_version = format!("Rust Version:      {}\n", RUST_VERSION);
124        log = [log, rust_version].join("");
125
126        let license = format!("License:           {}\n", LICENSE);
127        log = [log, license].join("");
128
129        let license = format!("User:              {}\n", user);
130        log = [log, license].join("");
131
132        let license = format!("Working Directory: {}\n", working_directory);
133        log = [log, license].join("");
134
135        log
136    }
137
138    /// Returns a structured JSON log line for one request/response pair.
139    ///
140    /// Enable with `RWS_CONFIG_LOG_FORMAT=json`. Useful for log aggregators
141    /// (Loki, Fluentd, Datadog) that ingest JSON from pod stdout.
142    pub fn json(request: &Request, response: &Response, peer_addr: &SocketAddr) -> String {
143        use std::time::{SystemTime, UNIX_EPOCH};
144
145        let body_size: usize = response.content_range_list.iter()
146            .map(|cr| cr.body.len())
147            .sum();
148
149        let secs = SystemTime::now()
150            .duration_since(UNIX_EPOCH)
151            .unwrap_or_default()
152            .as_secs();
153
154        let timestamp = Log::format_iso8601(secs);
155
156        fn escape(s: &str) -> String {
157            s.replace('\\', "\\\\")
158             .replace('"', "\\\"")
159             .replace('\n', "\\n")
160             .replace('\r', "\\r")
161        }
162
163        format!(
164            "{{\"time\":\"{}\",\"remote_addr\":\"{}\",\"method\":\"{}\",\"path\":\"{}\",\"protocol\":\"{}\",\"status\":{},\"bytes\":{}}}",
165            timestamp,
166            peer_addr.ip(),
167            escape(&request.method),
168            escape(&request.request_uri),
169            escape(&request.http_version),
170            response.status_code,
171            body_size,
172        )
173    }
174
175    /// Writes one access log line to stdout using the format configured by
176    /// `RWS_CONFIG_LOG_FORMAT` (`"combined"` or `"json"`). Call this instead of
177    /// `Log::combined` so that format selection is centralised.
178    pub fn log_access(request: &Request, response: &Response, peer_addr: &SocketAddr) {
179        let format = std::env::var(crate::entry_point::Config::RWS_CONFIG_LOG_FORMAT)
180            .unwrap_or_default();
181        let line = if format == "json" {
182            Log::json(request, response, peer_addr)
183        } else {
184            Log::combined(request, response, peer_addr)
185        };
186        println!("{}", line);
187    }
188
189    fn format_iso8601(secs: u64) -> String {
190        let sec = secs % 60;
191        let min = (secs / 60) % 60;
192        let hour = (secs / 3600) % 24;
193        let days = secs / 86400;
194        let (year, month, day) = Log::days_to_ymd(days);
195        format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", year, month, day, hour, min, sec)
196    }
197
198    /// Returns a Combined Log Format (CLF) line for one request/response pair:
199    /// `IP - - [DD/Mon/YYYY:HH:MM:SS +0000] "METHOD URI VERSION" STATUS SIZE`
200    pub fn combined(request: &Request, response: &Response, peer_addr: &SocketAddr) -> String {
201        use std::time::{SystemTime, UNIX_EPOCH};
202
203        let body_size: usize = response.content_range_list.iter()
204            .map(|cr| cr.body.len())
205            .sum();
206
207        let secs = SystemTime::now()
208            .duration_since(UNIX_EPOCH)
209            .unwrap_or_default()
210            .as_secs();
211
212        let timestamp = Log::format_clf_timestamp(secs);
213
214        let body_str = if body_size > 0 { body_size.to_string() } else { "-".to_string() };
215
216        format!("{} - - [{}] \"{} {} {}\" {} {}",
217            peer_addr.ip(),
218            timestamp,
219            request.method,
220            request.request_uri,
221            request.http_version,
222            response.status_code,
223            body_str,
224        )
225    }
226
227    fn format_clf_timestamp(secs: u64) -> String {
228        let sec = secs % 60;
229        let min = (secs / 60) % 60;
230        let hour = (secs / 3600) % 24;
231        let days = secs / 86400;
232        let (year, month, day) = Log::days_to_ymd(days);
233        const MONTHS: [&str; 12] = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
234        format!("{:02}/{}/{:04}:{:02}:{:02}:{:02} +0000",
235            day, MONTHS[(month - 1) as usize], year, hour, min, sec)
236    }
237
238    fn days_to_ymd(days: u64) -> (u64, u64, u64) {
239        let z = days + 719468;
240        let era = z / 146097;
241        let doe = z % 146097;
242        let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
243        let y = yoe + era * 400;
244        let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
245        let mp = (5 * doy + 2) / 153;
246        let d = doy - (153 * mp + 2) / 5 + 1;
247        let m = if mp < 10 { mp + 3 } else { mp - 9 };
248        let y = if m <= 2 { y + 1 } else { y };
249        (y, m, d)
250    }
251
252    pub fn server_url_thread_count(protocol: &str, bind_addr: &String, thread_count: i32) -> String {
253        let url = format!("Server is up and running at: {}://{}\n", protocol, &bind_addr);
254        let thread_count = format!("Spawned {} thread(s) to handle incoming requests\n", thread_count);
255        [url, thread_count].join("")
256    }
257}