mirror of
https://github.com/kristoferssolo/yoda-web.git
synced 2026-03-22 00:36:27 +00:00
Initial commit
This commit is contained in:
108
src/config.rs
Normal file
108
src/config.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use secrecy::{ExposeSecret, Secret};
|
||||
use serde::Deserialize;
|
||||
use serde_aux::field_attributes::deserialize_number_from_string;
|
||||
use sqlx::{
|
||||
postgres::{PgConnectOptions, PgSslMode},
|
||||
ConnectOptions,
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Settings {
|
||||
pub database: DatabaseSettings,
|
||||
pub application: ApplicationSettings,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DatabaseSettings {
|
||||
pub username: String,
|
||||
pub password: Secret<String>,
|
||||
#[serde(deserialize_with = "deserialize_number_from_string")]
|
||||
pub port: u16,
|
||||
pub host: String,
|
||||
pub database_name: String,
|
||||
pub require_ssl: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ApplicationSettings {
|
||||
#[serde(deserialize_with = "deserialize_number_from_string")]
|
||||
pub port: u16,
|
||||
pub host: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Environment {
|
||||
Local,
|
||||
Production,
|
||||
}
|
||||
|
||||
pub fn get_config() -> Result<Settings, config::ConfigError> {
|
||||
let base_path = std::env::current_dir().expect("Failed to determine current directory");
|
||||
let config_directory = base_path.join("config");
|
||||
let env: Environment = std::env::var("APP_ENVIRONMENT")
|
||||
.unwrap_or_else(|_| "local".into())
|
||||
.try_into()
|
||||
.expect("Failed to parse APP_ENVIRONMENT");
|
||||
|
||||
let env_filename = format!("{}.toml", &env);
|
||||
|
||||
let settings = config::Config::builder()
|
||||
.add_source(config::File::from(config_directory.join("base.toml")))
|
||||
.add_source(config::File::from(config_directory.join(env_filename)))
|
||||
.add_source(
|
||||
config::Environment::with_prefix("APP")
|
||||
.prefix_separator("_")
|
||||
.separator("__"),
|
||||
)
|
||||
.build()?;
|
||||
settings.try_deserialize::<Settings>()
|
||||
}
|
||||
|
||||
impl DatabaseSettings {
|
||||
pub fn without_db(&self) -> PgConnectOptions {
|
||||
let ssl_mode = if self.require_ssl {
|
||||
PgSslMode::Require
|
||||
} else {
|
||||
PgSslMode::Prefer
|
||||
};
|
||||
|
||||
PgConnectOptions::new()
|
||||
.host(&self.host)
|
||||
.username(&self.username)
|
||||
.password(self.password.expose_secret())
|
||||
.port(self.port)
|
||||
.ssl_mode(ssl_mode)
|
||||
}
|
||||
|
||||
pub fn with_db(&self) -> PgConnectOptions {
|
||||
self.without_db()
|
||||
.database(&self.database_name)
|
||||
.log_statements(tracing_log::log::LevelFilter::Trace)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Environment {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Environment::Local => write!(f, "local"),
|
||||
Environment::Production => write!(f, "production"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for Environment {
|
||||
type Error = String;
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
match value.to_lowercase().as_str() {
|
||||
"local" => Ok(Self::Local),
|
||||
"production" => Ok(Self::Production),
|
||||
other => Err(format!(
|
||||
"{} is not supported environment. \
|
||||
Use either `local` or `production`.",
|
||||
other
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
1
src/domain/mod.rs
Normal file
1
src/domain/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
4
src/lib.rs
Normal file
4
src/lib.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod config;
|
||||
pub mod domain;
|
||||
pub mod routes;
|
||||
pub mod telemetry;
|
||||
21
src/main.rs
Normal file
21
src/main.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use yoda_web::{
|
||||
config::get_config,
|
||||
routes::route,
|
||||
telemetry::{get_subscriber, init_subscriber},
|
||||
};
|
||||
use sqlx::postgres::PgPoolOptions;
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), std::io::Error> {
|
||||
let subscriber = get_subscriber("yoda-web", "info", std::io::stdout);
|
||||
init_subscriber(subscriber);
|
||||
let config = get_config().expect("Failed to read configuation.");
|
||||
let pool = PgPoolOptions::new().connect_lazy_with(config.database.with_db());
|
||||
let addr = format!("{}:{}", config.application.host, config.application.port);
|
||||
let listener = TcpListener::bind(addr)
|
||||
.await
|
||||
.expect("Failed to bind port 8000.");
|
||||
|
||||
axum::serve(listener, route(pool)).await
|
||||
}
|
||||
5
src/routes/health_check.rs
Normal file
5
src/routes/health_check.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
use axum::{http::StatusCode, response::IntoResponse};
|
||||
|
||||
pub async fn health_check() -> impl IntoResponse {
|
||||
StatusCode::OK
|
||||
}
|
||||
49
src/routes/mod.rs
Normal file
49
src/routes/mod.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
mod health_check;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use axum::{
|
||||
body::Bytes,
|
||||
extract::MatchedPath,
|
||||
http::{HeaderMap, Request},
|
||||
response::Response,
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
|
||||
pub use health_check::*;
|
||||
use sqlx::PgPool;
|
||||
use tower_http::{classify::ServerErrorsFailureClass, trace::TraceLayer};
|
||||
use tracing::{info_span, Span};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub fn route(pool: PgPool) -> Router {
|
||||
Router::new()
|
||||
.route("/health_check", get(health_check))
|
||||
.with_state(pool)
|
||||
.layer(
|
||||
TraceLayer::new_for_http()
|
||||
.make_span_with(|request: &Request<_>| {
|
||||
let matched_path = request
|
||||
.extensions()
|
||||
.get::<MatchedPath>()
|
||||
.map(MatchedPath::as_str);
|
||||
info_span!(
|
||||
"http-request",
|
||||
method = ?request.method(),
|
||||
matched_path,
|
||||
some_other_field = tracing::field::Empty,
|
||||
request_id=%Uuid::new_v4(),
|
||||
)
|
||||
})
|
||||
.on_request(|_request: &Request<_>, _span: &Span| {})
|
||||
.on_response(|_response: &Response<_>, _latency: Duration, _span: &Span| {})
|
||||
.on_body_chunk(|_chunk: &Bytes, _latency: Duration, _span: &Span| {})
|
||||
.on_eos(
|
||||
|_trailers: Option<&HeaderMap>, _stream_duration: Duration, _span: &Span| {},
|
||||
)
|
||||
.on_failure(
|
||||
|_error: ServerErrorsFailureClass, _latency: Duration, _span: &Span| {},
|
||||
),
|
||||
)
|
||||
}
|
||||
25
src/telemetry.rs
Normal file
25
src/telemetry.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use tracing::{subscriber::set_global_default, Subscriber};
|
||||
use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer};
|
||||
use tracing_log::LogTracer;
|
||||
use tracing_subscriber::{fmt::MakeWriter, layer::SubscriberExt, EnvFilter, Registry};
|
||||
|
||||
pub fn get_subscriber<Sink>(
|
||||
name: &str,
|
||||
env_filter: &str,
|
||||
sink: Sink,
|
||||
) -> impl Subscriber + Sync + Send
|
||||
where
|
||||
Sink: for<'a> MakeWriter<'a> + Send + Sync + 'static,
|
||||
{
|
||||
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| env_filter.into());
|
||||
let formatting_layer = BunyanFormattingLayer::new(name.into(), sink);
|
||||
Registry::default()
|
||||
.with(env_filter)
|
||||
.with(JsonStorageLayer)
|
||||
.with(formatting_layer)
|
||||
}
|
||||
|
||||
pub fn init_subscriber(subscriber: impl Subscriber + Sync + Send) {
|
||||
LogTracer::init().expect("Failed to set logger");
|
||||
set_global_default(subscriber).expect("Failed to set subscriber.");
|
||||
}
|
||||
Reference in New Issue
Block a user