Rust AI日志清洗工具:非结构化日志自动转JSON

一、问题场景:日志清洗的痛点

做过后端开发的同学应该都有这样的经历:生产环境出问题,打开日志一看,几百兆的非结构化文本,格式五花八门——有 Nginx 的 access log,有应用自己 println! 输出的,还有各种第三方 SDK 的 debug 信息。想从中提取关键信息?只能人眼一行行看,再用 grepawksed 手工拼凑。

有没有一种方法,能把这些乱七八糟的日志自动、准确地转成结构化 JSON,方便后续做监控、告警、分析?

答案是:用 AI 模型做数据清洗

二、方案设计思路

2.1 核心架构

整个工具分为三层:

  • CLI 层:使用 clap 做命令行参数解析,支持文件输入、管道输入、输出格式选择。
  • 清洗层:核心模块,负责读取日志、分块、调用模型。
  • 输出层:将清洗结果序列化为 JSON,可选输出到文件或标准输出。

2.2 为什么用 Rust?

  1. 性能:处理几百 MB 甚至 GB 级日志时,Rust 的内存管理和零成本抽象保证不会 OOM。
  2. 并发:天然支持多线程分块处理,rayon 一行代码就能并行。
  3. 生态serde_jsonclapreqwest 这些库已经很成熟。

2.3 关键代码实现

先看 CLI 入口的结构定义:

use clap::Parser;

/// AI 驱动的日志清洗 CLI 工具
/// 将非结构化日志自动转为结构化 JSON
#[derive(Parser, Debug)]
#[command(name = "log-cleaner")]
#[command(version = "0.1.0")]
#[command(about = "用 AI 模型把日志转成结构化 JSON", long_about = None)]
struct Cli {
    /// 输入日志文件路径(不传则从标准输入读取)
    #[arg(short, long)]
    input: Option<String>,

    /// 输出 JSON 文件路径(不传则输出到标准输出)
    #[arg(short, long)]
    output: Option<String>,

    /// 本地模型 API 地址
    #[arg(short = 'm', long, default_value = "http://127.0.0.1:11434")]
    model_url: String,

    /// 模型名称
    #[arg(short = 'n', long, default_value = "qwen2.5:7b")]
    model_name: String,

    /// 每次送模型的最大行数
    #[arg(short = 'b', long, default_value_t = 50)]
    batch_size: usize,
}

下面是核心的清洗逻辑——把每批日志送给本地模型,让它返回结构化 JSON:

use serde_json::Value;
use reqwest::Client;

/// 用本地 AI 模型清洗一批日志行
async fn clean_batch(
client: &Client,
model_url: &str,
model_name: &str,
lines: &[String],
) -> Result<Vec<Value>, Box<dyn std::error::Error>> {
// 拼接日志文本,作为 prompt 的一部分
let log_text = lines.join("\n");

// 构建发送给模型的提示词
// 要求模型返回严格的 JSON 数组格式
let prompt = format!(
r#"你是一个日志解析引擎。请把下面的原始日志解析成 JSON 数组。
每条日志输出一个 JSON 对象,包含以下字段:
- timestamp: 时间戳(ISO 8601 格式)
- level: 日志级别(INFO/WARN/ERROR/DEBUG)
- message: 日志内容
- source: 来源(如果能识别)
- extra: 其他额外信息

只返回 JSON 数组,不要加任何解释或 markdown 标记。
原始日志:
{}"#,
log_text
);

// 调用本地 Ollama API
let resp = client
.post(format!("{}/api/generate", model_url))
.json(&serde_json::json!({
"model": model_name,
"prompt": prompt,
"stream": false,
"format": "json", // 强制模型返回合法 JSON
"options": {
"temperature": 0.0 // 温度设为0,确保输出稳定
}
}))
.send()
.await?;

let body: Value = resp.json().await?;

// 从模型响应中提取 JSON
let response_text = body["response"].as_str().unwrap_or("[]");
let parsed: Vec<Value> = serde_json::from_str(response_text)?;

Ok(parsed)
}

