secra-logger 1.0.3

一个生产级的 Rust 日志系统库,基于 tracing 生态系统构建,支持结构化 JSON 日志、文件滚动、UTC+8 时区等特性
docs.rs failed to build secra-logger-1.0.3
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
Visit the last successful build: secra-logger-3.0.3

Secra Logger

一个生产级的 Rust 日志系统库,基于 tracing 生态系统构建。

License: MIT Crates.io

特性

  • ✅ 基于 tracing 生态,支持结构化日志
  • ✅ JSON 格式输出(基于 bunyan formatter)
  • ✅ 支持控制台、文件、或同时输出
  • ✅ 按文件大小滚动
  • ✅ 按日期分目录(YYYY-MM-DD)
  • ✅ 可配置文件保留策略
  • ✅ 线程安全
  • ✅ 支持 log crate 桥接
  • ✅ 支持 actix-web 集成
  • ✅ 高性能异步写入
  • ✅ 自动时区处理(UTC+8)

安装

Cargo.toml 中添加依赖:

[dependencies]
secra-logger = "0.3"

快速开始

基本使用

use secra_logger::{init_logger, LoggerConfig, LogOutput};
use tracing::{info, error, warn, debug};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 配置日志系统
    let config = LoggerConfig::new(
        tracing::Level::INFO,   // 日志级别
        LogOutput::Both,        // 同时输出到控制台和文件
        "./logs/app.log",       // 日志路径
        10 * 1024 * 1024,      // 10MB 文件大小限制
        Some(10),               // 最多保留 10 个文件
        "myapp",                // 应用名称
    );

    // 初始化日志系统
    init_logger(config)?;

    // 使用 tracing 宏记录日志
    info!("应用启动");
    error!("发生错误");
    warn!("警告信息");
    debug!("调试信息(不会输出,因为级别是 INFO)");

    Ok(())
}

结构化日志

secra-logger 完全支持 tracing 的结构化日志功能:

use secra_logger::{init_logger, LoggerConfig, LogOutput};
use tracing::info;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = LoggerConfig::new(
        tracing::Level::INFO,
        LogOutput::Both,
        "./logs/app.log",
        10 * 1024 * 1024,
        Some(10),
        "myapp",
    );
    init_logger(config)?;

    // 结构化日志记录
    let user_id = 12345;
    let action = "login";
    
    info!(
        user_id = user_id,
        action = action,
        ip = "192.168.1.1",
        duration_ms = 150,
        "用户登录成功"
    );

    // 使用格式化字符串
    info!(
        user_id = user_id,
        "用户 {} 执行了操作", user_id
    );

    Ok(())
}

输出示例(JSON 格式):

{
  "v": 0,
  "name": "myapp",
  "msg": "用户登录成功",
  "level": 30,
  "time": "2024-01-15T10:30:45.123+08:00",
  "user_id": 12345,
  "action": "login",
  "ip": "192.168.1.1",
  "duration_ms": 150
}

错误处理

use secra_logger::{init_logger, LoggerConfig, LogOutput, LoggerError};

fn main() {
    let config = LoggerConfig::new(
        tracing::Level::INFO,
        LogOutput::File,
        "./logs/app.log",
        10 * 1024 * 1024,
        Some(10),
        "myapp",
    );

    match init_logger(config) {
        Ok(()) => {
            tracing::info!("日志系统初始化成功");
        }
        Err(LoggerError::Io(e)) => {
            eprintln!("IO 错误: {}", e);
        }
        Err(LoggerError::ConfigError { message }) => {
            eprintln!("配置错误: {}", message);
        }
        Err(e) => {
            eprintln!("初始化失败: {}", e);
        }
    }
}

多线程使用

secra-logger 是线程安全的,可以在多线程环境中安全使用:

use secra_logger::{init_logger, LoggerConfig, LogOutput};
use std::thread;
use tracing::info;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = LoggerConfig::new(
        tracing::Level::INFO,
        LogOutput::Both,
        "./logs/app.log",
        10 * 1024 * 1024,
        Some(10),
        "myapp",
    );
    init_logger(config)?;

    let handles: Vec<_> = (0..5)
        .map(|i| {
            thread::spawn(move || {
                for j in 0..10 {
                    info!(
                        thread_id = i,
                        iteration = j,
                        "线程 {} 执行第 {} 次迭代", i, j
                    );
                }
            })
        })
        .collect();

    for handle in handles {
        handle.join().unwrap();
    }

    Ok(())
}

Actix-Web 集成

