日志记录

日志是最常见的遥测数据类型。

即使是从未听说过可观察性的开发人员,也能直观地理解日志的用处:当事情出错时,你会查看日志来了解正在发生的事情, 并祈祷自己能捕获足够的信息来有效地进行故障排除。

那么,日志是什么呢?

它的格式因时代、平台和你使用的技术而异。

如今,日志记录通常是一堆文本数据,并用换行符分隔当前记录和下一个记录。例如

The application is starting on port 8080
Handling a request to /index
Handling a request to /index
Returned a 200 OK

对于 Web 服务器来说,这四条日志记录完全有效。

Rust 生态系统在日志记录方面能为我们提供什么?

log crate

Rust 中用于日志记录的首选 crate 是 log

log 提供了五个宏:tracedebuginfowarnerror

它们的作用相同——发出一条日志记录——但正如其名称所暗示的那样,它们各自使用不同的日志级别。

trace 是最低级别: trace 级别的日志通常非常冗长,信噪比较低(例如,每次 Web 服务器收到 TCP 数据包时都会发出一条 trace 级别的日志记录)。

然后,我们依次按严重程度递增,依次为 debug、info、warn 和 error。

Error 级别的日志用于报告可能对用户造成影响的严重故障(例如,我们未能处理传入的请求或数据库查询超时)。

让我们看一个简单的使用示例:

fn fallible_operation() -> Result<String, String> { ... }

pub fn main() {
    match fallible_operation() {
        Ok(success) => {
            log::info!("Operation succeeded: {}", success);
        }
        Err(err) => {
            log::error!("Operation failed: {}", err);
        }
    }
}

我们正在尝试执行一个可能会失败的操作。

如果成功,我们将发出一条信息级别的日志记录。

如果失败,我们将发出一条错误级别的日志记录。

另请注意,log 的宏支持与标准库中 println/print 相同的插值语法。

我们可以使用 log 的宏来检测我们的代码库。

选择记录关于特定函数执行的信息通常是一个本地决策:只需查看函数本身即可决定哪些信息值得在日志记录中捕获。

这使得库能够被有效地检测,将遥测的范围扩展到我们亲自编写的代码之外。

actix-web 的日志中间件

actix_web 提供了一个 Logger 中间件。它会为每个传入的请求发出一条日志记录。

让我们将它添加到我们的应用程序中。

//! src/startup.rs
use std::net::TcpListener;

use actix_web::{dev::Server, middleware::Logger, web, App, HttpServer};
use sqlx::PgPool;

use crate::routes::{health_check, subscribe};

pub fn run(
    listener: TcpListener,
    db_pool: PgPool,
) -> Result<Server, std::io::Error> {
    let db_pool = web::Data::new(db_pool);

    let server = HttpServer::new(move || {
        App::new()
            // Middlewares are added using the `wrap` method on `App`
            .wrap(Logger::default())
            .route("/health_check", web::get().to(health_check))
            .route("/subscriptions", web::post().to(subscribe))
            .app_data(db_pool.clone())
    })
    .listen(listener)?
    .run();

    Ok(server)
}

现在,我们可以使用 cargo run 启动该应用,并使用 curl 快速发送一个请求:curl http://127.0.0.1:8000/health_check -v

请求返回 200,但是......我们用来启动应用的终端上没有任何反应。

没有日志。什么也没有。屏幕一片空白。

门面模式

我们说过,检测是一个局部决策。

相反,应用程序需要做出一个全局决策:我们应该如何处理所有这些日志记录?

应该将它们附加到文件中吗? 应该将它们打印到终端吗? 应该通过 HTTP 将它们发送到远程系统(例如 ElasticSearch)吗?

log crate 利用门面模式来处理这种二元性。

它为您提供了发出日志记录所需的工具,但没有规定应该如何处理这些日志记录。相反,它提供了一个 Log trait:

//! From `log`'s source code - src/lib.rs
/// A trait encapsulating the operations required of a logger.
pub trait Log: Sync + Send {
    /// Determines if a log message with the specified metadata would be
    /// logged.
    ///
    /// This is used by the `log_enabled!` macro to allow callers to avoid
    /// expensive computation of log message arguments if the message would be
    /// discarded anyway.
    fn enabled(&self, metadata: &Metadata) -> bool;
    /// Logs the `Record`.
    ///
    /// Note that `enabled` is *not* necessarily called before this method.
    /// Implementations of `log` should perform all necessary filtering
    /// internally.
    fn log(&self, record: &Record);
}

在主函数的开头, 您可以调用 set_logger 函数并传递 Log trait 的实现: 每次发出日志记录时,都会在您提供的记录器上调用 Log::log,从而可以执行您认为必要的任何形式的日志记录处理。

如果不调用 set_logger,所有日志记录都会被丢弃。这正是我们应用程序所发生的事情。

这次让我们初始化我们的日志记录器。

crates.io 上有一些日志实现 - 最常用的选项列在 log 本身的文档中。

我们将使用 env_logger - 如果像我们的例子一样,主要目标是将所有日志记录打印到终端,那么它就非常有效。

让我们将其添加为依赖项

cargo add env_logger

env_logger::Logger 将日志记录打印到终端,使用以下格式:

[<timestamp> <level> <module path>] <log message>

它会查看 RUST_LOG 环境变量来确定哪些日志应该打印,哪些日志应该被过滤掉。

例如, RUST_LOG=debug cargo run 会显示由我们的应用程序或我们正在使用的 crate 发出的所有调试级别或更高级别的日志。而 RUST_LOG=zero2prod 则会过滤掉由我们的依赖项发出的所有记录。

让我们根据需要修改 main.rs 文件:

//! src/main.rs
// [...]
use env_logger::Env;

#[tokio::main]
async fn main() -> std::io::Result<()> {
    // `init` does call `get_logger`, so this is all we need to do.
    // We are falling back to printing all logs at into-level or above
    // if the RUST_LOG envirionment variable has not been set.
    env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();

    // [...]
}

让我们尝试使用 cargo run 再次启动该应用程序(根据我们的默认逻辑,这相当于 RUST_LOG=info cargo run)。终端上应该会显示两条日志记录(使用换行符,并缩进以使其适合页边距)。

[2025-08-23T01:23:50Z INFO  actix_server::builder] starting 20 workers
[2025-08-23T01:23:50Z INFO  actix_server::server] Tokio runtime found; starting in existing Tokio r
untime
[2025-08-23T01:23:50Z INFO  actix_server::server] starting service: "actix-web-service-0.0.0.0:8000
", workers: 20, listening on: 0.0.0.0:8000

如果我们使用 curl 发送 http://127.0.0.1:8000/health_check 请求,你应该会看到另一条日志记录, 这条记录是由我们在前几段中添加的 Logger 中间件发出的。

[2025-08-23T01:24:24Z INFO  actix_web::middleware::logger] 127.0.0.1 "GET /health_check HTTP/1.1" 200 0 "-" "curl/8.15.0" 0.000094

日志也是探索我们正在使用的软件如何工作的绝佳工具。

尝试将 RUST_LOG 设置为 trace 并重新启动应用程序。

您应该会看到一堆来自 docs.rs/mio(一个用于非阻塞 IO 的底层库)的轮询器注册日志记录,以及由 actix-web 生成的每个工作进程的几个启动日志记录(每个工作进程对应您机器上每个可用的物理核心!)。

通过研究 trace 级别的日志,您可以学到很多有深度的东西。