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" }路径请根据调用项目和本仓库的实际位置调整。
下面以一个与本仓库并列的 demo-app 程序为例:
projects/
├── zlog/
└── demo-app/
├── Cargo.toml
├── config/
│ └── zlog.conf
└── src/
└── main.rs
在 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创建 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 个归档。
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配置文件和日志文件中的相对路径均相对于程序的当前工作目录,不是相对于可执行文件所在目录。
库 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 >stdoutfn 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 映射。
库应用或测试通常更适合持有独立的 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 时,需要传入文件、行号和函数/模块名称。
| 接口 | 用途 |
|---|---|
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| 配置 | 默认值 | 说明 |
|---|---|---|
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]
TRACE = 10
CRIT = 130, LOG_CRITlevel 名称查询不区分大小写,数值越大表示优先级越高。
[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。
规则结构:
category.level output-action; format-name
category 匹配:
| 写法 | 含义 |
|---|---|
* |
所有 category |
app |
仅精确匹配 app |
app_ |
匹配 app 及其下级,例如 app_http |
! |
没有普通 category 规则匹配时使用 |
level 匹配:
| 写法 | 含义 |
|---|---|
.* |
所有级别 |
.INFO |
大于等于 INFO |
.=INFO |
仅 INFO |
.!INFO |
除 INFO 外的级别 |
同一条日志可以匹配多条规则,并分别写入多个目标。
[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,避免同时轮转。
[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 会尝试重新启动命令并重试本次写入。
配置:
[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。
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(())
}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 数据只属于当前线程,适合保存 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: GlobalConfigrules: Vec<Rule>level(name)查询内置或自定义 levelformat(name)查询格式字符串
Rule 中可以检查 CategoryMatch、LevelMatch、Destination 和最终 format。
所有可能失败的主要接口都返回 zlog::Result<T>。常见错误包括:
Error::Config:配置语法或值错误Error::UnknownLevel/Error::UnknownFormatError::NotInitialized:尚未初始化全局 LoggerError::Io:文件、stdout/stderr、syslog 或 record sink I/O 错误Error::MissingRecordSinkError::UnsupportedPlatformError::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