2.4 并行分块处理

有了单批次清洗逻辑,接下来用 rayon 并行处理整个文件。

注意用 tokiospawn_blocking 避免阻塞异步运行时:

use rayon::prelude::*;

/// 并行处理整个日志文件
async fn process_file(input_path: &str, batch_size: usize) -> Vec<Value> {
    // 读取全部日志内容(对于大文件可以考虑 memory-map)
    let content = tokio::fs::read_to_string(input_path)
        .await
        .expect("无法读取日志文件");

    // 按行切分,再按 batch_size 分块
    let lines: Vec<&str> = content.lines().collect();
    let chunks: Vec<Vec<String>> = lines
        .chunks(batch_size)
        .map(|chunk| chunk.iter().map(|s| s.to_string()).collect())
        .collect();

    let client = reqwest::Client::new();

    // 并行调用模型处理每个块
    let results: Vec<Vec<Value>> = futures::future::join_all(
        chunks.iter().map(|chunk| {
            clean_batch(&client, "http://127.0.0.1:11434", "qwen2.5:7b", chunk)
        })
    )
    .await
    .into_iter()
    .filter_map(|r| r.ok())   // 跳过处理失败的批次
    .collect();

    // 展平所有结果
    results.into_iter().flatten().collect()
}

三、实践经验与踩坑

3.1 模型选择

本地跑推荐 qwen2.5:7bllama3.1:8b,两者都能在 16GB 内存的机器上流畅运行。如果用云端 API,可以考虑 gpt-4o-mini,成本极低。

3.2 Prompt 调优心得

这是整个方案最关键的一环:

  1. 明确输出格式:加上 "format": "json" 参数强制结构化输出。
  2. 温度设 0:数据清洗不是创意写作,需要确定性。
  3. Few-shot 示例:在 prompt 里塞 2-3 个正确示例,准确率能提升 30% 以上。

3.3 性能优化

  • 批次大小:太小(<20 行),模型调用开销太高;太大(>100 行),输出可能截断。50 行是一个甜点。
  • 并行度:本地模型受显存限制,同时只能处理 1-2 个请求,但网络传输和序列化可以并行预计算。
  • 缓存策略:对重复出现的日志模板,先用正则预匹配,命中就直接跳过模型调用。

3.4 内存控制

处理 GB 级日志时,不要 read_to_string,改用 memory-mapped file:

use memmap2::Mmap;
use std::fs::File;

/// 高效读取大文件:使用内存映射
fn read_large_file(path: &str) -> Result<Mmap, std::io::Error> {
    let file = File::open(path)?;
    // 安全:只读映射,不会修改源文件
    let mmap = unsafe { Mmap::map(&file)? };
    Ok(mmap)
}

memmap 让操作系统按需加载页面,即使日志文件大到 10GB,实际内存占用也只有几百 MB。

实际项目里踩过一个坑:用 Ollama 的 format: json 参数强制结构化输出时,模型有时返回的不是纯 JSON 数组,而是带说明文字的 JSON。给 prompt 加上"只返回 JSON 数组,不要加任何解释"后好了一些,但批量处理 500 行日志时,最后一批偶尔还是会多出一句"已完成处理"。最终加了一层 serde_json::from_str 的错误重试机制解决。

四、总结

  1. CLI 层clap 做参数解析,灵活支持文件和管道输入。
  2. 清洗层:调用本地 Ollama 模型,将非结构化日志转为结构化 JSON。
  3. 工程化:并行分块处理、内存优化、prompt 调优,保证性能和生产可用性。

Rust + AI 是一个非常有潜力的组合。Rust 负责高性能、低内存的工程底座,AI 负责处理那些传统规则引擎搞不定的"脏活累活"。两者结合,能做很多以前想都不敢想的事情。

0

评论0

请先
显示验证码
没有账号?注册  忘记密码?