文章目录
-
- 一、项目介绍
-
- [1. 核心功能](#1. 核心功能)
- [2. 技术栈](#2. 技术栈)
- 二、项目结构
- 三、完整代码实现
-
- [1. Cargo.toml(依赖配置)](#1. Cargo.toml(依赖配置))
- [2. src/log_core.rs(核心逻辑整合)](#2. src/log_core.rs(核心逻辑整合))
- [3. src/main.rs(程序入口・完整)](#3. src/main.rs(程序入口・完整))
- [4. src/lib.rs(导出)](#4. src/lib.rs(导出))
- 四、测试与使用指南
-
- [1. 准备测试日志](#1. 准备测试日志)
- [2. 运行命令示例](#2. 运行命令示例)
-
- (1)分析单个日志文件(文本报表输出)
- [(2)分析目录下所有日志(JSON 输出)](#(2)分析目录下所有日志(JSON 输出))
- 五、项目开发总结
一、项目介绍
1. 核心功能
- 支持 Nginx 标准日志格式解析(最常用场景);
- 统计关键指标:总请求数、独立 IP、TOP10 路径 / IP、4xx/5xx 错误;
- 输出格式:终端文本报表(直观查看)、JSON 格式(便于后续处理);
- 支持单个日志文件或目录下所有
.log文件批量分析。
2. 技术栈
clap:命令行参数解析(简化指令处理);regex:日志行正则匹配(核心解析逻辑);chrono:时间提取与格式化(统计时间范围);serde + serde_json:JSON 输出(结构化数据);- 标准库
std::fs/std::io:文件读写(避免额外依赖)。
二、项目结构
bash
simple-log-analyzer/
├── Cargo.toml # 依赖配置
├── src/
│ ├── main.rs # 程序入口(命令解析、主流程调度)
│ └── log_core.rs # 核心逻辑(解析、分析、导出整合)
| └── lib.rs
└── test_logs/ # 测试日志(Nginx 示例日志)
└── nginx_access.log

三、完整代码实现
1. Cargo.toml(依赖配置)
toml
[package]
name = "simple-log-analyzer"
version = "0.1.0"
edition = "2021"
[dependencies]
chrono = { version = "0.4", features = ["serde"] }
regex = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
clap = { version = "4.0", features = ["derive"] }
2. src/log_core.rs(核心逻辑整合)
rust
//! 整合日志解析、分析、导出功能(单文件简化版·终极修复)
use chrono::{DateTime, FixedOffset, NaiveDateTime, Utc};
use regex::Regex;
use serde::Serialize;
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::{BufRead, BufReader};
use std::path::Path;
// --------------------------
// 1. 数据结构定义
// --------------------------
/// 日志解析/分析错误类型
#[derive(Debug)]
pub enum LogError {
FileError(String),
ParseError(String),
RegexError(String),
DateTimeError(String),
InvalidArg(String),
}
impl std::fmt::Display for LogError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LogError::FileError(msg) => write!(f, "文件错误: {}", msg),
LogError::ParseError(msg) => write!(f, "解析错误: {}", msg),
LogError::RegexError(msg) => write!(f, "正则错误: {}", msg),
LogError::DateTimeError(msg) => write!(f, "时间错误: {}", msg),
LogError::InvalidArg(msg) => write!(f, "参数错误: {}", msg),
}
}
}
impl std::error::Error for LogError {}
pub type LogResult<T> = Result<T, LogError>;
/// 单条日志解析结果
#[derive(Debug, Serialize)]
pub struct LogEntry {
remote_ip: String,
datetime: DateTime<Utc>,
method: String,
path: String,
status: u16,
body_bytes: u64,
}
/// 整体统计结果
#[derive(Debug, Serialize, Default)]
pub struct LogStats {
total_requests: usize,
unique_ips: usize,
ip_count: HashMap<String, usize>,
path_count: HashMap<String, usize>,
status_count: HashMap<u16, usize>,
error_4xx: Vec<String>,
error_5xx: Vec<String>,
start_time: Option<DateTime<Utc>>,
end_time: Option<DateTime<Utc>>,
parse_failed: usize,
}
/// 分析配置
pub struct AnalyzeConfig {
pub input_path: String,
pub output_format: String,
}
// --------------------------
// 2. 日志解析函数
// --------------------------
/// 解析 Nginx 日志时间
fn parse_nginx_datetime(datetime_str: &str) -> LogResult<DateTime<Utc>> {
// 示例: 10/Oct/2024:13:55:36 +0800
DateTime::parse_from_str(datetime_str, "%d/%b/%Y:%H:%M:%S %z")
.map(|dt: DateTime<FixedOffset>| dt.with_timezone(&Utc))
.map_err(|e| LogError::DateTimeError(format!("时间解析失败:{}(输入:{})", e, datetime_str)))
}
/// 创建 Nginx 日志解析正则
fn create_nginx_parser() -> LogResult<Regex> {
Regex::new(r#"^(\S+) \S+ \S+ \[([^\]]+)\] "(\S+) (\S+) \S+" (\d+) (\d+)"#)
.map_err(|e| LogError::RegexError(format!("正则编译失败:{}", e)))
}
/// 解析单条日志行
fn parse_nginx_line(parser: &Regex, line: &str) -> LogResult<LogEntry> {
let line_trimmed = line.trim();
if line_trimmed.is_empty() {
return Err(LogError::ParseError("空行跳过".to_string()));
}
let caps = parser
.captures(line_trimmed)
.ok_or_else(|| LogError::ParseError(format!("日志格式不匹配(行:{})", line_trimmed)))?;
let status = caps[5]
.parse::<u16>()
.map_err(|_| LogError::ParseError(format!("状态码无效(行:{})", line_trimmed)))?;
let body_bytes = caps[6]
.parse::<u64>()
.map_err(|_| LogError::ParseError(format!("响应大小无效(行:{})", line_trimmed)))?;
Ok(LogEntry {
remote_ip: caps[1].to_string(),
datetime: parse_nginx_datetime(&caps[2])?,
method: caps[3].to_string(),
path: caps[4].to_string(),
status,
body_bytes,
})
}
/// 批量读取日志文件
fn read_log_files(input_path: &str) -> LogResult<Box<dyn Iterator<Item = LogResult<String>>>> {
let path = Path::new(input_path);
if path.is_file() {
let file = File::open(path)
.map_err(|e| LogError::FileError(format!("打开文件失败:{}(路径:{})", e, input_path)))?;
let reader = BufReader::new(file);
let lines = reader
.lines()
.map(|line| line.map_err(|e| LogError::FileError(format!("读取行失败:{}", e))));
Ok(Box::new(lines))
} else if path.is_dir() {
let entries = fs::read_dir(path)
.map_err(|e| LogError::FileError(format!("打开目录失败:{}(路径:{})", e, input_path)))?;
let flat_lines = entries.flat_map(move |entry| match entry {
Ok(entry) => {
let file_path = entry.path();
if file_path.is_file()
&& file_path.extension().and_then(|e| e.to_str()) == Some("log")
{
match File::open(&file_path) {
Ok(file) => {
let reader = BufReader::new(file);
Box::new(reader.lines().map(move |line| {
line.map_err(|e| {
LogError::FileError(format!(
"读取文件 {} 行失败:{}",
file_path.display(),
e
))
})
})) as Box<dyn Iterator<Item = LogResult<String>>>
}
Err(e) => Box::new(std::iter::once(Err(LogError::FileError(format!(
"打开文件 {} 失败:{}",
file_path.display(),
e
))))) as Box<dyn Iterator<Item = LogResult<String>>>,
}
} else {
Box::new(std::iter::empty()) as Box<dyn Iterator<Item = LogResult<String>>>
}
}
Err(e) => Box::new(std::iter::once(Err(LogError::FileError(format!(
"遍历目录失败:{}",
e
))))) as Box<dyn Iterator<Item = LogResult<String>>>,
});
Ok(Box::new(flat_lines))
} else {
Err(LogError::InvalidArg(format!(
"输入路径无效:{}(需为文件或目录)",
input_path
)))
}
}
// --------------------------
// 3. 日志分析逻辑
// --------------------------
fn analyze_entry(entry: &LogEntry, stats: &mut LogStats) {
stats.total_requests += 1;
*stats.ip_count.entry(entry.remote_ip.clone()).or_insert(0) += 1;
*stats.path_count.entry(entry.path.clone()).or_insert(0) += 1;
*stats.status_count.entry(entry.status).or_insert(0) += 1;
let path = entry.path.clone();
match entry.status / 100 {
4 => if !stats.error_4xx.contains(&path) {
stats.error_4xx.push(path);
},
5 => if !stats.error_5xx.contains(&path) {
stats.error_5xx.push(path);
},
_ => (),
}
let dt = entry.datetime;
if stats.start_time.as_ref().map_or(true, |start| dt < *start) {
stats.start_time = Some(dt);
}
if stats.end_time.as_ref().map_or(true, |end| dt > *end) {
stats.end_time = Some(dt);
}
stats.unique_ips = stats.ip_count.keys().count();
}
// --------------------------
// 4. 结果导出逻辑
// --------------------------
fn export_text(stats: &LogStats) -> LogResult<()> {
println!("==================== Nginx 日志分析报告 ====================");
println!("总请求数:{} 次", stats.total_requests);
println!("独立 IP 数:{} 个", stats.unique_ips);
println!("解析失败:{} 行", stats.parse_failed);
if let (Some(start), Some(end)) = (&stats.start_time, &stats.end_time) {
println!(
"时间范围:{} ~ {}",
start.format("%Y-%m-%d %H:%M:%S"),
end.format("%Y-%m-%d %H:%M:%S")
);
}
println!("\n【TOP 10 热门路径】");
let mut paths: Vec<(&String, &usize)> = stats.path_count.iter().collect();
paths.sort_by(|a, b| b.1.cmp(a.1));
for (i, (path, count)) in paths.iter().take(10).enumerate() {
let ratio = (**count as f64 / stats.total_requests as f64) * 100.0;
println!(" {}. {}:{} 次({:.1}%)", i + 1, path, count, ratio);
}
println!("\n【TOP 10 访问 IP】");
let mut ips: Vec<(&String, &usize)> = stats.ip_count.iter().collect();
ips.sort_by(|a, b| b.1.cmp(a.1));
for (i, (ip, count)) in ips.iter().take(10).enumerate() {
let ratio = (**count as f64 / stats.total_requests as f64) * 100.0;
println!(" {}. {}:{} 次({:.1}%)", i + 1, ip, count, ratio);
}
println!("\n【状态码分布】");
let mut statuses: Vec<(&u16, &usize)> = stats.status_count.iter().collect();
statuses.sort_by(|a, b| b.1.cmp(a.1));
for (status, count) in statuses {
let ratio = (*count as f64 / stats.total_requests as f64) * 100.0;
println!(" {}:{} 次({:.1}%)", status, count, ratio);
}
if !stats.error_4xx.is_empty() {
println!("\n【4xx 错误路径(共 {} 个)】", stats.error_4xx.len());
let display_paths = stats.error_4xx.iter().take(5).cloned().collect::<Vec<_>>();
println!(" {}", display_paths.join(", "));
if stats.error_4xx.len() > 5 {
println!(" ... 还有 {} 个路径未显示", stats.error_4xx.len() - 5);
}
}
if !stats.error_5xx.is_empty() {
println!("\n【5xx 错误路径(共 {} 个)】", stats.error_5xx.len());
let display_paths = stats.error_5xx.iter().take(5).cloned().collect::<Vec<_>>();
println!(" {}", display_paths.join(", "));
if stats.error_5xx.len() > 5 {
println!(" ... 还有 {} 个路径未显示", stats.error_5xx.len() - 5);
}
}
println!("==============================================================");
Ok(())
}
fn export_json(stats: &LogStats) -> LogResult<()> {
let json_str =
serde_json::to_string_pretty(stats).map_err(|e| LogError::FileError(format!("JSON 序列化失败:{}", e)))?;
println!("{}", json_str);
Ok(())
}
// --------------------------
// 5. 主分析入口
// --------------------------
pub fn run_analysis(config: AnalyzeConfig) -> LogResult<()> {
let parser = create_nginx_parser()?;
let mut stats = LogStats::default();
let log_lines = read_log_files(&config.input_path)?;
for line_result in log_lines {
let line = match line_result {
Ok(l) => l,
Err(e) => {
eprintln!("⚠️ 读取行警告:{}", e);
stats.parse_failed += 1;
continue;
}
};
match parse_nginx_line(&parser, &line) {
Ok(entry) => analyze_entry(&entry, &mut stats),
Err(e) => {
eprintln!("⚠️ 解析警告:{}", e);
stats.parse_failed += 1;
}
}
}
match config.output_format.as_str() {
"text" => export_text(&stats)?,
"json" => export_json(&stats)?,
other => {
return Err(LogError::InvalidArg(format!(
"输出格式无效:{}(仅支持 text/json)",
other
)))
}
}
Ok(())
}

3. src/main.rs(程序入口・完整)
rust
use clap::Parser;
use simple_log_analyzer::{AnalyzeConfig, run_analysis};
/// 🧾 Nginx 日志分析工具
#[derive(Parser, Debug)]
#[command(author, version, about = "轻量级 Nginx 日志分析工具", long_about = None)]
struct Cli {
/// 输入日志文件或目录路径
#[arg(short, long, required = true, help = "示例:test_logs/nginx_access.log 或 test_logs")]
input: String,
/// 输出格式,可选值:text(默认)、json
#[arg(short, long, default_value = "text")]
format: String,
}
fn main() {
let cli = Cli::parse();
let config = AnalyzeConfig {
input_path: cli.input,
output_format: cli.format,
};
if let Err(e) = run_analysis(config) {
eprintln!("❌ 分析失败:{}", e);
std::process::exit(1);
}
}

4. src/lib.rs(导出)
rust
pub mod log_core;
// 重新导出以便外部使用
pub use log_core::{AnalyzeConfig, run_analysis, LogError, LogResult, LogEntry, LogStats};
四、测试与使用指南
1. 准备测试日志
在项目根目录创建 test_logs 文件夹,并新建 nginx_access.log 文件,填入 Nginx 标准格式日志(示例如下):
192.168.1.100 - - [01/Nov/2024:08:30:45 +0800] "GET /index.html HTTP/1.1" 200 1024
192.168.1.101 - - [01/Nov/2024:08:35:12 +0800] "POST /api/login HTTP/1.1" 200 512
192.168.1.100 - - [01/Nov/2024:08:40:00 +0800] "GET /static/css/main.css HTTP/1.1" 200 2048
192.168.1.102 - - [01/Nov/2024:09:00:15 +0800] "GET /page/not/found HTTP/1.1" 404 256
192.168.1.100 - - [01/Nov/2024:09:10:30 +0800] "GET /api/data HTTP/1.1" 500 128
192.168.1.103 - - [01/Nov/2024:09:15:45 +0800] "GET /index.html HTTP/1.1" 200 1024
192.168.1.101 - - [01/Nov/2024:09:20:00 +0800] "GET /static/js/app.js HTTP/1.1" 200 3072
192.168.1.102 - - [01/Nov/2024:09:25:12 +0800] "POST /api/submit HTTP/1.1" 200 64
192.168.1.100 - - [01/Nov/2024:09:30:45 +0800] "GET /index.html HTTP/1.1" 200 1024
192.168.1.104 - - [01/Nov/2024:09:35:15 +0800] "GET /static/img/logo.png HTTP/1.1" 200 4096
192.168.1.101 - - [01/Nov/2024:09:40:00 +0800] "GET /api/user HTTP/1.1" 401 160
192.168.1.103 - - [01/Nov/2024:09:45:30 +0800] "GET /index.html HTTP/1.1" 200 1024
2. 运行命令示例
(1)分析单个日志文件(文本报表输出)
bash
# 终端执行(项目根目录)
cargo run -- --input test_logs/nginx_access.log --format text
预期输出(文本报表):

(2)分析目录下所有日志(JSON 输出)
bash
# 分析 test_logs 目录下所有 .log 文件,输出 JSON
cargo run -- --input test_logs --format json
预期输出(JSON 片段):
json
{
"total_requests": 12,
"unique_ips": 5,
"ip_count": {
"192.168.1.103": 2,
"192.168.1.102": 2,
"192.168.1.100": 4,
"192.168.1.101": 3,
"192.168.1.104": 1
},
"path_count": {
"/static/js/app.js": 1,
"/index.html": 4,
"/api/submit": 1,
"/page/not/found": 1,
"/api/data": 1,
"/static/img/logo.png": 1,
"/static/css/main.css": 1,
"/api/user": 1,
"/api/login": 1
},
"status_count": {
"404": 1,
"500": 1,
"401": 1,
"200": 9
},
"error_4xx": [
"/page/not/found",
"/api/user"
],
"error_5xx": [
"/api/data"
],
"start_time": "2024-11-01T00:30:45Z",
"end_time": "2024-11-01T01:45:30Z",
"parse_failed": 0
}
五、项目开发总结
该简易服务器访问日志分析工具是一款轻量实用的 Rust 命令行工具,专注 Nginx 标准日志解析,无需复杂模块拆分,核心整合日志读取、解析、多维度统计与结果导出功能,支持单文件或目录下批量日志分析,可快速输出总请求数、独立 IP 数、TOP10 热门路径 / 访问 IP、4xx/5xx 错误路径及状态码分布等关键指标,提供终端友好的文本报表与结构化 JSON 两种输出格式,兼顾易用性与灵活性,能满足开发者快速排查日志异常、分析访问趋势的日常需求。
想了解更多关于Rust语言的知识及应用,可前往华为开放原子旋武开源社区(https://xuanwu.openatom.cn/),了解更多资讯~