Skip to content

Commit

Permalink
test: ☑️ improve startup module testing
Browse files Browse the repository at this point in the history
  • Loading branch information
rodneylab committed Nov 14, 2024
1 parent 7139252 commit dc19fae
Show file tree
Hide file tree
Showing 12 changed files with 528 additions and 86 deletions.
329 changes: 315 additions & 14 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ strip = true

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
path = "src/lib.rs"

[[bin]]
path = "src/main.rs"
name = "axum-graphql"

[dependencies]
anyhow = "1.0.93"
async-graphql = "7.0.11"
Expand Down Expand Up @@ -56,5 +63,6 @@ insta = { version = "1.41.1", features = ["glob", "json", "redactions"] }
mime = "0.3.17"
once_cell = "1.20.2"
prometheus-parse = "0.2.5"
reqwest = { version = "0.12.8", features = ["json", "rustls-tls"] }
serde_json = "1.0"
tower = { version = "0.5.1", features = ['util'] }
10 changes: 10 additions & 0 deletions src/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ use sqlx::{
Sqlite, SqlitePool,
};

/// .
///
/// # Panics
///
/// Panics if unbale to create the database.
pub async fn create(db_url: &str) {
if Sqlite::database_exists(db_url).await.unwrap_or(false) {
tracing::info!("Database already exists");
Expand All @@ -15,6 +20,11 @@ pub async fn create(db_url: &str) {
}
}

/// .
///
/// # Panics
///
/// Panics if unable to run migrations successfully.
pub async fn run_migrations(db_pool: &SqlitePool) {
let migrations = std::path::PathBuf::from("migrations");

Expand Down
7 changes: 7 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#![warn(clippy::all, clippy::pedantic)]

pub mod database;
pub mod model;
pub mod observability;
pub mod routes;
pub mod startup;
8 changes: 1 addition & 7 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
#![warn(clippy::all, clippy::pedantic)]

mod database;
mod model;
mod observability;
mod routes;
mod startup;

use std::env;

use dotenvy::dotenv;

use crate::{
use axum_graphql::{
database::create as create_database,
observability::{
metrics::create_prometheus_recorder, tracing::create_tracing_subscriber_from_env,
Expand Down
12 changes: 9 additions & 3 deletions src/observability/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ use metrics_exporter_prometheus::{Matcher, PrometheusBuilder, PrometheusHandle};

const REQUEST_DURATION_METRIC_NAME: &str = "http_requests_duration_seconds";

pub(crate) fn create_prometheus_recorder() -> PrometheusHandle {
/// Creates a global Prometheus recorder.
///
/// # Panics
///
/// Panics if the recorder has already been initialised.
#[must_use]
pub fn create_prometheus_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,
];
Expand All @@ -26,7 +32,7 @@ pub(crate) fn create_prometheus_recorder() -> PrometheusHandle {
.expect("Could not install the Prometheus recorder, there might already be an instance running. It should only be started once.")
}

pub(crate) async fn track_metrics(req: Request, next: Next) -> impl IntoResponse {
pub async fn track(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()
Expand Down Expand Up @@ -125,7 +131,7 @@ mod tests {
let _ = Lazy::force(&METRICS);

// act
create_prometheus_recorder();
let _ = create_prometheus_recorder();

// assert
}
Expand Down
4 changes: 2 additions & 2 deletions src/observability/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
pub(crate) mod metrics;
pub(crate) mod tracing;
pub mod metrics;
pub mod tracing;
10 changes: 8 additions & 2 deletions src/observability/tracing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,17 @@ struct OpenTelemetryConfig {
tracing_service_name: String,
}

/// .
///
/// # Panics
///
/// Panics if `OPENTELEMETRY_ENABLED` environment variable exists and is not either `true` or
/// `false`.
pub fn create_tracing_subscriber_from_env() {
let opentelemetry_enabled: bool = env::var("OPENTELEMETRY_ENABLED")
.unwrap_or_else(|_| "false".into())
.parse()
.unwrap();
.expect("`OPENTELEMETRY_ENABLED` env variable should be either `true` or `false`");

if opentelemetry_enabled {
let config = get_opentelemetry_config_from_env();
Expand All @@ -32,7 +38,7 @@ pub fn create_tracing_subscriber_from_env() {
.with(tracing_subscriber::filter::LevelFilter::from_level(
Level::INFO,
))
.with(tracing_subscriber::fmt::layer())
.with(tracing_subscriber::fmt::layer().with_test_writer())
.with(OpenTelemetryLayer::new(tracer))
.init();

Expand Down
144 changes: 86 additions & 58 deletions src/startup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,88 @@ use tower_http::{compression::CompressionLayer, services::ServeDir, timeout::Tim
use crate::{
database::run_migrations,
model::get_schema,
observability::metrics::track_metrics,
observability::metrics::track as track_metrics,
routes::{graphql_handler, graphql_playground, health},
};

pub struct ApplicationRouters {
pub main_router: Router,
pub metrics_router: Router,
}

impl ApplicationRouters {
/// Build the main app and metics app routers. These routers can be used in unit tests.

Check warning on line 22 in src/startup.rs

View workflow job for this annotation

GitHub Actions / Spell Check with Typos

"metics" should be "metrics".
///
/// # Errors
/// Returns an error if the database is not reachable
///
/// This function will return an error if .
pub async fn build(
database_url: &str,
recorder_handle: PrometheusHandle,
) -> Result<Self, std::io::Error> {
Ok(Self {
main_router: main_router(database_url).await,
metrics_router: metrics_router(recorder_handle),
})
}
}

async fn shutdown_signal() {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};

#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("failed to install signal handler")
.recv()
.await;
};

#[cfg(not(unix))]
let terminate = std::future::pending::<()>();

tokio::select! {
() = ctrl_c => {
tracing::info!("Ctrl-C registered");
opentelemetry::global::shutdown_tracer_provider();
},
() = terminate => {
tracing::info!("Terminate registered");
opentelemetry::global::shutdown_tracer_provider();
},
}
}

pub struct Application {
main_server: Serve<Router, Router>,
metrics_server: Serve<Router, Router>,
pub main_server: Serve<Router, Router>,
pub metrics_server: Serve<Router, Router>,
}

impl Application {
/// Build an axum main app and a metrics app.
///
/// # Panics
/// Panics if the database is not reachable, if the main app or merics port is already in use.
///
/// # Errors
///
/// This function will return an error if the metric port is already in use.
pub async fn build(
database_url: &str,
recorder_handle: PrometheusHandle,
) -> Result<Self, std::io::Error> {
let ApplicationRouters {
main_router,
metrics_router,
} = ApplicationRouters::build(database_url, recorder_handle)
.await
.expect("database should be reachable");

let local_addr = "127.0.0.1:8000";
let main_listener = tokio::net::TcpListener::bind(local_addr)
.await
Expand All @@ -31,23 +99,25 @@ impl Application {
"Main app service listening on {}",
main_listener.local_addr().unwrap()
);
let main_server = run_main_server(main_listener, database_url).await;
let metrics_listener = tokio::net::TcpListener::bind("127.0.0.1:8001").await?;

let metrics_listener = tokio::net::TcpListener::bind("127.0.0.1:8001")
.await
.unwrap();
tracing::info!(
"Metrics service listening on {}",
metrics_listener.local_addr().unwrap()
);
let metrics_server = run_metrics_server(metrics_listener, recorder_handle);

Ok(Self {
main_server,
metrics_server,
main_server: axum::serve(main_listener, main_router),
metrics_server: axum::serve(metrics_listener, metrics_router),
})
}

/// Run both apps. Can be used in tests and when running the app in production.
///
/// # Errors
///
/// This function will return an error if the axum main server or metrics server returned an
/// error.
pub async fn run_until_stopped(self) -> Result<(), std::io::Error> {
let (main_server, metrics_server) = tokio::join!(
self.main_server.with_graceful_shutdown(shutdown_signal()),
Expand All @@ -61,6 +131,12 @@ impl Application {
}
}

/// Create the main app axum router.
///
/// # Panics
/// Panics when not able to reach the datase.
///
/// Panics if .
pub async fn main_router(database_url: &str) -> Router {
tracing::info!("Main app service starting");

Expand All @@ -87,51 +163,3 @@ pub async fn main_router(database_url: &str) -> Router {
pub fn metrics_router(recorder_handle: PrometheusHandle) -> Router {
Router::new().route("/metrics", get(move || ready(recorder_handle.render())))
}

async fn shutdown_signal() {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};

#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("failed to install signal handler")
.recv()
.await;
};

#[cfg(not(unix))]
let terminate = std::future::pending::<()>();

tokio::select! {
() = ctrl_c => {
tracing::info!("Ctrl-C registered");
opentelemetry::global::shutdown_tracer_provider();
},
() = terminate => {
tracing::info!("Terminate registered");
opentelemetry::global::shutdown_tracer_provider();
},
}
}

pub async fn run_main_server(
listener: tokio::net::TcpListener,
database_url: &str,
) -> Serve<Router, Router> {
let router = main_router(database_url).await;

axum::serve(listener, router)
}

pub fn run_metrics_server(
listener: tokio::net::TcpListener,
recorder_handle: PrometheusHandle,
) -> Serve<Router, Router> {
let router = metrics_router(recorder_handle);

axum::serve(listener, router)
}
6 changes: 6 additions & 0 deletions tests/api/helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
use metrics_exporter_prometheus::PrometheusHandle;
use once_cell::sync::Lazy;

use axum_graphql::observability::metrics::create_prometheus_recorder;

pub static METRICS: Lazy<PrometheusHandle> = Lazy::new(create_prometheus_recorder);
2 changes: 2 additions & 0 deletions tests/api/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
mod helpers;
mod startup;
Loading

0 comments on commit dc19fae

Please sign in to comment.