Skip to content
aishaomePublic

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

zlog-rs

zlog-rs 是一个安全、线程安全的 Rust 日志库,重实现了 zlog 的 category、level、format 和 rule 核心模型。

功能概览

  • 内置级别和 [levels] 自定义级别
  • *、精确 category、app_ 父 category 和 ! fallback 匹配
  • 全部、最低、相等和不等 level 匹配
  • stdout、stderr、syslog、管道、文件和自定义 record sink
  • 动态文件路径、按大小轮转和自定义编号归档
  • 跨线程安全写入和基于 advisory file lock 的跨进程安全轮转
  • 微秒/毫秒时间、进程、线程、源码位置、MDC 等格式符
  • 字节切片的紧凑十六进制和带偏移/ASCII 的 hex dump 输出
  • 原子配置 reload
  • 配置文件不可读取时自动使用内置兜底配置

添加依赖

当前项目作为本地 crate 使用:

[dependencies]
zlog = { path = "../zlog" }

路径请根据调用项目和本仓库的实际位置调整。

在其他 Rust 程序中使用

下面以一个与本仓库并列的 demo-app 程序为例:

projects/
├── zlog/
└── demo-app/
    ├── Cargo.toml
    ├── config/
    │   └── zlog.conf
    └── src/
        └── main.rs

1. 添加路径依赖

在 demo-app/Cargo.toml 中添加:

[package]
name = "demo-app"
version = "0.1.0"
edition = "2024"

[dependencies]
zlog = { path = "../zlog" }

也可以在 demo-app 目录执行:

cargo add zlog --path ../zlog

2. 创建日志配置

创建 demo-app/config/zlog.conf:

[global]
buffer min = 1KB
buffer max = 2MB
default format = "%d.%ms %-6V [%c] (%f:%L) %m%n"

[formats]
console = "%d.%ms %-6V [%c] %m%n"
file = "%d.%us %-6V [%c] (%F:%L) %m%n"

[rules]
demo.DEBUG >stdout; console
demo.INFO "logs/demo.log", 10MB*5; file

这份配置会将 demo category 的 DEBUG 及以上日志输出到标准输出,并将 INFO 及以上日志写入 logs/demo.log。文件超过 10 MB 时轮转,最多保留 5 个归档。

3. 在程序入口初始化

demo-app/src/main.rs:

fn run() -> zlog::Result<()> {
    zlog::zlog_debug!("demo", "loading application")?;
    zlog::zlog_info!("demo", "listening on {}", "127.0.0.1:8080")?;

    let packet = [0x48, 0x65, 0x6c, 0x6c, 0x6f];
    zlog::hzlog_debug!("demo", &packet, packet.len())?;

    if let Err(reason) = std::fs::read_to_string("settings.toml") {
        zlog::zlog_warn!("demo", "cannot read settings: {reason}")?;
    }

    Ok(())
}

fn main() -> zlog::Result<()> {
    zlog::init("config/zlog.conf")?;
    let result = run();
    zlog::fini();
    result
}

从 demo-app 根目录运行:

cargo run

配置文件和日志文件中的相对路径均相对于程序的当前工作目录,不是相对于可执行文件所在目录。

4. 在库 crate 中使用

库 crate 不建议自行调用全局 init 或 fini,因为日志生命周期应由最终应用程序控制。可以让应用传入共享的 Logger:

use std::sync::Arc;

pub struct Worker {
    logger: Arc<zlog::Logger>,
}

impl Worker {
    pub fn new(logger: Arc<zlog::Logger>) -> Self {
        Self { logger }
    }

    pub fn execute(&self) -> zlog::Result<()> {
        self.logger.log(
            "worker",
            zlog::info(),
            "job completed",
            file!(),
            line!(),
            module_path!(),
        )
    }
}

应用程序可以使用 Arc::clone(&logger) 把同一个线程安全的 Logger 传给多个组件。这种方式也适合单元测试,因为每个测试可以使用独立配置,不会互相替换全局 Logger。

