Complete Chapter 4

+ Add in Tracing Logging
+ Logs are JSON formatted with events
+ Tests are confirmed working
This commit is contained in:
Nick Bland 2024-03-13 00:05:31 +10:00
parent 2839aec040
commit c88d909639
8 changed files with 141 additions and 44 deletions

56
Cargo.lock generated
View File

@ -1160,11 +1160,14 @@ dependencies = [
"actix-web", "actix-web",
"chrono", "chrono",
"config", "config",
"once_cell",
"reqwest", "reqwest",
"secrecy",
"serde", "serde",
"sqlx", "sqlx",
"tokio", "tokio",
"tracing", "tracing",
"tracing-actix-web",
"tracing-bunyan-formatter", "tracing-bunyan-formatter",
"tracing-log 0.1.4", "tracing-log 0.1.4",
"tracing-subscriber", "tracing-subscriber",
@ -1228,6 +1231,12 @@ dependencies = [
"windows-sys 0.48.0", "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]] [[package]]
name = "native-tls" name = "native-tls"
version = "0.2.11" version = "0.2.11"
@ -1450,6 +1459,26 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" 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]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.13" version = "0.2.13"
@ -1772,6 +1801,16 @@ dependencies = [
"untrusted 0.7.1", "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]] [[package]]
name = "security-framework" name = "security-framework"
version = "2.9.2" version = "2.9.2"
@ -2381,6 +2420,19 @@ dependencies = [
"tracing-core", "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]] [[package]]
name = "tracing-attributes" name = "tracing-attributes"
version = "0.1.27" version = "0.1.27"
@ -2536,9 +2588,9 @@ checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.4.1" version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a"
dependencies = [ dependencies = [
"getrandom", "getrandom",
] ]

View File

@ -20,7 +20,9 @@ chrono = { version = "0.4.22", default-features = false, features = ["clock"] }
tracing = { version = "0.1", features = ["log"] } tracing = { version = "0.1", features = ["log"] }
tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] } tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] }
tracing-bunyan-formatter = "0.3" tracing-bunyan-formatter = "0.3"
tracing-actix-web = "0.7"
tracing-log = "0.1" tracing-log = "0.1"
secrecy = { version = "0.8", features = ["serde"] }
[dependencies.sqlx] [dependencies.sqlx]
version = "0.7" version = "0.7"
@ -36,3 +38,4 @@ features = [
[dev-dependencies] [dev-dependencies]
reqwest = { version = "0.11", features = ["json"] } reqwest = { version = "0.11", features = ["json"] }
once_cell = "1"

View File

@ -1,3 +1,5 @@
use secrecy::{ExposeSecret, Secret};
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
pub struct Settings { pub struct Settings {
pub database: DatabaseSettings, pub database: DatabaseSettings,
@ -7,7 +9,7 @@ pub struct Settings {
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
pub struct DatabaseSettings { pub struct DatabaseSettings {
pub username: String, pub username: String,
pub password: String, pub password: Secret<String>,
pub port: u16, pub port: u16,
pub host: String, pub host: String,
pub database_name: String, pub database_name: String,
@ -25,17 +27,24 @@ pub fn get_configuration() -> Result<Settings, config::ConfigError> {
} }
impl DatabaseSettings { impl DatabaseSettings {
pub fn connection_string(&self) -> String { pub fn connection_string(&self) -> Secret<String> {
format!( Secret::new(format!(
"postgres://{}:{}@{}:{}/{}", "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 { pub fn connection_string_without_db(&self) -> Secret<String> {
format!( Secret::new(format!(
"postgres://{}:{}@{}:{}", "postgres://{}:{}@{}:{}",
self.username, self.password, self.host, self.port self.username,
) self.password.expose_secret(),
self.host,
self.port
))
} }
} }

View File

@ -1,18 +1,20 @@
use mail_app::configuration::get_configuration; use mail_app::configuration::get_configuration;
use mail_app::startup::run; use mail_app::startup::run;
use mail_app::telemetry::{get_subscriber, init_subscriber}; use mail_app::telemetry::{get_subscriber, init_subscriber};
use secrecy::ExposeSecret;
use sqlx::postgres::PgPool; use sqlx::postgres::PgPool;
use std::net::TcpListener; use std::net::TcpListener;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), std::io::Error> { 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); init_subscriber(subscriber);
let configuration = get_configuration().expect("Failed to read configuration"); let configuration = get_configuration().expect("Failed to read configuration");
let connection_pool = PgPool::connect(&configuration.database.connection_string()) let connection_pool =
.await PgPool::connect(&configuration.database.connection_string().expose_secret())
.expect("Failed to connect to Postgres."); .await
.expect("Failed to connect to Postgres.");
let address = format!("127.0.0.1:{}", configuration.application_port); let address = format!("127.0.0.1:{}", configuration.application_port);
let listener = TcpListener::bind(address)?; let listener = TcpListener::bind(address)?;
run(listener, connection_pool)?.await run(listener, connection_pool)?.await

View File

@ -1,7 +1,6 @@
use actix_web::{web, HttpResponse}; use actix_web::{web, HttpResponse};
use chrono::Utc; use chrono::Utc;
use sqlx::PgPool; use sqlx::PgPool;
use tracing::Instrument;
use uuid::Uuid; use uuid::Uuid;
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
@ -10,18 +9,29 @@ pub struct FormData {
name: String, name: String,
} }
pub async fn subscribe(form: web::Form<FormData>, pool: web::Data<PgPool>) -> HttpResponse { #[tracing::instrument(
let request_id = Uuid::new_v4(); name = "Adding a new subscriber",
let request_span = tracing::info_span!( skip(form, pool),
"Adding a new subscriber.", fields(
%request_id,
subscriber_email = %form.email, subscriber_email = %form.email,
subscriber_name = %form.name subscriber_name = %form.name
); )
)]
let _request_span_guard = request_span.enter(); pub async fn subscribe(form: web::Form<FormData>, pool: web::Data<PgPool>) -> HttpResponse {
let query_span = tracing::info_span!("Saving new subscriber details to database"); match insert_subscriber(&pool, &form).await {
match sqlx::query!( 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#" r#"
INSERT INTO subscriptions (id, email, name, subscribed_at) INSERT INTO subscriptions (id, email, name, subscribed_at)
VALUES ($1, $2, $3, $4) VALUES ($1, $2, $3, $4)
@ -31,14 +41,11 @@ pub async fn subscribe(form: web::Form<FormData>, pool: web::Data<PgPool>) -> Ht
form.name, form.name,
Utc::now() Utc::now()
) )
.execute(pool.get_ref()) .execute(pool)
.instrument(query_span)
.await .await
{ .map_err(|e| {
Ok(_) => HttpResponse::Ok().finish(), tracing::error!("Failed to execute query: {:?}", e);
Err(e) => { e
tracing::error!("Failed to execute query: {:?}", e); })?;
HttpResponse::InternalServerError().finish() Ok(())
}
}
} }

View File

@ -1,18 +1,18 @@
use crate::routes::{health_check, subscribe}; use crate::routes::{health_check, subscribe};
use actix_web::dev::Server; use actix_web::dev::Server;
use actix_web::middleware::Logger; use actix_web::{web, web::Data, App, HttpServer};
use actix_web::{web, App, HttpServer};
use sqlx::PgPool; use sqlx::PgPool;
use std::net::TcpListener; use std::net::TcpListener;
use tracing_actix_web::TracingLogger;
pub fn run(listener: TcpListener, db_pool: PgPool) -> Result<Server, std::io::Error> { pub fn run(listener: TcpListener, db_pool: PgPool) -> Result<Server, std::io::Error> {
let connection = web::Data::new(db_pool); let db_pool = Data::new(db_pool);
let server = HttpServer::new(move || { let server = HttpServer::new(move || {
App::new() App::new()
.wrap(Logger::default()) .wrap(TracingLogger::default())
.route("/health_check", web::get().to(health_check)) .route("/health_check", web::get().to(health_check))
.route("/subscriptions", web::post().to(subscribe)) .route("/subscriptions", web::post().to(subscribe))
.app_data(connection.clone()) .app_data(db_pool.clone())
}) })
.listen(listener)? .listen(listener)?
.run(); .run();

View File

@ -1,12 +1,19 @@
use tracing::{subscriber::set_global_default, Subscriber}; use tracing::{subscriber::set_global_default, Subscriber};
use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer}; use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer};
use tracing_log::LogTracer; 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<Sink>(
name: String,
env_filter: String,
sink: Sink,
) -> impl Subscriber + Send + Sync
where
Sink: for<'a> MakeWriter<'a> + Send + Sync + 'static,
{
let env_filter = let env_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(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() Registry::default()
.with(env_filter) .with(env_filter)
.with(JsonStorageLayer) .with(JsonStorageLayer)

View File

@ -1,15 +1,32 @@
use mail_app::configuration::{get_configuration, DatabaseSettings}; use mail_app::configuration::{get_configuration, DatabaseSettings};
use mail_app::startup::run; 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 sqlx::{Executor, PgPool};
use std::net::TcpListener; use std::net::TcpListener;
use uuid::Uuid; 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 struct TestApp {
pub address: String, pub address: String,
pub db_pool: PgPool, pub db_pool: PgPool,
} }
async fn spawn_app() -> TestApp { 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 listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind to random port.");
let port = listener.local_addr().unwrap().port(); let port = listener.local_addr().unwrap().port();
let address = format!("http://127.0.0.1:{}", 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 { pub async fn configure_database(config: &DatabaseSettings) -> PgPool {
// Create Database // Create Database
let connection = PgPool::connect(&config.connection_string_without_db()) let connection = PgPool::connect(&config.connection_string_without_db().expose_secret())
.await .await
.expect("Failed to connect to Postgres."); .expect("Failed to connect to Postgres.");
connection connection
@ -39,7 +56,7 @@ pub async fn configure_database(config: &DatabaseSettings) -> PgPool {
.expect("Failed to create database."); .expect("Failed to create database.");
// Migrate Database // Migrate Database
let connection_pool = PgPool::connect(&config.connection_string()) let connection_pool = PgPool::connect(&config.connection_string().expose_secret())
.await .await
.expect("Failed to connect to Postgres."); .expect("Failed to connect to Postgres.");
sqlx::migrate!("./migrations") sqlx::migrate!("./migrations")