feat(telemetry): logging implementation
continuous-integration/drone/push Build is passing Details

main
flavien 2022-11-13 23:14:59 +01:00
parent d13de9acbb
commit 9679435a40
10 changed files with 79 additions and 39 deletions

View File

@ -20,9 +20,10 @@ tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
serde = { version = "1", features = ["derive"]}
validator = { version = "0.16", features = ["derive"] }
actix-web-validator = "5.0.1"
log = "0.4"
env_logger = "0.9.3"
config = "0.13"
tracing = { version = "0.1", features = ["log"] }
tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] }
tracing-bunyan-formatter = "0.3"
tracing-log = "0.1"
uuid = { version = "1", features = ["v4"] }
chrono = { version = "0.4.22", default-features = false, features = ["clock"] }
dotenv = "0.15.0"

View File

@ -1,8 +1,6 @@
# Zero_to_prod_in_rust
### TODO
- configure db from env
- get port from args with default
- continue book...
- build ci
- release ci

View File

@ -1,6 +1,4 @@
//! src/configuration.rs
use std::process;
use dotenv::dotenv;
use serde::Deserialize;
use sqlx::{Connection, Executor, PgConnection, PgPool};
@ -15,15 +13,11 @@ pub struct DatabaseConfig {
pub name: String,
}
pub fn get_db_config() -> DatabaseConfig {
pub fn parse_db_config() -> DatabaseConfig {
dotenv().ok();
match envy::prefixed("DB_").from_env::<DatabaseConfig>() {
Ok(config) => config,
Err(error) => {
eprintln!("Database config environment variables: {}", error);
process::exit(1)
}
}
envy::prefixed("DB_")
.from_env::<DatabaseConfig>()
.expect("Database config environment variables")
}
impl DatabaseConfig {
@ -46,22 +40,21 @@ impl DatabaseConfig {
pub async fn test_connection_pool(&mut self) -> PgPool {
let test_db_name = Uuid::new_v4().to_string();
let server_connection_string = self.server_connection_string();
let create_db_query = format!(r#"CREATE DATABASE "{}";"#, test_db_name);
let mut connection = PgConnection::connect(&self.server_connection_string())
PgConnection::connect(&server_connection_string)
.await
.expect("Failed to connect to Postgres");
connection
.execute(format!(r#"CREATE DATABASE "{}";"#, test_db_name).as_str())
.expect("Failed to connect to Postgres")
.execute(create_db_query.as_str())
.await
.expect("Failed to create database.");
let connection_pool = PgPool::connect(&format!(
"{}/{}",
self.server_connection_string(),
test_db_name
))
.await
.expect("Failed to connect to Postgres.");
let test_db_connection_string = format!("{}/{}", server_connection_string, test_db_name);
let connection_pool = PgPool::connect(&test_db_connection_string)
.await
.expect("Failed to connect to Postgres.");
sqlx::migrate!("./migrations")
.run(&connection_pool)
.await

View File

@ -2,3 +2,4 @@
pub mod configuration;
pub mod routes;
pub mod startup;
pub mod telemetry;

View File

@ -1,7 +1,8 @@
use clap::{command, Parser};
use std::net::TcpListener;
use zero2prod::configuration::get_db_config;
use zero2prod::configuration::parse_db_config;
use zero2prod::startup::run;
use zero2prod::telemetry::{get_subscriber, init_subscriber};
/// Zero2prod newsletter server
#[derive(Parser, Debug)]
@ -14,10 +15,13 @@ struct Args {
#[tokio::main]
async fn main() -> std::io::Result<()> {
env_logger::init();
let subscriber = get_subscriber("zero2prod".into(), "info".into());
init_subscriber(subscriber);
let args = Args::parse();
let connection_pool = get_db_config().connection_pool().await;
let address = format!("127.0.0.1:{}", args.port);
let listener = TcpListener::bind(address)?;
let connection_pool = parse_db_config().connection_pool().await;
run(listener, connection_pool)?.await
}

View File

@ -1,8 +1,6 @@
use actix_web::{get, HttpResponse, Responder};
use log::info;
#[get("/health_check")]
pub async fn health_check() -> impl Responder {
info!("GET /health_check");
HttpResponse::Ok()
}

View File

@ -1,9 +1,9 @@
use actix_web::{post, web, HttpResponse, Responder};
use actix_web_validator::Form;
use chrono::Utc;
use log::info;
use serde::Deserialize;
use sqlx::PgPool;
use sqlx::{Error, PgPool};
use tracing::Instrument;
use uuid::Uuid;
use validator::Validate;
@ -20,9 +20,28 @@ pub struct FormData {
name: String,
}
// TO EXTRACT WITH OTHERS GENERICS DATABASE OR ERRORS RELATED FUNCTIONS
fn query_error_is_duplicate_key(error: Error) -> bool {
match error {
Error::Database(db_error) => match db_error.code() {
Some(code) => code == "23505",
_ => false,
},
_ => false,
}
}
#[post("/subscriptions")]
pub async fn subscriptions(form: Form<FormData>, pool: web::Data<PgPool>) -> impl Responder {
info!("POST /subscriptions {:?}", form.0);
let request_id = Uuid::new_v4();
let request_span = tracing::info_span!(
"Adding a new subscriber",
%request_id,
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 in the database");
match sqlx::query!(
r#"
INSERT INTO subscriptions (id, email, name, subscribed_at) VALUES ($1, $2, $3, $4)
@ -33,11 +52,15 @@ pub async fn subscriptions(form: Form<FormData>, pool: web::Data<PgPool>) -> imp
Utc::now()
)
.execute(pool.get_ref())
.instrument(query_span)
.await
{
Err(e) => {
println!("Failed to execute query: {}", e);
HttpResponse::InternalServerError()
tracing::error!("Failed to execute query: {:?}", e);
match query_error_is_duplicate_key(e) {
true => HttpResponse::Conflict(),
false => HttpResponse::InternalServerError(),
}
}
_ => HttpResponse::Created(),
}

View File

@ -1,4 +1,5 @@
use crate::routes::{health_check, subscriptions};
use actix_web::middleware::Logger;
use actix_web::{dev::Server, web, App, HttpServer};
use sqlx::PgPool;
use std::net::TcpListener;
@ -7,6 +8,7 @@ pub fn run(listener: TcpListener, db_pool: PgPool) -> Result<Server, std::io::Er
let db_pool = web::Data::new(db_pool);
let server = HttpServer::new(move || {
App::new()
.wrap(Logger::default())
.service(health_check)
.service(subscriptions)
.app_data(db_pool.clone())

21
src/telemetry.rs Normal file
View File

@ -0,0 +1,21 @@
//! src/telemetry.rs
use tracing::subscriber::set_global_default;
use tracing::Subscriber;
use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer};
use tracing_log::LogTracer;
use tracing_subscriber::{layer::SubscriberExt, EnvFilter, Registry};
pub fn get_subscriber(name: String, env_filter: String) -> impl Subscriber + Send + Sync {
let env_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(env_filter));
let formatting_layer = BunyanFormattingLayer::new(name, std::io::stdout);
Registry::default()
.with(env_filter)
.with(JsonStorageLayer)
.with(formatting_layer)
}
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");
}

View File

@ -1,6 +1,6 @@
use sqlx::PgPool;
use std::net::TcpListener;
use zero2prod::configuration::get_db_config;
use zero2prod::configuration::parse_db_config;
use zero2prod::startup::run;
pub struct TestApp {
@ -13,7 +13,7 @@ async fn spawn_app() -> TestApp {
let port = listener.local_addr().unwrap().port();
let address = format!("http://127.0.0.1:{}", port);
let connection_pool = get_db_config().test_connection_pool().await;
let connection_pool = parse_db_config().test_connection_pool().await;
let server = run(listener, connection_pool.clone()).expect("Failed to bind address");
let _ = tokio::spawn(server);
@ -84,7 +84,6 @@ async fn subscribe_returns_a_400_when_data_is_missing() {
assert_eq!(
400,
response.status().as_u16(),
// Additional customised error message on test failure
"The API did not fail with 400 Bad Request when the payload was {}.",
error_message
);