快速开始

从配置字符串初始化

fn main() -> zlog::Result<()> {
    zlog::init_from_string(
        r#"
        [formats]
        normal = "%d.%ms %-6V [%c] %m%n"

        [rules]
        app.DEBUG >stdout; normal
        "#,
    )?;

    zlog::zlog_debug!("app", "starting on port {}", 8080)?;
    zlog::zlog_info!("app", "service ready")?;

    zlog::fini();
    Ok(())
}

日志宏返回 zlog::Result<()>,应用可以使用 ? 传播写入错误。

从配置文件初始化

fn main() -> zlog::Result<()> {
    let logger = zlog::init("config/zlog.conf")?;

    zlog::zlog_info!("server", "server started")?;
    assert!(logger.enabled("server", zlog::info().value));

    zlog::fini();
    Ok(())
}

zlog::init(path) 的行为:

  • 文件读取成功:解析并使用文件内容。
  • 文件不存在、无权限或发生其他 I/O 错误:使用 zlog::DEFAULT_CONFIG。
  • 文件能够读取但配置语法错误:返回错误,不启用兜底配置。

如果不希望使用兜底配置,可以直接调用 Config::from_file 和 Logger::from_config:

fn build_logger() -> zlog::Result<zlog::Logger> {
    let config = zlog::Config::from_file("config/zlog.conf")?;
    Ok(zlog::Logger::from_config(config))
}

日志级别和宏

内置级别如下:

函数 数值 对应宏
zlog::debug() 20 zlog_debug!
zlog::info() 40 zlog_info!
zlog::notice() 60 zlog_notice!
zlog::warn() 80 zlog_warn!
zlog::error() 100 zlog_error!
zlog::fatal() 120 zlog_fatal!
fn write_logs() -> zlog::Result<()> {
    zlog::zlog_debug!("db", "query: {}", "SELECT 1")?;
    zlog::zlog_info!("http", "request completed")?;
    zlog::zlog_notice!("worker", "queue recovered")?;
    zlog::zlog_warn!("cache", "cache miss")?;
    zlog::zlog_error!("payment", "charge failed")?;
    zlog::zlog_fatal!("runtime", "unrecoverable state")?;

    // 任意 Level 也可以使用通用宏。
    zlog::log!("audit", zlog::Level::new(130, "CRIT"), "manual level")?;
    Ok(())
}

使用自定义级别时,建议在配置中声明,再从 Logger 查询:

[levels]
TRACE = 10
CRIT = 130, LOG_CRIT

[rules]
app.TRACE >stdout
fn custom_level(logger: &zlog::Logger) -> zlog::Result<()> {
    let trace = logger.level("TRACE")?;
    logger.log("app", trace, "trace message", file!(), line!(), module_path!())
}

目前自定义 level 逗号后的 syslog level 名称会被接受,但尚未用于 syslog severity 映射。

不使用全局 Logger

库应用或测试通常更适合持有独立的 Arc<Logger>:

use std::sync::Arc;

fn main() -> zlog::Result<()> {
    let config = zlog::Config::parse(
        r#"
        [formats]
        short = "%V %m%n"
        [rules]
        worker.INFO >stdout; short
        "#,
    )?;
    let logger = Arc::new(zlog::Logger::from_config(config));

    logger.log(
        "worker",
        zlog::info(),
        "job completed",
        file!(),
        line!(),
        module_path!(),
    )?;

    let category = logger.category("worker");
    if category.enabled(&zlog::warn()) {
        category.log(zlog::warn(), "queue is nearly full")?;
    }
    Ok(())
}

Category::log 会自动记录调用位置。直接使用 Logger::log 时,需要传入文件、行号和函数/模块名称。

全局 Logger 接口

接口 用途
init(path) 从文件初始化;读取失败时使用 DEFAULT_CONFIG
init_from_string(text) 从字符串初始化
global() 获取当前的 Arc<Logger>
fini() 移除全局 Logger

