
让程序学会"说话"和"听话"——tracing 日志与 config 配置全攻略
你有没有过这种经历:
程序跑着跑着崩了,你一脸懵逼:
没有日志, debugging 就像摸黑找猫。
今天咱们聊聊两件事:
生活化类比:
级别 | 用途 | 示例 |
|---|---|---|
ERROR | 严重错误 | 数据库连不上 |
WARN | 警告 | 配置缺失,用默认值 |
INFO | 一般信息 | 服务器启动 |
DEBUG | 调试信息 | 函数入参 |
TRACE | 详细追踪 | 每步执行流程 |
类比:
Rust 主要有两个日志生态:
我的建议: 新项目用 tracing,老项目随意。
为什么?
问题: 数据库地址写死在代码里?改配置得重新编译?
解决: 配置文件 + 环境变量。
类比:
cargo new my_logging_app
cd my_logging_app
修改 Cargo.toml:
[package]
name = "my_logging_app"
version = "0.1.0"
edition = "2021"
[dependencies]
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
tokio = { version = "1.35", features = ["full"] }
use tracing::{info, warn, error, debug, trace};
use tracing_subscriber;
fn main() {
// 初始化日志
tracing_subscriber::fmt::init();
info!("服务器启动");
debug!("加载配置...");
warn!("配置文件不存在,使用默认值");
error!("数据库连接失败");
trace!("函数调用细节");
}
运行:
RUST_LOG=info cargo run
输出:
2024-01-01T12:00:00.000000Z INFO my_logging_app: 服务器启动
2024-01-01T12:00:00.000000Z DEBUG my_logging_app: 加载配置...
2024-01-01T12:00:00.000000Z WARN my_logging_app: 配置文件不存在,使用默认值
2024-01-01T12:00:00.000000Z ERROR my_logging_app: 数据库连接失败
控制日志级别:
RUST_LOG=error cargo run # 只显示 ERROR
RUST_LOG=debug cargo run # 显示 DEBUG 及以上
RUST_LOG=trace cargo run # 显示所有
use tracing::{info, warn, instrument};
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
#[derive(Debug)]
struct User {
id: u32,
name: String,
}
fn main() {
// 初始化 JSON 格式日志
tracing_subscriber::registry()
.with(EnvFilter::from_default_env())
.with(fmt::layer().json())
.init();
let user = User {
id: ,
name: "Larry".to_string(),
};
// 结构化日志
info!(user_id = user.id, user_name = %user.name, "用户登录");
warn!(
attempt = ,
max_attempts = ,
"登录失败,即将锁定账户"
);
}
输出 (JSON):
{"timestamp":"2024-01-01T12:00:00Z","level":"INFO","fields":{"message":"用户登录","user_id":,"user_name":"Larry"},"target":"my_logging_app"}
{"timestamp":"2024-01-01T12:00:00Z","level":"WARN","fields":{"message":"登录失败,即将锁定账户","attempt":,"max_attempts":},"target":"my_logging_app"}
爽在哪? JSON 格式可以方便地导入日志分析系统(ELK、Splunk 等)。
use tracing::{info, instrument, Level};
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
use std::thread;
use std::time::Duration;
// 方式 1:用 attribute 宏
#[instrument(name = "expensive_function", skip(input))]
fn expensive_function(input: &str) -> String {
info!("开始处理");
thread::sleep(Duration::from_millis());
info!("处理完成");
format!("Processed: {}", input)
}
// 方式 2:手动创建 span
fn manual_span() {
let span = tracing::span!(Level::INFO, "my_span");
let _guard = span.enter();
info!("在 span 内");
do_something();
}
fn do_something() {
info!("doing something");
}
fn main() {
tracing_subscriber::registry()
.with(EnvFilter::from_default_env())
.with(fmt::layer())
.init();
let result = expensive_function("test");
info!("结果:{}", result);
manual_span();
}
输出:
2024-01-01T12:00:00Z INFO expensive_function: 开始处理
2024-01-01T12:00:00Z INFO expensive_function: 处理完成
2024-01-01T12:00:00Z INFO 结果:Processed: test
2024-01-01T12:00:00Z INFO my_span: 在 span 内
2024-01-01T12:00:00Z INFO my_span:doing_something: doing something
看到没? span 会自动记录层级关系,知道哪个函数调了哪个。
use tracing::{info, instrument};
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
use tokio::time::{sleep, Duration};
#[instrument]
async fn async_task(name: &str, delay_ms: u64) {
info!("开始任务:{}", name);
sleep(Duration::from_millis(delay_ms)).await;
info!("完成任务:{}", name);
}
#[tokio::main]
async fn main() {
tracing_subscriber::registry()
.with(EnvFilter::from_default_env())
.with(fmt::layer())
.init();
info!("启动异步任务");
// 并发执行
tokio::join!(
async_task("任务 A", ),
async_task("任务 B", ),
async_task("任务 C", ),
);
info!("所有任务完成");
}
[dependencies]
config = "0.14"
serde = { version = "1.0", features = ["derive"] }
dotenvy = "0.15"
use config::{Config, ConfigError, File, Environment};
use serde::Deserialize;
use std::path::Path;
#[derive(Debug, Deserialize)]
struct Settings {
debug: bool,
database: Database,
server: Server,
}
#[derive(Debug, Deserialize)]
struct Database {
url: String,
pool_size: u32,
}
#[derive(Debug, Deserialize)]
struct Server {
host: String,
port: u16,
}
fn main() -> Result<(), ConfigError> {
// 1. 读取配置文件
let config_file = File::with_name("config/settings").required(false);
// 2. 读取环境变量 (覆盖配置文件)
let env = Environment::with_prefix("APP");
// 3. 构建配置
let settings = Config::builder()
.add_source(config_file)
.add_source(env)
.build()?;
// 4. 反序列化为结构体
let settings: Settings = settings.try_deserialize()?;
println!("调试模式:{}", settings.debug);
println!("数据库:{}", settings.database.url);
println!("服务器:{}:{}", settings.server.host, settings.server.port);
Ok(())
}
配置文件 (config/settings.yaml):
debug: true
database:
url: "postgres://localhost/mydb"
pool_size:
server:
host: "127.0.0.1"
port:
环境变量覆盖:
export APP_DEBUG=false
export APP_SERVER_PORT=3000
cargo run
优先级: 环境变量 > 配置文件 > 默认值
use config::{Config, File, Environment};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Settings {
app_name: String,
database: Database,
}
#[derive(Debug, Deserialize)]
struct Database {
url: String,
}
fn load_settings(env: &str) -> Result<Settings, config::ConfigError> {
let settings = Config::builder()
// 基础配置 (所有环境共用)
.add_source(File::with_name("config/default").required(false))
// 环境特定配置
.add_source(File::with_name(&format!("config/{}", env)).required(false))
// 环境变量
.add_source(Environment::with_prefix("APP").separator("__"))
.build()?;
settings.try_deserialize()
}
fn main() -> Result<(), config::ConfigError> {
// 从环境变量读取环境名,默认 development
let env = std::env::var("APP_ENV").unwrap_or_else(|_| "development".to_string());
println!("当前环境:{}", env);
let settings = load_settings(&env)?;
println!("配置:{:?}", settings);
Ok(())
}
配置文件结构:
config/
├── default.yaml # 默认配置
├── development.yaml # 开发环境
├── production.yaml # 生产环境
└── test.yaml # 测试环境
config/default.yaml:
app_name: "my-app"
database:
url: "postgres://localhost/mydb"
pool_size:
config/production.yaml:
database:
url: "postgres://prod-server/proddb"
pool_size:
use config::Config;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Settings {
server: Server,
database: Database,
}
#[derive(Debug, Deserialize)]
struct Server {
host: String,
port: u16,
}
#[derive(Debug, Deserialize)]
struct Database {
url: String,
pool_size: u32,
}
impl Settings {
fn validate(&self) -> Result<(), String> {
// 验证端口范围
if self.server.port == || self.server.port > {
return Err(format!("无效端口:{}", self.server.port));
}
// 验证连接池大小
if self.database.pool_size == {
return Err("连接池大小必须大于 0".to_string());
}
if self.database.pool_size > {
return Err("连接池大小不能超过 100".to_string());
}
// 验证数据库 URL
if !self.database.url.starts_with("postgres://")
&& !self.database.url.starts_with("mysql://")
&& !self.database.url.starts_with("sqlite:") {
return Err("不支持的数据库类型".to_string());
}
Ok(())
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let settings: Settings = Config::builder()
.add_source(File::with_name("config/settings"))
.build()?
.try_deserialize()?;
// 验证配置
settings.validate()?;
println!("✅ 配置验证通过");
Ok(())
}
现象: 写了 info! 但没输出
原因: 忘了调用 tracing_subscriber::fmt::init()
解决:
fn main() {
tracing_subscriber::fmt::init(); // ✅ 必须先初始化
info!("现在能看到了");
}
现象: 只看到 INFO,看不到 DEBUG
解决:
RUST_LOG=debug cargo run
或者代码里设置:
tracing_subscriber::fmt()
.with_env_filter("debug") // ✅ 强制设为 debug
.init();
现象: ConfigError: configuration file "config/settings" not found
解决:
// 用绝对路径
File::with_name("/absolute/path/config/settings")
// 或者检查当前目录
println!("当前目录:{:?}", std::env::current_dir()?);
现象: 环境变量没生效
原因: 嵌套结构体要用双下划线
解决:
// Rust 结构体
struct Settings {
server: Server,
}
struct Server {
port: u16,
}
// 环境变量名
export APP_SERVER__PORT=// ✅ 双下划线分隔嵌套
// src/main.rs
use config::{Config, File, Environment};
use serde::Deserialize;
use tracing::{info, warn, error, instrument};
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
use axum::{routing::get, Router};
use std::net::SocketAddr;
// 配置结构
#[derive(Debug, Deserialize, Clone)]
struct Settings {
app: AppConfig,
server: ServerConfig,
database: DatabaseConfig,
logging: LoggingConfig,
}
#[derive(Debug, Deserialize, Clone)]
struct AppConfig {
name: String,
version: String,
debug: bool,
}
#[derive(Debug, Deserialize, Clone)]
struct ServerConfig {
host: String,
port: u16,
}
#[derive(Debug, Deserialize, Clone)]
struct DatabaseConfig {
url: String,
pool_size: u32,
}
#[derive(Debug, Deserialize, Clone)]
struct LoggingConfig {
level: String,
format: LogFormat,
}
#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "lowercase")]
enum LogFormat {
Pretty,
Json,
}
impl Settings {
fn load() -> Result<Self, config::ConfigError> {
let env = std::env::var("APP_ENV").unwrap_or_else(|_| "development".to_string());
Config::builder()
.add_source(File::with_name("config/default").required(false))
.add_source(File::with_name(&format!("config/{}", env)).required(false))
.add_source(Environment::with_prefix("APP").separator("__"))
.build()?
.try_deserialize()
}
fn validate(&self) -> Result<(), String> {
if self.database.pool_size == || self.database.pool_size > {
return Err("连接池大小必须在 1-100 之间".to_string());
}
Ok(())
}
}
// 初始化日志
fn init_logging(settings: &Settings) {
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new(&settings.logging.level));
let subscriber = tracing_subscriber::registry().with(filter);
match settings.logging.format {
LogFormat::Pretty => {
subscriber.with(fmt::layer().pretty()).init();
}
LogFormat::Json => {
subscriber.with(fmt::layer().json()).init();
}
}
info!("日志系统初始化完成");
}
// 健康检查接口
#[instrument]
async fn health_check() -> &'static str {
info!("健康检查");
"OK"
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 加载配置
let settings = Settings::load()?;
settings.validate()?;
// 初始化日志
init_logging(&settings);
info!("应用启动:{} v{}", settings.app.name, settings.app.version);
info!("调试模式:{}", settings.app.debug);
// 构建路由
let app = Router::new()
.route("/health", get(health_check));
// 启动服务器
let addr: SocketAddr = format!("{}:{}", settings.server.host, settings.server.port)
.parse()?;
info!("服务器监听:{}", addr);
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, app).await?;
Ok(())
}
配置文件 (config/development.yaml):
app:
name: "my-web-service"
version: "1.0.0"
debug: true
server:
host: "127.0.0.1"
port:
database:
url: "sqlite:./dev.db"
pool_size:
logging:
level: "debug"
format: "pretty"
配置文件 (config/production.yaml):
app:
debug: false
server:
host: "0.0.0.0"
port:
database:
url: "postgres://prod-server/proddb"
pool_size:
logging:
level: "info"
format: "json"
运行:
# 开发环境
cargo run
# 生产环境
APP_ENV=production cargo run
# 覆盖配置
APP_SERVER__PORT=9000 cargo run
核心要点:
金句:
没有日志的程序,就像没有黑匣子的飞机。
配置写死在代码里,就像空调焊死在 26 度。
生产环境用 JSON 日志,机器分析才方便。
下篇预告:
日志有了,配置有了,但程序跑在浏览器里是不是更酷?下篇咱们聊聊 WASM 实践,用 Rust 写前端代码,让浏览器跑 Rust!