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

View File

@ -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"

View File

@ -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<String>,
pub port: u16,
pub host: String,
pub database_name: String,
@ -25,17 +27,24 @@ pub fn get_configuration() -> Result<Settings, config::ConfigError> {
}
impl DatabaseSettings {
pub fn connection_string(&self) -> String {
format!(
pub fn connection_string(&self) -> Secret<String> {
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<String> {
Secret::new(format!(
"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::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

View File

@ -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<FormData>, pool: web::Data<PgPool>) -> 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<FormData>, pool: web::Data<PgPool>) -> 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<FormData>, pool: web::Data<PgPool>) -> 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(())
}

View File

@ -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<Server, std::io::Error> {
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();

View File

@ -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<Sink>(
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)

View File

@ -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")