再次调用 init 或 init_from_string 会原子替换当前全局 Logger。调用 fini() 后再使用日志宏会返回 Error::NotInitialized。

配置文件教程

完整示例:

[global]
strict init = true
buffer min = 1KB
buffer max = 2MB
rotate lock file = /tmp/zlog.lock
default format = "%d.%us %-6V (%c:%F:%L) - %m%n"
file perms = 640

[levels]
TRACE = 10
CRIT = 130, LOG_CRIT

[formats]
simple = "%m%n"
normal = "%d %V [%c] %m%n"
normalms = "%d.%ms %m%n"

[rules]
app.DEBUG >stdout; normal
app_db.INFO "/tmp/%c.log", 10MB*5; normalms
audit.=CRIT $audit, "/tmp/audit/%d(%F).log"; normal
!.WARN >stderr; simple

[global]

配置 默认值 说明
strict init false 为 true 时拒绝未知的 [global] 配置项
buffer min 1KB 单条日志格式化缓冲区的初始容量
buffer max 2MB 单条格式化结果的最大字节数;0 表示不限制
rotate lock file 无 多进程轮转使用的 advisory lock 文件
default format 内置格式 规则未指定 format 时使用的格式
file perms 600 Unix 新建日志文件权限,仍受进程 umask 影响

尺寸支持 K、KB、M、MB、G 和 GB,不区分大小写。其中 K/M/G 使用十进制,KB/MB/GB 使用 1024 进制。

当前 strict init 检查未知 global 项;其他 section 的所有语法错误始终返回 Error::Config。

[levels]

[levels]
TRACE = 10
CRIT = 130, LOG_CRIT

level 名称查询不区分大小写,数值越大表示优先级越高。

[formats]

[formats]
simple = "%m%n"
detailed = "%d.%us %-6V [%c] (%F:%L) %m%n"

常用格式符:

格式符 内容
%d 默认日期时间
%d(...) 使用 chrono/strftime 格式的日期时间
%ms / %us 3 位毫秒 / 6 位微秒
%m 日志消息
%n 换行
%c category
%V / %v 大写 / 小写 level 名称
%F / %f 完整源码路径 / 源码文件名
%L 源码行号
%U 函数或模块名
%p 进程 ID
%t / %T 线程 ID
%H HOSTNAME 环境变量
%E(NAME) 环境变量值
%M(key) 当前线程的 MDC 值
%% 百分号

支持最小宽度、最大宽度和左对齐,例如 %-6V、%20c、%.12m。

[rules]

规则结构:

category.level  output-action; format-name

category 匹配:

写法 含义
* 所有 category
app 仅精确匹配 app
app_ 匹配 app 及其下级,例如 app_http
! 没有普通 category 规则匹配时使用

level 匹配:

写法 含义
.* 所有级别
.INFO 大于等于 INFO
.=INFO 仅 INFO
.!INFO 除 INFO 外的级别

同一条日志可以匹配多条规则,并分别写入多个目标。

输出目标

stdout 和 stderr

[rules]
app.DEBUG >stdout; normal
app.ERROR >stderr; normal

文件和动态路径

[rules]
*.INFO "/var/log/myapp/%c.log"; normal

路径可以使用格式符,例如 %c、%d(...) 和 %E(HOME)。父目录不存在时会自动创建。

文件轮转

默认编号归档:

[rules]
*.INFO "/var/log/myapp/app.log", 10MB*5; normal

超过 10MB 后依次生成 app.log.1 到 app.log.5。

自定义归档路径:

[rules]
*.INFO "/var/log/myapp/app.log", 10MB*5 ~ "/var/log/myapp/archive/app.#2r.log"; normal

#r 是轮转编号,#2r 表示至少两位并以零补齐。配置 rotate lock file 后,多个进程共享同一把 advisory lock,避免同时轮转。

syslog

[rules]
system.WARN >syslog, LOG_LOCAL3; normal

支持 LOG_USER 和 LOG_LOCAL0 至 LOG_LOCAL7。syslog 输出当前仅支持 Unix。

