Skip to content

Commit

Permalink
Add tracing
Browse files Browse the repository at this point in the history
  • Loading branch information
vjousse committed Apr 29, 2022
1 parent 1f79ad5 commit 8e51a75
Show file tree
Hide file tree
Showing 10 changed files with 378 additions and 129 deletions.
292 changes: 222 additions & 70 deletions Cargo.lock

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,15 @@ serde = { version = "1", features = ["derive"]}
chrono = "0.4.15"
sqlx = { version = "0.5.5", default-features = false, features = [ "runtime-actix-rustls", "macros", "postgres", "uuid", "chrono", "migrate", "offline"] }
uuid = { version = "0.8.1", features = ["v4", "serde"] }
env_logger = "0.9"
log = "0.4"
tracing = { version = "0.1", features = ["log"] }
tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] }
tracing-bunyan-formatter = "0.3"
tracing-log = "0.1"
secrecy = { version = "0.8", features = ["serde"] }
tracing-actix-web = "0.5"

[dev-dependencies]
actix-rt = "2"
reqwest = "0.11"
tokio = "1"
once_cell = "1"
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Running

# Sqlx
First, export the database url:

export DATABASE_URL=postgres://postgres:password@localhost:5432/newsletter
sqlx migrate add create_subscriptions_table

Then, start the database using docker:

./scripts/init_db.sh

Finally, run the project:

cargo run

To enable nice display of logs:

RUST_LOG=trace cargo run | bunyan
28 changes: 19 additions & 9 deletions src/configuration.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
use secrecy::ExposeSecret;
use secrecy::Secret;

#[derive(serde::Deserialize)]
pub struct Settings {
pub database: DatabaseSettings,
Expand All @@ -7,7 +10,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,
Expand All @@ -24,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
))
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![allow(clippy::toplevel_ref_arg)]
pub mod configuration;
pub mod routes;
pub mod startup;
pub mod telemetry;
13 changes: 8 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
use env_logger::Env;
use secrecy::ExposeSecret;
use sqlx::PgPool;
use std::net::TcpListener;
use zero2prod::configuration::get_configuration;
use zero2prod::startup::run;
use zero2prod::telemetry::{get_subscriber, init_subscriber};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
let subscriber = get_subscriber("zero2prod".into(), "info".into(), std::io::stdout);
init_subscriber(subscriber);

// Panic if we can't read configuration
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
Expand Down
67 changes: 32 additions & 35 deletions src/routes/subscriptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,48 +8,45 @@ pub struct FormData {
email: String,
name: String,
}

#[tracing::instrument(
name = "Adding a new subscriber",
skip(form, pool),
fields(
subscriber_email = %form.email,
subscriber_name= %form.name
)
)]
pub async fn subscribe(form: web::Form<FormData>, pool: web::Data<PgPool>) -> HttpResponse {
let request_id = Uuid::new_v4();
log::info!(
"request_id {} - Adding '{}' '{}' as a new subscriber.",
request_id,
form.email,
form.name
);
log::info!(
"request_id {} - Saving new subscriber details in the database",
request_id
);
match insert_subscriber(&pool, &form).await {
Ok(_) => HttpResponse::Ok().finish(),
Err(_) => HttpResponse::InternalServerError().finish(),
}
}

