diff --git a/Cargo.lock b/Cargo.lock index a76ed99..ad14f16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -141,7 +141,7 @@ dependencies = [ "serde_urlencoded", "static_assertions_next", "tempfile", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -176,7 +176,7 @@ dependencies = [ "quote", "strum", "syn", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -364,6 +364,7 @@ dependencies = [ "opentelemetry-semantic-conventions", "opentelemetry_sdk", "prometheus-parse", + "reqwest", "serde", "serde_json", "sqlx", @@ -419,7 +420,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 1.1.0", "shlex", "syn", "which", @@ -521,6 +522,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.38" @@ -869,6 +876,21 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1007,8 +1029,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -1066,7 +1090,7 @@ dependencies = [ "pest_derive", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -1237,6 +1261,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", + "webpki-roots", ] [[package]] @@ -1252,6 +1277,22 @@ dependencies = [ "tower-service", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.10" @@ -1664,7 +1705,7 @@ dependencies = [ "metrics", "metrics-util", "quanta", - "thiserror", + "thiserror 1.0.69", "tokio", "tracing", ] @@ -1749,6 +1790,23 @@ dependencies = [ "version_check", ] +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nom" version = "7.1.3" @@ -1831,12 +1889,50 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "openssl" +version = "0.10.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "openssl-probe" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "opentelemetry" version = "0.26.0" @@ -1848,7 +1944,7 @@ dependencies = [ "js-sys", "once_cell", "pin-project-lite", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -1864,7 +1960,7 @@ dependencies = [ "opentelemetry-proto", "opentelemetry_sdk", "prost", - "thiserror", + "thiserror 1.0.69", "tokio", "tonic", ] @@ -1903,7 +1999,7 @@ dependencies = [ "percent-encoding", "rand", "serde_json", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-stream", ] @@ -1971,7 +2067,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879952a81a83930934cbf1786752d6dedc3b1f29e8f8fb2ad1d0a36f377cf442" dependencies = [ "memchr", - "thiserror", + "thiserror 1.0.69", "ucd-trie", ] @@ -2161,6 +2257,58 @@ dependencies = [ "winapi", ] +[[package]] +name = "quinn" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.0.0", + "rustls", + "socket2", + "thiserror 2.0.3", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" +dependencies = [ + "bytes", + "getrandom", + "rand", + "ring", + "rustc-hash 2.0.0", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.3", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a626c6807713b15cac82a6acaccd6043c9a5408c24baae07611fec3f243da" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" version = "1.0.37" @@ -2262,6 +2410,54 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "reqwest" +version = "0.12.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "windows-registry", +] + [[package]] name = "ring" version = "0.17.8" @@ -2309,6 +2505,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + [[package]] name = "rustix" version = "0.38.40" @@ -2364,6 +2566,9 @@ name = "rustls-pki-types" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" +dependencies = [ + "web-time", +] [[package]] name = "rustls-webpki" @@ -2662,7 +2867,7 @@ dependencies = [ "sha2", "smallvec", "sqlformat", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-stream", "tracing", @@ -2746,7 +2951,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 1.0.69", "tracing", "whoami", ] @@ -2784,7 +2989,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 1.0.69", "tracing", "whoami", ] @@ -2891,6 +3096,9 @@ name = "sync_wrapper" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -2903,6 +3111,27 @@ dependencies = [ "syn", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.14.0" @@ -2922,7 +3151,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" +dependencies = [ + "thiserror-impl 2.0.3", ] [[package]] @@ -2936,6 +3174,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.8" @@ -3000,6 +3249,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.0" @@ -3270,7 +3529,7 @@ dependencies = [ "log", "rand", "sha1", - "thiserror", + "thiserror 1.0.69", "utf-8", ] @@ -3435,6 +3694,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.95" @@ -3555,6 +3826,36 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index 6fe5cc1..594da7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" @@ -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'] } diff --git a/src/database.rs b/src/database.rs index d7b7c07..9c113dc 100644 --- a/src/database.rs +++ b/src/database.rs @@ -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"); @@ -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"); diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..74251a9 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,7 @@ +#![warn(clippy::all, clippy::pedantic)] + +pub mod database; +pub mod model; +pub mod observability; +pub mod routes; +pub mod startup; diff --git a/src/main.rs b/src/main.rs index 649d742..b4cfd2c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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, diff --git a/src/observability/metrics.rs b/src/observability/metrics.rs index e6ecb4b..416fcd3 100644 --- a/src/observability/metrics.rs +++ b/src/observability/metrics.rs @@ -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, ]; @@ -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::() { matched_path.as_str().to_owned() @@ -125,7 +131,7 @@ mod tests { let _ = Lazy::force(&METRICS); // act - create_prometheus_recorder(); + let _ = create_prometheus_recorder(); // assert } diff --git a/src/observability/mod.rs b/src/observability/mod.rs index 0671195..55d406c 100644 --- a/src/observability/mod.rs +++ b/src/observability/mod.rs @@ -1,2 +1,2 @@ -pub(crate) mod metrics; -pub(crate) mod tracing; +pub mod metrics; +pub mod tracing; diff --git a/src/observability/tracing.rs b/src/observability/tracing.rs index 1987017..d5b9e9f 100644 --- a/src/observability/tracing.rs +++ b/src/observability/tracing.rs @@ -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(); @@ -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(); diff --git a/src/startup.rs b/src/startup.rs index fa8c7c3..1a08354 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -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. + /// + /// # 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 { + 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, - metrics_server: Serve, + pub main_server: Serve, + pub metrics_server: Serve, } 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 { + 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 @@ -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()), @@ -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"); @@ -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 { - let router = main_router(database_url).await; - - axum::serve(listener, router) -} - -pub fn run_metrics_server( - listener: tokio::net::TcpListener, - recorder_handle: PrometheusHandle, -) -> Serve { - let router = metrics_router(recorder_handle); - - axum::serve(listener, router) -} diff --git a/tests/api/helpers.rs b/tests/api/helpers.rs new file mode 100644 index 0000000..a65df73 --- /dev/null +++ b/tests/api/helpers.rs @@ -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 = Lazy::new(create_prometheus_recorder); diff --git a/tests/api/main.rs b/tests/api/main.rs new file mode 100644 index 0000000..08517fc --- /dev/null +++ b/tests/api/main.rs @@ -0,0 +1,2 @@ +mod helpers; +mod startup; diff --git a/tests/api/startup.rs b/tests/api/startup.rs new file mode 100644 index 0000000..9f7a819 --- /dev/null +++ b/tests/api/startup.rs @@ -0,0 +1,74 @@ +use axum::{ + body::Body, + http::{Request, StatusCode}, +}; +use metrics_exporter_prometheus::PrometheusHandle; +use once_cell::sync::Lazy; +use reqwest::Client; +use tower::ServiceExt; + +use crate::helpers::METRICS; + +use axum_graphql::{ + observability::tracing::create_tracing_subscriber_from_env, + startup::{Application, ApplicationRouters}, +}; + +#[tokio::test] +async fn aplication_router_build_successfully_creates_main_and_metrics_routers() { + // arrange + let database_url = "sqlite://:memory:"; + let recorder_handle: PrometheusHandle = Lazy::::force(&METRICS).clone(); + + // act + let routers = ApplicationRouters::build(database_url, recorder_handle.clone()) + .await + .unwrap(); + let ApplicationRouters { + main_router, + metrics_router, + } = routers; + let main_server_response = main_router + .oneshot(Request::get("/").body(Body::empty()).unwrap()) + .await + .unwrap(); + let metrics_server_response = metrics_router + .oneshot(Request::get("/metrics").body(Body::empty()).unwrap()) + .await + .unwrap(); + + // assert + assert_eq!(main_server_response.status(), StatusCode::OK); + assert_eq!(metrics_server_response.status(), StatusCode::OK); +} + +#[tokio::test] +async fn aplication_build_successfully_creates_main_and_metrics_servers() { + // arrange + let database_url = "sqlite://:memory:"; + let recorder_handle: PrometheusHandle = Lazy::::force(&METRICS).clone(); + let client = Client::builder() + .timeout(std::time::Duration::from_millis(1_000)) + .build() + .unwrap(); + + // act + + let app = Application::build(database_url, recorder_handle.clone()) + .await + .unwrap(); + + #[expect(clippy::let_underscore_future)] + let _ = tokio::spawn(app.run_until_stopped()); + + let main_server_response = client.get("http://localhost:8000").send().await.unwrap(); + let metrics_server_response = client + .get("http://localhost:8001/metrics") + .send() + .await + .unwrap(); + + // assert + assert_eq!(main_server_response.status(), StatusCode::OK); + assert_eq!(metrics_server_response.status(), StatusCode::OK); +}