管道

[rules]
access.INFO | gzip >> /tmp/access.log.gz; simple

管道命令会交给系统 shell 执行,配置文件必须来自可信来源。命令管道关闭后,Logger 会尝试重新启动命令并重试本次写入。

自定义 record sink

配置:

[formats]
json_line = "%m%n"

[rules]
audit.INFO $audit, "/events/%c"; json_line

注册处理函数:

use std::sync::Arc;

fn main() -> zlog::Result<()> {
    let config = zlog::Config::parse(
        r#"
        [formats]
        line = "%V %m%n"
        [rules]
        audit.INFO $audit, "/events/%c"; line
        "#,
    )?;
    let logger = Arc::new(zlog::Logger::from_config(config));

    logger.set_record("audit", |record| {
        println!(
            "sink path={} category={} level={} message={}",
            record.path,
            record.category,
            record.level.name,
            record.message.trim_end()
        );
        Ok(())
    })?;

    logger.log(
        "audit",
        zlog::info(),
        "user signed in",
        file!(),
        line!(),
        module_path!(),
    )?;
    Ok(())
}

规则引用了未注册的 sink 时,写日志会返回 Error::MissingRecordSink。

十六进制数据输出

C zlog 风格的 hzlog_* 接口

Rust 提供与 C 宏同名、参数顺序对应的接口:

hzlog_debug(category, buffer, buffer_length);
hzlog_info(category, buffer, buffer_length);

对应 Rust 调用:

fn log_binary(packet: &[u8]) -> zlog::Result<()> {
    zlog::hzlog_debug!("network", packet, packet.len())?;
    zlog::hzlog_info!("network", packet, packet.len())?;
    zlog::hzlog_notice!("network", packet, packet.len())?;
    zlog::hzlog_warn!("network", packet, packet.len())?;
    zlog::hzlog_error!("network", packet, packet.len())?;
    zlog::hzlog_fatal!("network", packet, packet.len())?;
    Ok(())
}

这些宏分别使用与 C zlog 相同的 DEBUG 20、INFO 40、NOTICE 60、WARN 80、ERROR 100 和 FATAL 120 级别,并自动记录源码文件和行号。通用版本可以传入任意 level:

zlog::hzlog!("network", zlog::Level::new(130, "CRIT"), packet, packet.len())?;

buffer 必须实现 AsRef<[u8]>。buffer_length 大于 slice 实际长度时会安全地限制为 slice 长度,不会发生 C 接口可能出现的越界读取。

hex 数据作为 %m 内容参与规则格式化。数据行采用 C zlog 的布局:从 1 开始的十进制行号、每行 16 字节、大写十六进制和 ASCII 区域。当前 Rust 版的列头使用更紧凑的间距,因此整体输出并非与 C zlog 逐字节相同:

 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
         1   48 65 6C 6C 6F 2C 20 7A 6C 6F 67 21 00 FF 6D 6F   Hello, zlog!..mo
         2   72 65                                             re

与 C API 相比,Rust 接口还有以下语义差异:

  • C 宏返回 void;Rust 宏返回 zlog::Result<()>,便于处理未初始化或写入失败。
  • C 接口允许传入空指针并输出 buf=(null);安全 Rust 接口只接收字节切片,空切片会输出列头和一个空数据行。
  • C 的 %U 表示 __func__ 函数名;Rust 宏使用 module_path!(),因此 %U 输出的是模块路径。

独立 Logger 和 Category 也提供同样的调用方式:

fn log_without_global(
    logger: &zlog::Logger,
    category: &zlog::Category,
    packet: &[u8],
) -> zlog::Result<()> {
    logger.log_hex(
        "network",
        zlog::info(),
        packet,
        file!(),
        line!(),
        module_path!(),
    )?;
    category.log_hex(zlog::debug(), packet)?;
    Ok(())
}

Display 格式器

hex() 和 hex_dump() 返回实现了 Display 的字节视图,不复制原始数据,可以嵌入普通日志消息。

