diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..0aa63b0 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,186 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "colored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "nginx-log-fmt" +version = "0.1.0" +dependencies = [ + "colored", + "serde", + "serde_json", + "tap", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "2.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0b498c8 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "nginx-log-fmt" +version = "0.1.0" +edition = "2024" + +[dependencies] +colored = "3.0.0" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +tap = "1.0.1" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..27f2c24 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,102 @@ +use serde::Deserialize; +use std::io::{self, BufRead}; +use colored::*; +use tap::Tap; + +#[derive(Deserialize)] +struct LogEntry { + t: String, + m: String, + p: String, + s: u16, + u: String, + ip: String, + ic: String, + ua: String, + sz: Option, + rf: Option, +} + +fn main() { + let stdin = io::stdin(); + for line in stdin.lock().lines() { + if let Ok(line) = line { + if line.trim().is_empty() { + continue; + } + match serde_json::from_str::(&line) { + Ok(entry) => { + print_entry(entry); + } + Err(e) => { + eprintln!("Failed to parse line: {}\nError: {}", line, e); + } + } + } + } +} + +fn exactly_n(s: &str, n: usize) -> String { + if s.chars().count() > n { + s.chars().take(n).collect() + } else { + s.to_string().tap_mut(|x| x.push_str(&" ".repeat(n - s.chars().count()))) + } +} + +fn fmt_size(size: u64) -> String { + let size = size as f64; + let units = [" ", "K", "M", "G", "T"]; + let mut i = 0; + let mut size = size; + + while size >= 1024.0 && i < units.len() - 1 { + size /= 1024.0; + i += 1; + } + + format!("{:3.0}{}", size, units[i]) +} + +fn print_entry(log: LogEntry) { + let timestamp = &log.t[5..19].replace('T', " "); + let proto = log.p.splitn(2, '/').nth(1).unwrap_or("").to_string(); + let ip = if !log.ic.is_empty() { log.ic } else { log.ip }; + + // Parse URL for domain and path + let prefix = format!("{}://", log.u.splitn(2, "://").next().unwrap_or("")); + let tmp_url = log.u.splitn(2, "://").nth(1).unwrap_or(""); + let (domain, path) = match tmp_url.find('/') { + Some(pos) => (&tmp_url[..pos], &tmp_url[pos..]), + None => (tmp_url, "/"), + }; + + // Status color + let status = if (200..400).contains(&log.s) { + log.s.to_string().blue() + } else { + log.s.to_string().red() + }; + + // Print + print!( + "{} {} {} {} {} {} {}{}{} {}", + timestamp.dimmed(), + exactly_n(&log.m, 4).yellow(), + proto.yellow(), + status, + fmt_size(log.sz.unwrap_or(0)).dimmed(), + exactly_n(&ip, 15).yellow(), + prefix.dimmed(), + domain.blue(), + path.dimmed(), + log.ua.truecolor(255, 165, 0), + ); + + // Print referrer if present + if let Some(refer) = log.rf.filter(|r| !r.is_empty()) { + print!(" {} {}", "from".dimmed(), refer.truecolor(255, 165, 0)); + } + + println!(); +}