From c88d9096398ba341ed00d4de81449837cd7d135a Mon Sep 17 00:00:00 2001 From: Nick Bland Date: Wed, 13 Mar 2024 00:05:31 +1000 Subject: [PATCH] Complete Chapter 4 + Add in Tracing Logging + Logs are JSON formatted with events + Tests are confirmed working --- Cargo.lock | 56 +++++++++++++++++++++++++++++++++++-- Cargo.toml | 3 ++ src/configuration.rs | 27 ++++++++++++------ src/main.rs | 10 ++++--- src/routes/subscriptions.rs | 45 ++++++++++++++++------------- src/startup.rs | 10 +++---- src/telemetry.rs | 13 +++++++-- tests/health_check.rs | 21 ++++++++++++-- 8 files changed, 141 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8a00784..b8d8eb8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1160,11 +1160,14 @@ dependencies = [ "actix-web", "chrono", "config", + "once_cell", "reqwest", + "secrecy", "serde", "sqlx", "tokio", "tracing", + "tracing-actix-web", "tracing-bunyan-formatter", "tracing-log 0.1.4", "tracing-subscriber", @@ -1228,6 +1231,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mutually_exclusive_features" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d02c0b00610773bb7fc61d85e13d86c7858cbdf00e1a120bfc41bc055dbaa0e" + [[package]] name = "native-tls" version = "0.2.11" @@ -1450,6 +1459,26 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -1772,6 +1801,16 @@ dependencies = [ "untrusted 0.7.1", ] +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "serde", + "zeroize", +] + [[package]] name = "security-framework" version = "2.9.2" @@ -2381,6 +2420,19 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-actix-web" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa069bd1503dd526ee793bb3fce408895136c95fc86d2edb2acf1c646d7f0684" +dependencies = [ + "actix-web", + "mutually_exclusive_features", + "pin-project", + "tracing", + "uuid", +] + [[package]] name = "tracing-attributes" version = "0.1.27" @@ -2536,9 +2588,9 @@ checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" [[package]] name = "uuid" -version = "1.4.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" +checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" dependencies = [ "getrandom", ] diff --git a/Cargo.toml b/Cargo.toml index 0fc11a0..dc2c5fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,9 @@ chrono = { version = "0.4.22", default-features = false, features = ["clock"] } tracing = { version = "0.1", features = ["log"] } tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] } tracing-bunyan-formatter = "0.3" +tracing-actix-web = "0.7" tracing-log = "0.1" +secrecy = { version = "0.8", features = ["serde"] } [dependencies.sqlx] version = "0.7" @@ -36,3 +38,4 @@ features = [ [dev-dependencies] reqwest = { version = "0.11", features = ["json"] } +once_cell = "1" diff --git a/src/configuration.rs b/src/configuration.rs index f8efebf..21a9bf0 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -1,3 +1,5 @@ +use secrecy::{ExposeSecret, Secret}; + #[derive(serde::Deserialize)] pub struct Settings { pub database: DatabaseSettings, @@ -7,7 +9,7 @@ pub struct Settings { #[derive(serde::Deserialize)] pub struct DatabaseSettings { pub username: String, - pub password: String, + pub password: Secret, pub port: u16, pub host: String, pub database_name: String, @@ -25,17 +27,24 @@ pub fn get_configuration() -> Result { } impl DatabaseSettings { - pub fn connection_string(&self) -> String { - format!( + pub fn connection_string(&self) -> Secret { + Secret::new(format!( "postgres://{}:{}@{}:{}/{}", - self.username, self.password, self.host, self.port, self.database_name - ) + self.username, + self.password.expose_secret(), + self.host, + self.port, + self.database_name + )) } - pub fn connection_string_without_db(&self) -> String { - format!( + pub fn connection_string_without_db(&self) -> Secret { + Secret::new(format!( "postgres://{}:{}@{}:{}", - self.username, self.password, self.host, self.port - ) + self.username, + self.password.expose_secret(), + self.host, + self.port + )) } } diff --git a/src/main.rs b/src/main.rs index 1cfaac9..7594a49 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,20 @@ use mail_app::configuration::get_configuration; use mail_app::startup::run; use mail_app::telemetry::{get_subscriber, init_subscriber}; +use secrecy::ExposeSecret; use sqlx::postgres::PgPool; use std::net::TcpListener; #[tokio::main] async fn main() -> Result<(), std::io::Error> { - let subscriber = get_subscriber("mail_app".into(), "info".into()); + let subscriber = get_subscriber("mail_app".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 connection_pool = + PgPool::connect(&configuration.database.connection_string().expose_secret()) + .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 diff --git a/src/routes/subscriptions.rs b/src/routes/subscriptions.rs index bdd930b..7ab2de1 100644 --- a/src/routes/subscriptions.rs +++ b/src/routes/subscriptions.rs @@ -1,7 +1,6 @@ use actix_web::{web, HttpResponse}; use chrono::Utc; use sqlx::PgPool; -use tracing::Instrument; use uuid::Uuid; #[derive(serde::Deserialize)] @@ -10,18 +9,29 @@ pub struct FormData { name: String, } -pub async fn subscribe(form: web::Form, pool: web::Data) -> HttpResponse { - let request_id = Uuid::new_v4(); - let request_span = tracing::info_span!( - "Adding a new subscriber.", - %request_id, +#[tracing::instrument( + name = "Adding a new subscriber", + skip(form, pool), + fields( subscriber_email = %form.email, subscriber_name = %form.name - ); + ) +)] - let _request_span_guard = request_span.enter(); - let query_span = tracing::info_span!("Saving new subscriber details to database"); - match sqlx::query!( +pub async fn subscribe(form: web::Form, pool: web::Data) -> HttpResponse { + match insert_subscriber(&pool, &form).await { + Ok(_) => HttpResponse::Ok().finish(), + Err(_) => HttpResponse::InternalServerError().finish(), + } +} + +#[tracing::instrument( + name = "Saving new subscriber details in the 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) @@ -31,14 +41,11 @@ pub async fn subscribe(form: web::Form, pool: web::Data) -> Ht form.name, Utc::now() ) - .execute(pool.get_ref()) - .instrument(query_span) + .execute(pool) .await - { - Ok(_) => HttpResponse::Ok().finish(), - Err(e) => { - tracing::error!("Failed to execute query: {:?}", e); - HttpResponse::InternalServerError().finish() - } - } + .map_err(|e| { + tracing::error!("Failed to execute query: {:?}", e); + e + })?; + Ok(()) } diff --git a/src/startup.rs b/src/startup.rs index b53e6b9..86eae7e 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -1,18 +1,18 @@ use crate::routes::{health_check, subscribe}; use actix_web::dev::Server; -use actix_web::middleware::Logger; -use actix_web::{web, App, HttpServer}; +use actix_web::{web, web::Data, App, HttpServer}; use sqlx::PgPool; use std::net::TcpListener; +use tracing_actix_web::TracingLogger; pub fn run(listener: TcpListener, db_pool: PgPool) -> Result { - let connection = web::Data::new(db_pool); + let db_pool = Data::new(db_pool); let server = HttpServer::new(move || { App::new() - .wrap(Logger::default()) + .wrap(TracingLogger::default()) .route("/health_check", web::get().to(health_check)) .route("/subscriptions", web::post().to(subscribe)) - .app_data(connection.clone()) + .app_data(db_pool.clone()) }) .listen(listener)? .run(); diff --git a/src/telemetry.rs b/src/telemetry.rs index e275201..3340602 100644 --- a/src/telemetry.rs +++ b/src/telemetry.rs @@ -1,12 +1,19 @@ use tracing::{subscriber::set_global_default, Subscriber}; use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer}; use tracing_log::LogTracer; -use tracing_subscriber::{layer::SubscriberExt, EnvFilter, Registry}; +use tracing_subscriber::{fmt::MakeWriter, layer::SubscriberExt, EnvFilter, Registry}; -pub fn get_subscriber(name: String, env_filter: String) -> impl Subscriber + Send + Sync { +pub fn get_subscriber( + name: String, + env_filter: String, + sink: Sink, +) -> impl Subscriber + Send + Sync +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, std::io::stdout); + let formatting_layer = BunyanFormattingLayer::new(name, sink); Registry::default() .with(env_filter) .with(JsonStorageLayer) diff --git a/tests/health_check.rs b/tests/health_check.rs index fcf97ae..21405fc 100644 --- a/tests/health_check.rs +++ b/tests/health_check.rs @@ -1,15 +1,32 @@ use mail_app::configuration::{get_configuration, DatabaseSettings}; use mail_app::startup::run; +use mail_app::telemetry::{get_subscriber, init_subscriber}; +use once_cell::sync::Lazy; +use secrecy::ExposeSecret; use sqlx::{Executor, PgPool}; use std::net::TcpListener; use uuid::Uuid; +static TRACING: Lazy<()> = Lazy::new(|| { + let default_filter_level = "info".to_string(); + let subscriber_name = "test".to_string(); + if std::env::var("TEST_LOG").is_ok() { + let subscriber = get_subscriber(subscriber_name, default_filter_level, std::io::stdout); + init_subscriber(subscriber); + } else { + let subscriber = get_subscriber(subscriber_name, default_filter_level, std::io::sink); + init_subscriber(subscriber); + }; +}); + pub struct TestApp { pub address: String, pub db_pool: PgPool, } async fn spawn_app() -> TestApp { + Lazy::force(&TRACING); + let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind to random port."); let port = listener.local_addr().unwrap().port(); let address = format!("http://127.0.0.1:{}", port); @@ -30,7 +47,7 @@ async fn spawn_app() -> TestApp { pub async fn configure_database(config: &DatabaseSettings) -> PgPool { // Create Database - let connection = PgPool::connect(&config.connection_string_without_db()) + let connection = PgPool::connect(&config.connection_string_without_db().expose_secret()) .await .expect("Failed to connect to Postgres."); connection @@ -39,7 +56,7 @@ pub async fn configure_database(config: &DatabaseSettings) -> PgPool { .expect("Failed to create database."); // Migrate Database - let connection_pool = PgPool::connect(&config.connection_string()) + let connection_pool = PgPool::connect(&config.connection_string().expose_secret()) .await .expect("Failed to connect to Postgres."); sqlx::migrate!("./migrations")