Lines
0 %
Functions
Branches
100 %
#[macro_use]
extern crate rust_i18n;
i18n!("locales", fallback = "en");
mod config;
mod files;
mod handler;
mod jwt_auth;
mod model;
mod pages;
mod redirect_middleware;
mod response;
mod route;
mod token;
use axum::{
Router,
extract::{MatchedPath, Request},
middleware::{self, Next},
response::IntoResponse,
routing::get,
};
use axum::http::{
HeaderValue, Method,
header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE},
use metrics_exporter_prometheus::{Matcher, PrometheusBuilder, PrometheusHandle};
use redis::Client;
use std::sync::Arc;
use std::{future::ready, time::Instant};
use tower_http::{cors::CorsLayer, services::ServeDir};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use config::Config;
use route::{
create_accounts_router, create_api_router, create_pages_router, create_tags_router,
create_transactions_router,
pub struct AppState {
conf: Config,
redis_client: Client,
frac: i64,
}
fn metrics_app() -> Router {
let recorder_handle = setup_metrics_recorder();
Router::new().route("/metrics", get(move || ready(recorder_handle.render())))
fn setup_metrics_recorder() -> PrometheusHandle {
const EXPONENTIAL_SECONDS: &[f64] = &[
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
];
PrometheusBuilder::new()
.set_buckets_for_metric(
Matcher::Full("http_requests_duration_seconds".to_string()),
EXPONENTIAL_SECONDS,
)
.unwrap()
.install_recorder()
async fn track_metrics(req: Request, next: Next) -> impl IntoResponse {
let start = Instant::now();
let path = if let Some(matched_path) = req.extensions().get::<MatchedPath>() {
matched_path.as_str().to_owned()
} else {
req.uri().path().to_owned()
let method = req.method().clone();
let response = next.run(req).await;
let latency = start.elapsed().as_secs_f64();
let status = response.status().as_u16().to_string();
let labels = [
("method", method.to_string()),
("path", path),
("status", status),
metrics::counter!("http_requests_total", &labels).increment(1);
metrics::histogram!("http_requests_duration_seconds", &labels).record(latency);
response
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "with_axum_htmx_askama=debug".into()),
.with(tracing_subscriber::fmt::layer())
.try_init()
.expect("Failed to set tracing subscriber");
let conf = Config::init().await.unwrap();
log::debug!("Config ready");
let redis_client = Client::open(conf.redis_url.clone())
.and_then(|client| {
client.get_connection().map(|_| {
log::debug!(
"Connection to the redis db {} successful!",
client.get_connection_info().redis.db
);
client
})
.unwrap_or_else(|e| {
log::error!("Error connecting to Redis: {e}");
std::process::exit(1);
});
let cors = CorsLayer::new()
.allow_origin(conf.site_url.parse::<HeaderValue>()?)
.allow_methods([Method::GET, Method::POST, Method::PATCH, Method::DELETE])
.allow_credentials(true)
.allow_headers([AUTHORIZATION, ACCEPT, CONTENT_TYPE]);
let state = Arc::new(AppState {
conf,
redis_client: redis_client.clone(),
frac: 0,
let router = Router::new()
.route("/", get(pages::index))
.route("/register", get(pages::register))
.route("/login", get(pages::login))
.merge(create_pages_router(state.clone()))
.merge(create_accounts_router(state.clone()))
.merge(create_transactions_router(state.clone()))
.merge(create_tags_router(state.clone()))
.nest("/api", create_api_router(state.clone()))
.nest_service(
"/static",
ServeDir::new(std::env::var("STATIC_PATH").unwrap_or("web/static".to_string())),
.with_state(state)
.layer(cors)
.route_layer(middleware::from_fn(track_metrics));
let app = metrics_app();
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
let metrics_listener = tokio::net::TcpListener::bind("0.0.0.0:3001").await?;
let (_main_server, _metrics_server) = tokio::join!(
run_server(metrics_listener, app),
run_server(listener, router)
Ok(())
async fn run_server(listener: tokio::net::TcpListener, app: Router) {
axum::serve(listener, app).await.unwrap();