有段日子没有用到 rust 了,最近想折腾一下日志配置,结果又用掉了不少时间。
日志应该是目前公认程序中的一个重要部分,rust 甚至很早就有了 log crate, 不过 log crate 只定义了记 log 的方式,真正输出的工作还要有 fern、 log4rs 这些 crate 来进行实操,这些我以前也都用过,时间应该有个两年了, 不晓得现在会不会简单一点。
随便搜了下 rust 什么日志库好用,一个新的系列被很多人推荐:tracing。
Tracing 虽然不是官方库,但是由 tokio 项目维护, tokio 的名字相信 rust 用比较多的朋友会比较熟悉了, 我觉得基本可以认为是半个官方库了,可以放心使用。
不过 tracing 的使用方式相对于 log crate 却没有什么变化, 一样是 tracing crate 定义使用方式,tracing-subscriber 实现日志的输出, 由于我还需要输出日志到文件,所以还用到了另一个 crate:tracing-appender。 相对于其他有类似懒人包或者已经内嵌日志模块的部分语言或框架, 这属实是不够新手友好了,别难过,因为这还不是最坏的部分,后面还有更不友好的内容, 那时候难过也不迟。
简单总结下我的需求,我需要程序同时在控制台和文件中输出日志, 日志中需要包含本地时间、代码位置、以及线程的名字。
本地时间这一点其实是后面加上去的,因为默认的 tracing 输出的时间是格林威治时间, 我觉得还是本地时间看着舒服一些。
加依赖一般情况下,不是一个很难的事情,多数时间我们 cargo add
几下也就可以了,
不过对于 tracing 来说也没那么简单,可能一些朋友很快会猜到,是的,
tracing-subscriber 和 time 都需要指定 feature:
[package]
name = "tracing-test"
version = "0.1.0"
edition = "2021"
[dependencies]
time = { version = "0.3.36", features = ["std", "formatting", "parsing", "local-offset"] }
tracing = "0.1.41"
tracing-appender = "0.2.3"
tracing-subscriber = { version = "0.3.19", features = ["std", "registry", "fmt", "local-time"] }
这是我实验成功的版本,还有不少水分,如果你只是想快速建立一个输出日志的环境, 文章末尾有一个更精简的版本,下面一点篇幅我想讲一下我的思路, 给不熟悉 rust 的朋友们一些提示,来帮助我们确定自己需要的 feature, feature 是 rust 很有意思的一个设定,我们可以通过这个设定, 只选择库中自己需要的功能,很多库都提供了 feature 的配置方式, 但是这个设定无疑增加了新手的入门门槛,rust 小白多数时间需要的是开箱即用的配置, 而不是去慢慢研究哪个 feature 是我所需要的。不同的库对这一功能实现的偏好也不相同, 有些库会默认开启所有功能,有些库则不会,比如我们今天遇到的这个。
确定 feature 首先当然是要阅读说明,如果说明解释得比较详尽, 看说明可以让我们少走很多弯路。不过也是有因为我们不熟悉, 或是说明写得不够透彻的情况。
另一个确定 feature 的方法就是从代码入手了, 一般情况下我的做法是找一个比较简洁的 demo,先把它编过,然后再改成我想要的样子, 比如这段是我已经改好的代码:
use tracing_subscriber::fmt;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
fn main() {
init_log();
tracing::info!("Hello, world!");
}
fn init_log() {
let timer_fmt = time::format_description::parse(
"[year]-[month padding:zero]-[day padding:zero] [hour]:[minute]:[second].[subsecond digits:6]",
)
.expect("Failed to parse time format");
let time_offset =
time::UtcOffset::current_local_offset().unwrap_or_else(|_| time::UtcOffset::UTC);
let timer = fmt::time::OffsetTime::new(time_offset, timer_fmt);
let file_appender = tracing_appender::rolling::daily("logs", "test.log");
let file_layer = tracing_subscriber::fmt::Layer::new()
.compact()
.with_timer(timer.clone())
.with_file(true)
.with_line_number(true)
.with_thread_names(true)
.with_ansi(false)
.with_writer(file_appender);
let console_layer = tracing_subscriber::fmt::Layer::new()
.pretty()
.with_timer(timer)
.with_file(true)
.with_line_number(true)
.with_thread_names(true);
tracing_subscriber::Registry::default()
.with(file_layer)
.with(console_layer)
.init();
}
应该比较容易理解,这段代码配置了日志的输出方式,输出方式就是需求中描述的内容, 接着输出一行日志,然后退出。
那么为了还原确定 feature 的过程,我们把所有 feature 全部删掉,
代码中 use
的内容我们也去掉,为什么去掉 use
,其实这也是其中的一个坑位,
我们先把它挖开,重编,很自然会报错,不过我们每次都只看第一个报错:
error[E0433]: failed to resolve: use of undeclared crate or module `fmt`
--> src/main.rs:14:17
|
14 | let timer = fmt::time::OffsetTime::new(time_offset, timer_fmt);
| ^^^ use of undeclared crate or module `fmt`
这个是因为它不认识 fmt,fmt 是 tracing_subscriber 的一个模块,
我们加上 use tracing_subscriber::fmt
;,重编:
error[E0433]: failed to resolve: could not find `OffsetTime` in `time`
--> src/main.rs:15:28
|
15 | let timer = fmt::time::OffsetTime::new(time_offset, timer_fmt);
| ^^^^^^^^^^ could not find `OffsetTime` in `time`
|
note: found an item that was configured out
--> C:\Users\tricks1\scoop\persist\rustup\.cargo\registry\src\index.crates.io-6f17d22bba15001f\tracing-subscriber-0.3.19\src\fmt\time\mod.rs:21:21
|
21 | pub use time_crate::OffsetTime;
| ^^^^^^^^^^
note: the item is gated behind the `time` feature
--> C:\Users\tricks1\scoop\persist\rustup\.cargo\registry\src\index.crates.io-6f17d22bba15001f\tracing-subscriber-0.3.19\src\fmt\time\mod.rs:19:7
|
19 | #[cfg(feature = "time")]
| ^^^^^^^^^^^^^^^^
仔细阅读输出错误,你会发现,不认识 OffsetTime
的原因 cargo 说了,
要打开 tracing-subscriber 的 time feature,打开后再重编:
error[E0599]: no function or associated item named `current_local_offset` found for struct `UtcOffset` in the current scope
--> src/main.rs:14:26
|
14 | time::UtcOffset::current_local_offset().unwrap_or_else(|_| time::UtcOffset::UTC);
| ^^^^^^^^^^^^^^^^^^^^ function or associated item not found in `UtcOffset`
|
note: if you're trying to build a new `UtcOffset` consider using one of the following associated functions:
UtcOffset::__from_hms_unchecked
UtcOffset::from_hms
UtcOffset::from_whole_seconds
UtcOffset::parse
--> C:\Users\tricks1\scoop\persist\rustup\.cargo\registry\src\index.crates.io-6f17d22bba15001f\time-0.3.37\src\utc_offset.rs:85:5
|
85 | pub const unsafe fn __from_hms_unchecked(hours: i8, minutes: i8, seconds: i8) -> Self {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...
108 | / pub const fn from_hms(
109 | | hours: i8,
110 | | minutes: i8,
111 | | seconds: i8,
112 | | ) -> Result<Self, error::ComponentRange> {
| |____________________________________________^
...
184 | pub const fn from_whole_seconds(seconds: i32) -> Result<Self, error::ComponentRange> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...
409 | / pub fn parse(
410 | | input: &str,
411 | | description: &(impl Parsable + ?Sized),
412 | | ) -> Result<Self, error::Parse> {
| |___________________________________^
这个错误就没有那么明显了对不对,
但是它同样是找不到函数 time::UtcOffset::current_local_offset
,
我们可以 F12 打开 UtcOffset 的代码文件,来寻找 current_local_offset
函数,
注意如果你不是在用 VSCode,那可能转向定义的快捷键不是 F12:
#[cfg(feature = "local-offset")]
pub fn current_local_offset() -> Result<Self, error::IndeterminateOffset> {
let now = OffsetDateTime::now_utc();
local_offset_at(now).ok_or(error::IndeterminateOffset)
}
所以我们要打开 time 的 local-offset feature,再重编:
error[E0599]: no method named `with` found for struct `Registry` in the current scope
--> src/main.rs:36:10
|
35 | / tracing_subscriber::Registry::default()
36 | | .with(file_layer)
| | -^^^^ method not found in `Registry`
| |_________|
|
|
::: C:\Users\tricks1\scoop\persist\rustup\.cargo\registry\src\index.crates.io-6f17d22bba15001f\tracing-subscriber-0.3.19\src\layer\mod.rs:1502:8
|
1502 | fn with<L>(self, layer: L) -> Layered<L, Self>
| ---- the method is available for `Registry` here
|
= help: items from traits can only be used if the trait is in scope
help: trait `SubscriberExt` which provides `with` is implemented but not in scope; perhaps you want to import it
|
1 + use tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt;
|
For more information about this error, try `rustc --explain E0599`.
help 有一个提示,我们要 import SubscriberExt
,
源代码补一个 use tracing_subscriber::layer::SubscriberExt
,再编译:
error[E0599]: no method named `init` found for struct `Layered` in the current scope
--> src/main.rs:39:10
|
36 | / tracing_subscriber::Registry::default()
37 | | .with(file_layer)
38 | | .with(console_layer)
39 | | .init();
| |_________-^^^^
|
::: C:\Users\tricks1\scoop\persist\rustup\.cargo\registry\src\index.crates.io-6f17d22bba15001f\tracing-subscriber-0.3.19\src\util.rs:89:8
|
89 | fn init(self) {
| ---- the method is available for `Layered<tracing_subscriber::fmt::Layer<Layered<tracing_subscriber::fmt::Layer<Registry, DefaultFields, tracing_subscriber::fmt::format::Format<Compact, OffsetTime<Vec<BorrowedFormatItem<'_>>>>, RollingFileAppender>, Registry>, Pretty, tracing_subscriber::fmt::format::Format<Pretty, OffsetTime<Vec<BorrowedFormatItem<'_>>>>>, Layered<tracing_subscriber::fmt::Layer<Registry, DefaultFields, tracing_subscriber::fmt::format::Format<Compact, OffsetTime<Vec<BorrowedFormatItem<'_>>>>, RollingFileAppender>, Registry>>` here
|
= note: the full type name has been written to 'D:\src\rs\tracing-test\target\debug\deps\tracing_test.long-type-11157003815784641205.txt'
= note: consider using `--verbose` to print the full type name to the console
= help: items from traits can only be used if the trait is in scope
help: trait `SubscriberInitExt` which provides `init` is implemented but not in scope; perhaps you want to import it
|
1 + use tracing_subscriber::util::SubscriberInitExt;
|
help: there is a method `try_init` with a similar name
|
39 | .try_init();
| ~~~~~~~~
For more information about this error, try `rustc --explain E0599`.
error: could not compile `tracing-test` (bin "tracing-test") due to 1 previous error
这个应该不用解释了,
use tracing_subscriber::util::SubscriberInitExt
都给我们写好了,直接加上去重编:
Compiling tracing-test v0.1.0 (D:\src\rs\tracing-test)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.88s
非常神奇,没有报错了,以防万一我们 cargo run
跑一下程序:
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
Running `target\debug\tracing-test.exe`
2025-01-16 20:32:19.069147 INFO tracing_test: Hello, world!
at src/main.rs:6 on main
终端日志正常,再看一下文件日志:
2025-01-16 20:32:19.069028 INFO main tracing_test: src/main.rs:6: Hello, world!
你可能会注意到一个问题,两个日志记录的时间不同, 这应该是日志输出到控制台和文件的时间是不一样的,这是个不大不小的问题, 但是想解决却比较困难,我们可能要定制一个 timer,来确保两次输出的时间完全相同, 这个细节我们就不展开了。经过我们的又一番折腾,代码没有什么变化, 但是 feature 少了很多,现在的 Cargo.toml 清爽了不少:
[package]
name = "tracing-test"
version = "0.1.0"
edition = "2021"
[dependencies]
time = { version = "0.3.36", features = ["local-offset"] }
tracing = "0.1.41"
tracing-appender = "0.2.3"
tracing-subscriber = { version = "0.3.19", features = ["time"] }
其实写这段 demo 的时候也有些插曲,比如如何配置同时输出终端和文件的功能, 因为多数是二选一,而不是二合一,然后就是日志的格式如何配置, 基本上是通过阅读说明和找 demo 试出来的,不再赘述。Rust 总是被吐槽入门门槛高, 其实相对于最开始的版本,rust 的难度已经降了很多,但是因为一些语言本身的设计, 对于新手来说还是略显复杂。不过从新手的角度来看的话,如果没有过早放弃, 其实入门也只是时间问题,一切基本上都是有迹可循的,总结一下我的处理方式:
以上是我这次从头探索 tracing 的一段记录,希望能让 rust 对新手更友好一些。 我也放了一份在 github,有需要可以自取。