Complete Chapter 4
+ Add in Tracing Logging + Logs are JSON formatted with events + Tests are confirmed working
This commit is contained in:
parent
2839aec040
commit
c88d909639
56
Cargo.lock
generated
56
Cargo.lock
generated
@ -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",
|
||||||
]
|
]
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
10
src/main.rs
10
src/main.rs
@ -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
|
||||||
|
@ -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(())
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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)
|
||||||
|
@ -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")
|
||||||
|
Loading…
Reference in New Issue
Block a user