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",
|
||||
"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",
|
||||
]
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
))
|
||||
}
|
||||
}
|
||||
|
10
src/main.rs
10
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
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
|
Loading…
Reference in New Issue
Block a user