本节我们开始使用tracing来记录日志,实际上在生产环境中,更推荐使用tracing作为日志记录的首先,它提供了更丰富的上下文信息和结构化日志记录功能。tracing
不仅可以记录日志信息,还可以跟踪函数调用、异步任务等,适用于复杂的分布式系统和微服务架构。
添加配置依赖
#Cargo.toml
tracing = "0.1.19"
tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] }
tracing-bunyan-formatter = "0.3.1"
tracing-log = "0.2.0"
tracing-actix-web = "0.7"
将log替换为tracing并重构
rust
//! src/routes/subscriptions.rs
use core::result::Result::{Err, Ok};
use actix_web::{web, HttpResponse};
use chrono::Utc;
use sqlx::PgPool;
use uuid::Uuid;
#[derive(serde::Deserialize)]
pub struct FormData {
email: String,
name: String,
}
#[allow(clippy::async_yields_async)]
#[tracing::instrument(name = "Adding a new subscriber",
skip(form, pool),
fields(subscriber_email=%form.email,subscriber_name=%form.name))]
pub async fn subscribe(form: web::Form<FormData>, pool: web::Data<PgPool>) -> HttpResponse {
match insert_subscriber(&pool, &form).await {
Ok(_) => HttpResponse::Ok().finish(),
Err(_) => HttpResponse::InternalServerError().finish(),
}
}
#[tracing::instrument(name = "Save new subscriber detial in database",
skip(form, pool))]
pub async fn insert_subscriber(pool: &PgPool, form: &FormData) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"insert into subscriptions (id,email,name,subscribed_at) values($1,$2,$3,$4)"#,
Uuid::new_v4(),
form.email,
form.name,
Utc::now()
)
.execute(pool)
.await
.map_err(|e| {
tracing::error!("Failed to execute query :{:?}", e);
e
})?;
Ok(())
}
clippy::async_yields_async
lint 的目的是确保异步函数在等待另一个异步操作时不会阻塞当前的任务。如果一个异步函数在等待另一个异步操作时没有及时释放当前的任务,可能会导致性能问题或死锁。
何时使用 #[allow(clippy::async_yields_async)]
有时,你可能会遇到一些特殊情况,Clippy 的警告并不是你需要关注的问题,或者你已经确保了代码的行为是正确的。在这种情况下,你可以使用 #[allow(clippy::async_yields_async)]
来禁用这个特定的警告。
name = "Adding a new subscriber":这个字段指定了生成的 Span 的名称。在这个例子中,生成的 Span 名称为 "Adding a new subscriber"。
skip(form, pool):skip 关键字用于指定哪些参数不应该被捕获并记录在 Span 中。在这个例子中,form 和 pool 参数不会被捕获和记录。这通常用于避免记录敏感信息或大型数据结构。
fields(subscriber_email=%form.email, subscriber_name=%form.name):fields 关键字用于指定要在 Span 中记录的额外字段。
%form.email 和 %form.name 表示这些字段的值将从 form 结构体中提取,并记录在 Span 中。
使用 % 符号表示这些字段的值将被格式化为字符串。
接下来,我们把main.rs 、health_check.rs 中的log依赖去掉,同时把get_subscriber和init_subscriber两个模块集成到zero2prod library库中
touch src/telemetry.rs
完整代码
rust
use tracing::subscriber::set_global_default;
use tracing::Subscriber;
use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer};
use tracing_log::LogTracer;
use tracing_subscriber::fmt::MakeWriter;
use tracing_subscriber::{layer::SubscriberExt, EnvFilter, Registry};
pub fn get_subscriber<Sink>(
name: String,
env_filter: String,
sink: Sink,
) -> impl Subscriber + Sync + Send
where
Sink: for<'a> MakeWriter<'a> + Send + Sync + 'static,
{
let env_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(env_filter));
let formatting_layer = BunyanFormattingLayer::new(name, sink);
Registry::default()
.with(env_filter)
.with(JsonStorageLayer)
.with(formatting_layer)
}
pub fn init_subscriber(subscriber: impl Subscriber + Sync + Send) {
LogTracer::init().expect("Failed to set logger");
set_global_default(subscriber).expect("Failed to set subscriber");
}
get_subscriber
函数用于构建一个 tracing
记录器(Subscriber
),该记录器由多个层(layers)组成,包括环境过滤器(EnvFilter
)、JSON 存储层(JsonStorageLayer
)和 Bunyan 格式化层(BunyanFormattingLayer
)。
参数
name: String
:记录器的名称,用于标识日志来源。env_filter: String
:环境过滤器的初始配置字符串,用于控制日志级别。sink: Sink
:日志输出的目标,可以是任何实现了MakeWriter
特性的类型。
返回值
impl Subscriber + Sync + Send
:返回一个实现了Subscriber
、Sync
和Send
特性的匿名类型。这样可以避免显式写出复杂的类型签名。
lib.rs 添加telemetry
rust
pub mod configuration;
pub mod routes;
pub mod startup;
pub mod telemetry;
main.rs 重构
rust
use sqlx::postgres::PgPool;
use std::net::TcpListener;
use zero2prod::configuration::get_configuration;
use zero2prod::startup::run;
use zero2prod::telemetry::{get_subscriber, init_subscriber};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let subscriber = get_subscriber("zero2prod".into(), "info".into(), std::io::stdout);
init_subscriber(subscriber);
let configuration = get_configuration().expect("Failed to read configuration.");
let connection_pool = PgPool::connect(&configuration.database.connection_string())
.await
.expect("Failed to connect to Postgres.");
let address = format!("127.0.0.1:{}", configuration.application_port);
let listener = TcpListener::bind(address)?;
run(listener, connection_pool)?.await?;
Ok(())
}
执行 cargo run
{"v":0,"name":"zero2prod","msg":"starting 8 workers","level":30,"hostname":"MacBook-Pro-3.local","pid":2604,"time":"2024-11-26T09:40:27.551249Z","target":"actix_server::builder","line":272,"file":"/Users/kunliu/.cargo/registry/src/index.crates.io-6f17d22bba15001f/actix-server-2.5.0/src/builder.rs"}
{"v":0,"name":"zero2prod","msg":"Tokio runtime found; starting in existing Tokio runtime","level":30,"hostname":"MacBook-Pro-3.local","pid":2604,"time":"2024-11-26T09:40:27.551419Z","target":"actix_server::server","line":192,"file":"/Users/kunliu/.cargo/registry/src/index.crates.io-6f17d22bba15001f/actix-server-2.5.0/src/server.rs"}
{"v":0,"name":"zero2prod","msg":"starting service: \"https://zhida.zhihu.com/search?content_id=250851574&content_type=Article&match_order=1&q=actix-web&zhida_source=entity-service-127.0.0.1:8000\", workers: 8, listening on: 127.0.0.1:8000","level":30,"hostname":"MacBook-Pro-3.local","pid":2604,"time":"2024-11-26T09:40:27.551475Z","target":"actix_server::server","line":197,"file":"/Users/kunliu/.cargo/registry/src/index.crates.io-6f17d22bba15001f/actix-server-2.5.0/src/server.rs"}
{"v":0,"name":"zero2prod","msg":"[ADDING A NEW SUBSCRIBER - START]","level":30,"hostname":"MacBook-Pro-3.local","pid":2604,"time":"2024-11-26T09:41:26.183856Z","target":"zero2prod::routes::subscriptions","line":15,"file":"src/routes/subscriptions.rs","subscriber_email":"chenqi@126.com","subscriber_name":"chenqi"}
{"v":0,"name":"zero2prod","msg":"[SAVE NEW SUBSCRIBER DETIAL IN DATABASE - START]","level":30,"hostname":"MacBook-Pro-3.local","pid":2604,"time":"2024-11-26T09:41:26.184047Z","target":"zero2prod::routes::subscriptions","line":25,"file":"src/routes/subscriptions.rs","subscriber_email":"chenqi@126.com","subscriber_name":"chenqi"}
{"v":0,"name":"zero2prod","msg":"[SAVE NEW SUBSCRIBER DETIAL IN DATABASE - END]","level":30,"hostname":"MacBook-Pro-3.local","pid":2604,"time":"2024-11-26T09:41:26.195659Z","target":"zero2prod::routes::subscriptions","line":25,"file":"src/routes/subscriptions.rs","subscriber_email":"chenqi@126.com","elapsed_milliseconds":11,"subscriber_name":"chenqi"}
{"v":0,"name":"zero2prod","msg":"[ADDING A NEW SUBSCRIBER - END]","level":30,"hostname":"MacBook-Pro-3.local","pid":2604,"time":"2024-11-26T09:41:26.195969Z","target":"zero2prod::routes::subscriptions","line":15,"file":"src/routes/subscriptions.rs","subscriber_email":"chenqi@126.com","elapsed_milliseconds":11,"subscriber_name":"chenqi"}
数据库密码明文处理
在生产环境中应避免数据库密采用明文,应该采取一系列的安全措施来确保密码的安全性。
Cargo.toml添加依赖
secrecy = { version = "0.8", features = ["serde"] }
rust
use secrecy::{ExposeSecret, Secret};
#[derive(serde::Deserialize)]
pub struct Settings {
pub database: DatabaseSettings,
pub application_port: u16,
}
#[derive(serde::Deserialize, Clone)]
pub struct DatabaseSettings {
pub username: String,
pub password: Secret<String>,
pub port: u16,
pub host: String,
pub database_name: String,
}
pub fn get_configuration() -> Result<Settings, config::ConfigError> {
let settings = config::Config::builder()
.add_source(config::File::new(
"configuration.yaml",
config::FileFormat::Yaml,
))
.build()?;
settings.try_deserialize::<Settings>()
}
impl DatabaseSettings {
pub fn connection_string(&self) -> Secret<String> {
Secret::new(format!(
"postgres://{}:{}@{}:{}/{}",
self.username,
self.password.expose_secret(),
self.host,
self.port,
self.database_name
))
}
}
main.rs 修改
rust
#[tokio::main]
async fn main() -> std::io::Result<()> {
//xxx
let connection_pool =
PgPool::connect(&configuration.database.connection_string().expose_secret())
.await
.expect("Failed to connect to Postgres.");
//...
}
startup.rs 修改
rust
use crate::routes::{health_check, subscribe};
use actix_web::dev::Server;
use actix_web::web::Data;
use actix_web::{web, App, HttpServer};
use sqlx::PgPool;
use std::net::TcpListener;
use tracing_actix_web::TracingLogger;
pub fn run(listener: TcpListener, db_pool: PgPool) -> Result<Server, std::io::Error> {
let db_pool = Data::new(db_pool);
let server = HttpServer::new(move || {
App::new()
.wrap(TracingLogger::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)
}