紧凑十六进制:

fn log_packet(packet: &[u8]) -> zlog::Result<()> {
    zlog::zlog_info!("network", "packet={}", zlog::hex(packet))?;
    Ok(())
}

输入:

[0x00, 0x01, 0xAB, 0xFF]

输出:

packet=00 01 AB FF

多行 C zlog 风格 hex dump:

fn dump_packet(packet: &[u8]) -> zlog::Result<()> {
    zlog::zlog_debug!("network", "packet dump:\n{}", zlog::hex_dump(packet))?;
    Ok(())
}

hex_dump() 即使接收空切片,也会按 C zlog 行为输出列头和一个空数据行。所有 hex 输出都受 [global] buffer max 限制。

MDC:线程局部上下文

MDC 数据只属于当前线程,适合保存 request ID、trace ID 等上下文:

fn handle_request(request_id: &str) -> zlog::Result<()> {
    zlog::put_mdc("request_id", request_id);
    zlog::zlog_info!("http", "handling request")?;

    assert_eq!(zlog::get_mdc("request_id").as_deref(), Some(request_id));
    zlog::remove_mdc("request_id");
    zlog::clean_mdc();
    Ok(())
}

对应格式:

[formats]
request = "[%M(request_id)] %m%n"

新线程不会自动继承父线程 MDC,需要在目标线程中重新设置。

配置重载

从字符串原子重载:

fn reload(logger: &zlog::Logger) -> zlog::Result<()> {
    logger.reload(
        r#"
        [formats]
        short = "%m%n"
        [rules]
        app.WARN >stderr; short
        "#,
    )
}

从文件重载:

logger.reload_file("config/zlog.conf")?;

新配置会先完整解析,成功后才替换旧配置。解析失败时旧配置继续有效。当前没有自动文件监听或周期性 reload。

解析和检查配置

fn inspect_config(text: &str) -> zlog::Result<()> {
    let config = zlog::Config::parse(text)?;

    println!("rules={}", config.rules.len());
    println!("buffer_max={}", config.global.buffer_max);
    println!("default={:?}", config.format("default"));

    let info = config.level("info")?;
    println!("INFO={} ({})", info.value, info.name);
    Ok(())
}

Config 公开提供:

  • global: GlobalConfig
  • rules: Vec<Rule>
  • level(name) 查询内置或自定义 level
  • format(name) 查询格式字符串

Rule 中可以检查 CategoryMatch、LevelMatch、Destination 和最终 format。

错误处理

所有可能失败的主要接口都返回 zlog::Result<T>。常见错误包括:

  • Error::Config:配置语法或值错误
  • Error::UnknownLevel / Error::UnknownFormat
  • Error::NotInitialized:尚未初始化全局 Logger
  • Error::Io:文件、stdout/stderr、syslog 或 record sink I/O 错误
  • Error::MissingRecordSink
  • Error::UnsupportedPlatform
  • Error::PipeClosed

推荐在应用边界记录或转换错误,在业务函数中使用 ? 传播。

并发与生命周期

  • Logger 的配置、文件、管道和 record sink 均通过同步原语保护。
  • 多线程可共享 Arc<Logger>。
  • 配置了 rotate lock file 后,可协调多个进程对同一日志的轮转。
  • fini() 只清除全局引用;其他地方持有的 Arc<Logger> 仍然有效。
  • 文件和管道资源会在对应 Logger 最后一个引用释放时关闭。

当前限制

  • 尚未实现 #s 序列归档。
  • 尚未实现自动配置文件监听或按日志次数自动 reload。
  • 自定义 level 的 syslog level 名称尚未参与 severity 映射。
  • 没有 C ABI;兼容 C variadic 日志接口需要额外 C shim。
  • syslog 仅支持 Unix,且 facility 当前限于 LOG_USER、LOG_LOCAL0 至 LOG_LOCAL7。

开发与验证

cargo fmt --check
cargo test
cargo clippy --all-targets -- -D warnings
cargo doc --no-deps

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages