diff --git a/Cargo.lock b/Cargo.lock index d20a2f7..045e822 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -827,6 +827,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "gethostname" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "getrandom" version = "0.2.12" @@ -1183,6 +1193,15 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "md-5" version = "0.10.6" @@ -1232,6 +1251,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" @@ -1271,6 +1296,16 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -1397,6 +1432,12 @@ dependencies = [ "hashbrown 0.13.2", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking_lot" version = "0.12.1" @@ -1492,6 +1533,26 @@ dependencies = [ "sha2", ] +[[package]] +name = "pin-project" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -1608,8 +1669,17 @@ checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.5", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -1620,9 +1690,15 @@ checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.2", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.8.2" @@ -1812,6 +1888,16 @@ dependencies = [ "untrusted", ] +[[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" @@ -1915,6 +2001,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -2296,6 +2391,16 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "time" version = "0.3.31" @@ -2466,6 +2571,19 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-actix-web" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fe0d5feac3f4ca21ba33496bcb1ccab58cca6412b1405ae80f0581541e0ca78" +dependencies = [ + "actix-web", + "mutually_exclusive_features", + "pin-project", + "tracing", + "uuid", +] + [[package]] name = "tracing-attributes" version = "0.1.27" @@ -2477,6 +2595,24 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "tracing-bunyan-formatter" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5c266b9ac83dedf0e0385ad78514949e6d89491269e7065bee51d2bb8ec7373" +dependencies = [ + "ahash", + "gethostname", + "log", + "serde", + "serde_json", + "time", + "tracing", + "tracing-core", + "tracing-log 0.1.4", + "tracing-subscriber", +] + [[package]] name = "tracing-core" version = "0.1.32" @@ -2484,6 +2620,47 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log 0.2.0", ] [[package]] @@ -2569,6 +2746,12 @@ dependencies = [ "getrandom", ] +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" @@ -2684,6 +2867,28 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.52.0" @@ -2860,10 +3065,17 @@ dependencies = [ "actix-web", "chrono", "config", + "once_cell", "reqwest", + "secrecy", "serde", "sqlx", "tokio", + "tracing", + "tracing-actix-web", + "tracing-bunyan-formatter", + "tracing-log 0.2.0", + "tracing-subscriber", "uuid", ] diff --git a/Cargo.toml b/Cargo.toml index a2012c7..8da988d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,13 @@ serde = { version = "1", features = ["derive"] } config = "0.14" uuid = { version = "1.7.0", features = ["v4"] } chrono = "0.4.34" +tracing = { version = "0.1", features = ["log"] } +tracing-subscriber = { version = "0.3", features = [ "registry", "env-filter"] } +tracing-bunyan-formatter = "0.3.9" +tracing-log = "0.2" +once_cell = "1" +secrecy = { version = "0.8", features = ["serde"] } +tracing-actix-web = "0.7" [dependencies.sqlx] version = "0.7.3" diff --git a/src/configuration.rs b/src/configuration.rs index 6a15c3a..01dac62 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -1,4 +1,5 @@ use config::Config; +use secrecy::{ExposeSecret, Secret}; #[derive(serde::Deserialize)] pub struct Settings { @@ -9,25 +10,25 @@ 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, } 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/lib.rs b/src/lib.rs index 5d1dce7..9079ef9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ pub mod configuration; pub mod routes; pub mod startup; +pub mod telemetry; diff --git a/src/main.rs b/src/main.rs index 5efee55..d0425a1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,18 @@ use std::net::TcpListener; +use secrecy::ExposeSecret; use sqlx::postgres::PgPoolOptions; 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 config = get_configuration().expect("Failed to read configuration"); let connection_pool = PgPoolOptions::new() - .max_connections(10).connect(&config.database.connection_string()).await.expect("Failed to connect to Postgres."); + .max_connections(10).connect(config.database.connection_string().expose_secret()).await.expect("Failed to connect to Postgres."); let address = format!("127.0.0.1:{}", config.application_port); let listener = TcpListener::bind(address).expect("Failed to bind random port"); diff --git a/src/routes/subscriptions.rs b/src/routes/subscriptions.rs index df93978..4b27f8f 100644 --- a/src/routes/subscriptions.rs +++ b/src/routes/subscriptions.rs @@ -10,8 +10,28 @@ pub struct FormData { pub name: String, } +#[tracing::instrument( + name = "Adding a new subscriber", + skip(form, connection_pool), + fields( + subscriber_email = %form.email, + subscriber_name = %form.name + ) +)] pub async fn subscribe(form: web::Form, connection_pool: web::Data>) -> HttpResponse { - match query!( + match insert_subscriber(&form, &connection_pool).await + { + Ok(_) => HttpResponse::Ok().finish(), + Err(_) => HttpResponse::InternalServerError().finish(), + } +} + +#[tracing::instrument( + name = "Saving new subscriber details in the database", + skip(form, connection_pool) +)] +async fn insert_subscriber(form: &FormData, connection_pool: &Pool) -> Result<(), sqlx::Error> { + query!( r#" INSERT INTO subscriptions (id, email, name, subscribed_at) VALUES ($1, $2, $3, $4) @@ -21,12 +41,11 @@ pub async fn subscribe(form: web::Form, connection_pool: web::Data HttpResponse::Ok().finish(), - Err(e) => { - println!("Failed to execute query: {:?}", e); - HttpResponse::InternalServerError().finish() - } - } + .execute(connection_pool) + .await + .map_err(|e| { + tracing::error!("Failed to execute query: {:?}", e); + e + })?; + Ok(()) } diff --git a/src/startup.rs b/src/startup.rs index 3308a70..cc1413a 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -1,6 +1,7 @@ use actix_web::dev::Server; use actix_web::{web, App, HttpServer}; use sqlx::{Pool, Postgres}; +use tracing_actix_web::TracingLogger; use std::net::TcpListener; use crate::routes::{health_check, subscribe}; @@ -9,6 +10,7 @@ pub fn run(listener: TcpListener, connection_pool: Pool) -> Result(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(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 + Send + Sync) { + LogTracer::init().expect("Failed to set logger."); + set_global_default(subscriber.into()).expect("Failed to set subscriber."); +} diff --git a/tests/health_check.rs b/tests/health_check.rs index 30c423d..5abe054 100644 --- a/tests/health_check.rs +++ b/tests/health_check.rs @@ -1,15 +1,31 @@ -use sqlx::{postgres::PgPoolOptions, query, Executor, Pool, Postgres}; +use once_cell::sync::Lazy; +use secrecy::ExposeSecret; +use sqlx::{postgres::PgPoolOptions, query, Connection, Executor, PgConnection, Pool, Postgres}; use uuid::Uuid; -use std::net::TcpListener; - -use zero2prod::configuration::{get_configuration, DatabaseSettings}; +use std:: net::TcpListener; +use zero2prod::{configuration::{get_configuration, DatabaseSettings}, telemetry::{get_subscriber, init_subscriber}}; pub struct TestApp { pub address: String, pub connection_pool: Pool, } +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); + } +}); + async fn spawn_app() -> TestApp { + Lazy::force(&TRACING); + let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind random port"); let port = listener.local_addr().unwrap().port(); let address = format!("http://127.0.0.1:{}", port); @@ -28,16 +44,20 @@ async fn spawn_app() -> TestApp { } pub async fn configure_database(config: &DatabaseSettings) -> Pool { - let connection_pool = PgPoolOptions::new() - .max_connections(10) - .connect(&config.connection_string_without_db()) + let mut connection = PgConnection::connect(config.connection_string_without_db().expose_secret()) .await .expect("Failed to connect to Postgres."); - connection_pool.execute(format!(r#"CREATE DATABASE "{}";"#, config.database_name).as_str()) + connection.execute(format!(r#"CREATE DATABASE "{}";"#, config.database_name).as_str()) .await .expect("Failed to create database."); + let connection_pool = PgPoolOptions::new() + .max_connections(10) + .connect(config.connection_string().expose_secret()) + .await + .expect("Failed to connect to Postgres."); + sqlx::migrate!("./migrations") .run(&connection_pool) .await