构建一个rust生产应用读书笔记四(实战6)

本节我们开始使用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:返回一个实现了 SubscriberSyncSend 特性的匿名类型。这样可以避免显式写出复杂的类型签名。

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"] }

修改配置configuration.rs

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)
}
相关推荐
振浩微433射频芯片3 小时前
433MHz在智能家居中的应用大全(二):智能安防篇——安全不容“信号死角”
网络·单片机·嵌入式硬件·物联网·智能家居
Rust研习社3 小时前
关于 Rust Option 的那些事:从基础到常用 API 全解析
rust
曹牧5 小时前
Oracle数据库中,将JSON字符串转换为多行数据
数据库·oracle·json
fengfuyao9855 小时前
基于STM32的4轴步进电机加减速控制工程源码(梯形加减速算法)
网络·stm32·算法
jnrjian5 小时前
text index 查看index column index定义 index 刷新频率 index视图
数据库·oracle
韶博雅6 小时前
emcc升级
oracle
瀚高PG实验室6 小时前
审计策略修改
网络·数据库·瀚高数据库
forAllforMe6 小时前
etherCAT的协议VoE,FoE,EoE,CoE的概念和区别
网络
爱分享的阿Q6 小时前
Rust加WebAssembly前端性能革命实践指南
前端·rust·wasm
大数据新鸟7 小时前
操作系统之虚拟内存
java·服务器·网络