【Rust】基于Rust 设计开发nginx运行日志高效分析工具

文章目录


一、项目介绍

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/),了解更多资讯~

相关推荐
独行soc3 小时前
2025年渗透测试面试题总结-250(题目+回答)
网络·驱动开发·python·安全·web安全·渗透测试·安全狮
csdn_wuwt3 小时前
前后端中Dto是什么意思?
开发语言·网络·后端·安全·前端框架·开发
四问四不知3 小时前
Rust语言入门
开发语言·rust
JosieBook3 小时前
【Rust】 基于Rust 从零构建一个本地 RSS 阅读器
开发语言·后端·rust
云边有个稻草人3 小时前
部分移动(Partial Move)的使用场景:Rust 所有权拆分的精细化实践
开发语言·算法·rust
电话交换机IPPBX-3CX4 小时前
电话交换机IPPBX-3CX的呼叫记录导出
运维·服务器·网络
C-DHEnry6 小时前
Linux 不小心挂载错磁盘导致无法启动系统咋办
linux·运维·服务器·雨云
JosieBook6 小时前
【若依框架】若依前后端分离项目怎么部署到服务器?
运维·服务器
f***68607 小时前
【Sql Server】sql server 2019设置远程访问,外网服务器需要设置好安全组入方向规则
运维·服务器·安全