use actix_web::{web, App, HttpServer, Responder};
use secra_logger::{init_logger, LoggerConfig, LogOutput};
use tracing_actix_web::TracingLogger;
use tracing::info;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // 初始化日志系统
    let config = LoggerConfig::new(
        tracing::Level::INFO,
        LogOutput::Both,
        "./logs/webapp.log",
        10 * 1024 * 1024,
        Some(20),
        "webapp",
    );
    init_logger(config).expect("初始化日志系统失败");

    info!("Web 服务器启动中...");

    // 启动 HTTP 服务器
    HttpServer::new(|| {
        App::new()
            .wrap(TracingLogger::default())  // 自动记录 HTTP 请求日志
            .route("/", web::get().to(|| async { "Hello, World!" }))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

使用 log crate 兼容接口

如果你需要使用传统的 log crate API,secra-logger 也提供了桥接支持:

use secra_logger::{init_logger, LoggerConfig, LogOutput};
use log::{info, error, warn};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = LoggerConfig::new(
        tracing::Level::INFO,
        LogOutput::Both,
        "./logs/app.log",
        10 * 1024 * 1024,
        Some(10),
        "myapp",
    );
    init_logger(config)?;

    // 可以使用 log crate 的宏
    info!("使用 log crate 记录日志");
    warn!("警告信息");
    error!("错误信息");

    Ok(())
}

配置说明

LoggerConfig

LoggerConfig 是日志系统的核心配置结构:

pub struct LoggerConfig {
    pub level: Level,              // 日志级别
    pub output: LogOutput,         // 输出目标
    pub path: PathBuf,             // 日志路径(目录或文件)
    pub max_size: u64,             // 单个文件最大大小(字节)
    pub max_files: Option<usize>,  // 最大保留文件数(None 表示无限)
    pub app_name: String,          // 应用名称
}

创建配置

使用 LoggerConfig::new() 创建配置:

let config = LoggerConfig::new(
    tracing::Level::INFO,      // 日志级别
    LogOutput::Both,           // 输出目标
    "./logs/app.log",          // 日志路径
    10 * 1024 * 1024,         // 10MB 文件大小限制
    Some(10),                 // 最多保留 10 个文件
    "myapp",                  // 应用名称
);

配置验证

配置对象提供了 validate() 方法用于验证配置的有效性:

let config = LoggerConfig::new(
    tracing::Level::INFO,
    LogOutput::File,
    "./logs/app.log",
    0,  // 无效:max_size 不能为 0
    Some(10),
    "myapp",
);

if let Err(e) = config.validate() {
    eprintln!("配置验证失败: {}", e);
}

获取基础文件名

使用 get_base_name() 方法获取日志文件的基础名称:

let config = LoggerConfig::new(
    tracing::Level::INFO,
    LogOutput::File,
    "./logs/app.log",
    10 * 1024 * 1024,
    Some(10),
    "myapp",
);

// 如果 path 是文件,返回文件名(不含扩展名)
assert_eq!(config.get_base_name().unwrap(), "app");

// 如果 path 是目录,返回 app_name
let config_dir = LoggerConfig::new(
    tracing::Level::INFO,
    LogOutput::File,
    "./logs",
    10 * 1024 * 1024,
    Some(10),
    "myapp",
);
assert_eq!(config_dir.get_base_name().unwrap(), "myapp");

LogOutput

LogOutput 枚举定义了日志的输出目标:

  • LogOutput::Stdout - 仅输出到标准输出(适合开发环境)
  • LogOutput::File - 仅输出到文件(适合生产环境)
  • LogOutput::Both - 同时输出到标准输出和文件(适合需要同时查看和保存的场景)

日志级别

支持的日志级别(从低到高):

  • Level::TRACE - 最详细的调试信息
  • Level::DEBUG - 调试信息
  • Level::INFO - 一般信息(推荐用于生产环境)
  • Level::WARN - 警告信息
  • Level::ERROR - 错误信息

只有等于或高于配置级别的日志才会被记录。例如,如果配置为 Level::INFO,则 TRACEDEBUG 级别的日志不会被记录。

日志文件组织

目录规则

日志文件按日期分目录存储,使用 YYYY-MM-DD 格式(基于 UTC+8 时区):

  • 情况一path 为目录

    • 配置:path = ./logs/test
    • 实际目录:./logs/test/2024-01-15/
    • 文件命名:{app_name}.log{app_name}-1.log
  • 情况二path 为文件

    • 配置:path = ./logs/app.log
    • 实际路径:./logs/2024-01-15/app.log
    • 文件命名:app.logapp-1.log

示例

// 配置 1:使用目录
let config1 = LoggerConfig::new(
    tracing::Level::INFO,
    LogOutput::File,
    "./logs/myapp",  // 目录
    10 * 1024 * 1024,
    Some(10),
    "myapp",
);
// 生成文件:./logs/myapp/2024-01-15/myapp.log

// 配置 2:使用文件路径
let config2 = LoggerConfig::new(
    tracing::Level::INFO,
    LogOutput::File,
    "./logs/app.log",  // 文件
    10 * 1024 * 1024,
    Some(10),
    "myapp",
);
// 生成文件:./logs/2024-01-15/app.log

文件命名规则

  • 基础日志文件{base}.log
  • 滚动日志文件{base}-n.log(n 从 1 开始递增)

示例

  • app.log(第一个文件)
  • app-1.log(第一次滚动)
  • app-2.log(第二次滚动)
  • app-3.log(第三次滚动)

重要规则

  • n 在单日内永远递增,不回绕、不复用
  • 即使旧文件被删除(由于 max_files 限制),n 仍然继续递增
  • 日期变化后,n 重置为 1(新的一天从 {base}.log 开始)

时区说明

日志系统使用 UTC+8 时区来确定日期目录。这意味着:

  • 日志文件的日期目录基于 UTC+8 时区的当前日期
  • 跨日期时,会自动创建新的日期目录
  • 时间戳在 JSON 日志中使用 ISO 8601 格式,包含时区信息

文件滚动

滚动机制

当日志文件大小达到 max_size 时,会自动触发滚动:

  1. 关闭当前文件
  2. 创建新的 {base}-n.log 文件(n 自动递增)
  3. 如果文件数量超过 max_files,删除序号最小的文件(最老的文件)

滚动示例

假设配置为:

  • max_size = 10 * 1024 * 1024 (10MB)
  • max_files = Some(5)
  • path = "./logs/app.log"

当日志文件达到 10MB 时:

./logs/2024-01-15/app.log      (10MB,关闭)
./logs/2024-01-15/app-1.log    (新建,继续写入)

app-1.log 也达到 10MB 时:

./logs/2024-01-15/app.log
./logs/2024-01-15/app-1.log    (10MB,关闭)
./logs/2024-01-15/app-2.log    (新建,继续写入)

当文件数量达到 5 个时,最老的文件会被删除:

./logs/2024-01-15/app.log      (删除,因为最老)
./logs/2024-01-15/app-1.log
./logs/2024-01-15/app-2.log
./logs/2024-01-15/app-3.log
./logs/2024-01-15/app-4.log
./logs/2024-01-15/app-5.log    (新建)

文件保留策略

  • max_files: Some(n) - 最多保留 n 个文件,超过时删除最老的文件
  • max_files: None - 不限制文件数量,保留所有历史文件

建议

  • 生产环境建议设置合理的 max_files 值,避免磁盘空间耗尽
  • 可以根据磁盘容量和日志生成速度来调整 max_sizemax_files
  • 例如:max_size = 100MBmax_files = Some(30),可以保留约 3GB 的日志

测试文件滚动

可以使用 rotation 示例来测试文件滚动功能:

cargo run --example rotation

这个示例会生成大量日志来触发文件滚动,帮助你验证滚动机制是否正常工作。

API 参考

init_logger

初始化日志系统的主函数。

pub fn init_logger(config: LoggerConfig) -> Result<()>

参数

  • config: LoggerConfig - 日志配置

返回值

  • Ok(()) - 初始化成功
  • Err(LoggerError) - 初始化失败

说明

  • 此函数可以多次调用,但只会初始化一次(使用 OnceCell 保证)
  • 后续调用会返回成功但不执行任何操作
  • 初始化失败会返回相应的错误信息

错误类型

  • LoggerError::Io - IO 错误(如无法创建目录或文件)
  • LoggerError::ConfigError - 配置错误(如 max_size 为 0)
  • LoggerError::InitError - 初始化错误(如 subscriber 设置失败)

LoggerError

日志系统的错误类型:

pub enum LoggerError {
    Io(std::io::Error),                    // IO 错误
    PathError { path: PathBuf },           // 路径错误
    ParseError { message: String },        // 文件解析错误
    InitError { message: String },         // 初始化错误
    ConfigError { message: String },       // 配置错误
    RotationError { message: String },     // 文件滚动错误
}

所有错误类型都实现了 std::error::ErrorDisplay trait,可以直接打印或转换为字符串。

运行示例

项目提供了多个示例来演示不同的使用场景:

# 运行基本示例(展示基本日志记录功能)
cargo run --example basic

# 运行文件滚动测试示例(测试文件滚动机制)
cargo run --example rotation

# 运行路径测试示例(测试目录和文件路径配置)
cargo run --example path_test

# 运行目录路径测试示例
cargo run --example path_test_dir

# 运行 actix-web 集成示例(需要 actix-web 依赖)
cargo run --example actix_web

运行示例后,可以在 ./logs/ 目录下查看生成的日志文件。

最佳实践

1. 日志级别选择

  • 开发环境:使用 Level::DEBUGLevel::TRACE 获取详细调试信息
  • 生产环境:使用 Level::INFOLevel::WARN,避免过多日志影响性能
  • 关键服务:使用 Level::WARN,只记录警告和错误

2. 输出目标选择

  • 开发环境:使用 LogOutput::StdoutLogOutput::Both,方便查看日志
  • 生产环境:使用 LogOutput::File,避免控制台输出影响性能
  • 容器环境:使用 LogOutput::Both,文件用于持久化,stdout 用于容器日志收集

3. 文件大小和保留策略

// 小型应用
let config = LoggerConfig::new(
    tracing::Level::INFO,
    LogOutput::File,
    "./logs/app.log",
    10 * 1024 * 1024,  // 10MB
    Some(10),          // 保留 10 个文件(约 100MB)
    "myapp",
);

// 大型应用
let config = LoggerConfig::new(
    tracing::Level::INFO,
    LogOutput::File,
    "./logs/app.log",
    100 * 1024 * 1024,  // 100MB
    Some(50),           // 保留 50 个文件(约 5GB)
    "myapp",
);

4. 结构化日志

充分利用结构化日志的优势:

// 好的做法:使用结构化字段
info!(
    user_id = user_id,
    action = "login",
    ip = %client_ip,
    duration_ms = elapsed.as_millis(),
    "用户登录成功"
);

// 避免:只使用格式化字符串
info!("用户 {} 从 {} 登录,耗时 {}ms", user_id, client_ip, elapsed.as_millis());

5. 错误处理

始终处理初始化错误:

match init_logger(config) {
    Ok(()) => {
        tracing::info!("日志系统初始化成功");
    }
    Err(e) => {
        eprintln!("日志系统初始化失败: {}", e);
        // 根据应用需求决定是否退出
        std::process::exit(1);
    }
}

6. 多线程环境

secra-logger 是线程安全的,可以在多线程环境中直接使用,无需额外同步:

use std::thread;

// 多个线程可以安全地同时记录日志
for i in 0..10 {
    thread::spawn(move || {
        tracing::info!(thread_id = i, "线程 {} 启动", i);
    });
}

性能考虑

  • 异步写入:日志写入是异步的,不会阻塞主线程
  • 批量刷新:日志会批量刷新到磁盘,提高性能
  • 线程安全:使用高性能锁(parking_lot),多线程环境下性能优秀
  • JSON 序列化:使用高效的 JSON 序列化库,性能开销小

常见问题

Q: 为什么日志文件没有按日期创建目录?

A: 确保日志路径的父目录存在且有写权限。如果父目录不存在,日志系统会尝试创建,但需要相应的权限。

Q: 如何更改时区?

A: 当前版本固定使用 UTC+8 时区。如果需要其他时区,可以在配置中指定(需要修改代码)。

Q: 日志文件滚动后,旧文件会被立即删除吗?

A: 不会。只有当文件数量超过 max_files 限制时,才会删除最老的文件。如果 max_filesNone,则永远不会自动删除文件。

Q: 可以在运行时更改日志级别吗?

A: 当前版本不支持运行时更改配置。需要在初始化时设置好日志级别。如果需要动态调整,可以考虑使用 tracing-subscriberEnvFilter

Q: 日志格式可以自定义吗?

A: 当前版本使用固定的 bunyan JSON 格式。如果需要自定义格式,可以修改 subscriber.rs 中的格式化逻辑。

Q: 如何集成到现有的 tracing 系统中?

A: secra-logger 使用标准的 tracing-subscriber,可以与其他 subscriber 组合使用。但需要注意,多次调用 init_logger 只会初始化一次。

依赖

主要依赖:

  • tracing - 结构化日志框架
  • tracing-subscriber - 订阅者实现
  • tracing-bunyan-formatter - JSON 格式化(bunyan 格式)
  • tracing-log - log crate 桥接
  • tracing-actix-web - actix-web 集成
  • tracing-appender - 异步日志写入
  • parking_lot - 高性能锁实现
  • chrono / chrono-tz - 日期时间处理
  • file-rotate - 文件滚动功能
  • once_cell - 全局初始化状态管理

版本历史

0.3.0

  • 初始发布
  • 支持基本的日志记录功能
  • 支持文件滚动和日期分目录
  • 支持 actix-web 集成

贡献

欢迎提交 Issue 和 Pull Request!

许可证

MIT License

Copyright (c) 2024 Secra Team

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.