match sqlx::query!(
#[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)
"#,
INSERT INTO subscriptions (id, email, name, subscribed_at)
VALUES ($1, $2, $3, $4)
"#,
Uuid::new_v4(),
form.email,
form.name,
Utc::now()
)
// We use `get_ref` to get an immutable reference to the `PgConnection`
// wrapped by `web::Data`.
.execute(pool.as_ref())
.execute(pool)
.await
{
Ok(_) => {
log::info!(
"request_id {} - New subscriber details have been saved",
request_id
);
HttpResponse::Ok().finish()
}
Err(e) => {
log::error!(
"request_id {} - Failed to execute query: {:?}",
request_id,
e
);
HttpResponse::InternalServerError().finish()
}
}
.map_err(|e| {
tracing::error!("Failed to execute query: {:?}", e);
e
// Using the `?` operator to return early
// if the function failed, returning a sqlx::Error
// We will talk about error handling in depth later!
})?;
Ok(())
}
4 changes: 2 additions & 2 deletions src/startup.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
use crate::routes::{health_check, subscribe};
use actix_web::dev::Server;
use actix_web::middleware::Logger;
use actix_web::{web, 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> {
// Wrap the connection in a smart pointer
let db_pool = web::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))
// Get a pointer copy and attach it to the application state
Expand Down
45 changes: 45 additions & 0 deletions src/telemetry.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use tracing::subscriber::set_global_default;
use tracing::Subscriber;
use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer};
use tracing_log::LogTracer;
use tracing_subscriber::fmt::MakeWriter;
use tracing_subscriber::{layer::SubscriberExt, EnvFilter, Registry};

/// Compose multiple layers into a `tracing`'s subscriber.
///
/// # Implementation Notes
///
/// We are using `impl Subscriber` as return type to avoid having to
/// spell out the actual type of the returned subscriber, which is
/// indeed quite complex.
/// We need to explicitly call out that the returned subscriber is
/// `Send` and `Sync` to make it possible to pass it to `init_subscriber`
/// later on.
pub fn get_subscriber<Sink>(
name: String,
env_filter: String,
sink: Sink,
) -> impl Subscriber + Sync + Send
where
// This "weird" syntax is a higher-ranked trait bound (HRTB)
// It basically means that Sink implements the `MakeWriter`
// trait for all choices of the lifetime parameter `'a`
// Check out https://doc.rust-lang.org/nomicon/hrtb.html
// for more details.
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, sink);
Registry::default()
.with(env_filter)
.with(JsonStorageLayer)
.with(formatting_layer)
}
/// Register a subscriber as global default to process span data.
///
/// It should only be called once!
pub fn init_subscriber(subscriber: impl Subscriber + Send + Sync) {
LogTracer::init().expect("Failed to set logger");
set_global_default(subscriber).expect("Failed to set subscriber");
}
31 changes: 27 additions & 4 deletions tests/health_check.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,37 @@
use once_cell::sync::Lazy;
use secrecy::ExposeSecret;
use sqlx::{Connection, Executor, PgConnection, PgPool};
use std::net::TcpListener;
use uuid::Uuid;
use zero2prod::configuration::{get_configuration, DatabaseSettings};
use zero2prod::startup::run;
use zero2prod::telemetry::{get_subscriber, init_subscriber};

pub struct TestApp {
pub address: String,
pub db_pool: PgPool,
}

static TRACING: Lazy<()> = Lazy::new(|| {
let default_filter_level = "info".to_string();
let subscriber_name = "test".to_string();
// We cannot assign the output of `get_subscriber` to a variable based on the value of `TEST_LOG`
// because the sink is part of the type returned by `get_subscriber`, therefore they are not the
// same type. We could work around it, but this is the most straight-forward way of moving forward.
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 {
// The first time `initialize` is invoked the code in `TRACING` is executed.
// All other invocations will instead skip execution.
Lazy::force(&TRACING);

let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind random port");
// We retrieve the port assigned to us by the OS
let port = listener.local_addr().unwrap().port();
Expand All @@ -29,15 +51,16 @@ async fn spawn_app() -> TestApp {

pub async fn configure_database(config: &DatabaseSettings) -> PgPool {
// Create database
let mut connection = PgConnection::connect(&config.connection_string_without_db())
.await
.expect("Failed to connect to Postgres");
let mut connection =
PgConnection::connect(&config.connection_string_without_db().expose_secret())
.await
.expect("Failed to connect to Postgres");
connection
.execute(format!(r#"CREATE DATABASE "{}";"#, config.database_name).as_str())
.await
.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")
Expand Down

0 comments on commit 8e51a75

Please sign in to comment.