From c8602361c561e88b30eb7bebbe22fe27d14dd0c6 Mon Sep 17 00:00:00 2001 From: Spencer Ferris <3319370+spencewenski@users.noreply.github.com> Date: Wed, 9 Oct 2024 13:42:41 -0700 Subject: [PATCH] feat: Add `SmtpHealthCheck` (#396) --- examples/full/Cargo.toml | 2 +- src/api/core/health.rs | 33 ++++++++++- src/config/health_check/default.toml | 2 + src/config/health_check/mod.rs | 4 ++ .../roadster__config__tests__test.snap | 2 + src/health_check/default.rs | 8 ++- src/health_check/email/mod.rs | 2 + src/health_check/email/smtp.rs | 57 +++++++++++++++++++ src/health_check/mod.rs | 2 + ...ult__tests__default_middleware@case_2.snap | 1 + 10 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 src/health_check/email/mod.rs create mode 100644 src/health_check/email/smtp.rs diff --git a/examples/full/Cargo.toml b/examples/full/Cargo.toml index edfd7f5e..9ff6d2c3 100644 --- a/examples/full/Cargo.toml +++ b/examples/full/Cargo.toml @@ -38,7 +38,7 @@ rusty-sidekiq = { workspace = true, default-features = false } serde = { workspace = true, features = ["derive"] } # Email -lettre = { workspace = true } +lettre = { workspace = true, features = ["pool"] } [build-dependencies] tonic-build = { workspace = true } diff --git a/src/api/core/health.rs b/src/api/core/health.rs index fedff0b2..ca675cea 100644 --- a/src/api/core/health.rs +++ b/src/api/core/health.rs @@ -3,7 +3,7 @@ use crate::error::RoadsterResult; use crate::health_check::{CheckResponse, ErrorData, HealthCheck, Status}; #[cfg(feature = "open-api")] use aide::OperationIo; -#[cfg(feature = "sidekiq")] +#[cfg(any(feature = "sidekiq", feature = "email-smtp"))] use anyhow::anyhow; use axum::extract::FromRef; use futures::future::join_all; @@ -135,6 +135,37 @@ async fn ping_db(db: &DatabaseConnection, duration: Option) -> Roadste Ok(()) } +#[cfg(feature = "email-smtp")] +pub(crate) async fn smtp_health(context: &AppContext, duration: Option) -> CheckResponse { + let timer = Instant::now(); + let status = match ping_smtp(context.mailer(), duration).await { + Ok(_) => Status::Ok, + Err(err) => Status::Err(ErrorData::builder().msg(err.to_string()).build()), + }; + let timer = timer.elapsed(); + CheckResponse::builder() + .status(status) + .latency(timer) + .build() +} + +#[cfg(feature = "email-smtp")] +async fn ping_smtp( + mailer: &lettre::SmtpTransport, + duration: Option, +) -> RoadsterResult<()> { + let connected = if let Some(duration) = duration { + timeout(duration, async { mailer.test_connection() }).await?? + } else { + mailer.test_connection()? + }; + if connected { + Ok(()) + } else { + Err(anyhow!("Not connected to the SMTP server").into()) + } +} + #[cfg(feature = "sidekiq")] #[instrument(skip_all)] pub(crate) async fn redis_health( diff --git a/src/config/health_check/default.toml b/src/config/health_check/default.toml index dc97deed..a471783f 100644 --- a/src/config/health_check/default.toml +++ b/src/config/health_check/default.toml @@ -6,3 +6,5 @@ cli = 10000 [health-check.database] [health-check.sidekiq] + +[health-check.smtp] diff --git a/src/config/health_check/mod.rs b/src/config/health_check/mod.rs index ecd29a1e..341c4ce1 100644 --- a/src/config/health_check/mod.rs +++ b/src/config/health_check/mod.rs @@ -30,6 +30,10 @@ pub struct HealthCheck { #[validate(nested)] pub sidekiq: HealthCheckConfig<()>, + #[cfg(feature = "email-smtp")] + #[validate(nested)] + pub smtp: HealthCheckConfig<()>, + /// Allows providing configs for custom health checks. Any configs that aren't pre-defined above /// will be collected here. /// diff --git a/src/config/snapshots/roadster__config__tests__test.snap b/src/config/snapshots/roadster__config__tests__test.snap index 3e73a48b..c55ba6fc 100644 --- a/src/config/snapshots/roadster__config__tests__test.snap +++ b/src/config/snapshots/roadster__config__tests__test.snap @@ -26,6 +26,8 @@ cli = 10000 [health-check.sidekiq] +[health-check.smtp] + [service] default-enable = true diff --git a/src/health_check/default.rs b/src/health_check/default.rs index 754d8e78..16c9dc9a 100644 --- a/src/health_check/default.rs +++ b/src/health_check/default.rs @@ -1,6 +1,8 @@ use crate::app::context::AppContext; #[cfg(feature = "db-sql")] use crate::health_check::database::DatabaseHealthCheck; +#[cfg(feature = "email-smtp")] +use crate::health_check::email::smtp::SmtpHealthCheck; #[cfg(feature = "sidekiq")] use crate::health_check::sidekiq_enqueue::SidekiqEnqueueHealthCheck; #[cfg(feature = "sidekiq")] @@ -25,6 +27,10 @@ pub fn default_health_checks( Arc::new(SidekiqFetchHealthCheck { context: context.clone(), }), + #[cfg(feature = "email-smtp")] + Arc::new(SmtpHealthCheck { + context: context.clone(), + }), ]; health_checks @@ -34,7 +40,7 @@ pub fn default_health_checks( .collect() } -#[cfg(all(test, feature = "sidekiq", feature = "db-sql",))] +#[cfg(all(test, feature = "sidekiq", feature = "db-sql", feature = "email-smtp"))] mod tests { use crate::app::context::AppContext; use crate::config::AppConfig; diff --git a/src/health_check/email/mod.rs b/src/health_check/email/mod.rs new file mode 100644 index 00000000..6b8c7982 --- /dev/null +++ b/src/health_check/email/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "email-smtp")] +pub mod smtp; diff --git a/src/health_check/email/smtp.rs b/src/health_check/email/smtp.rs new file mode 100644 index 00000000..6708da99 --- /dev/null +++ b/src/health_check/email/smtp.rs @@ -0,0 +1,57 @@ +use crate::api::core::health::smtp_health; +use crate::app::context::AppContext; +use crate::error::RoadsterResult; +use crate::health_check::{CheckResponse, HealthCheck}; +use async_trait::async_trait; +use tracing::instrument; + +pub struct SmtpHealthCheck { + pub(crate) context: AppContext, +} + +#[async_trait] +impl HealthCheck for SmtpHealthCheck { + fn name(&self) -> String { + "smtp".to_string() + } + + fn enabled(&self) -> bool { + enabled(&self.context) + } + + #[instrument(skip_all)] + async fn check(&self) -> RoadsterResult { + Ok(smtp_health(&self.context, None).await) + } +} + +fn enabled(context: &AppContext) -> bool { + context.config().health_check.smtp.common.enabled(context) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::AppConfig; + use rstest::rstest; + + #[rstest] + #[case(false, Some(true), true)] + #[case(false, Some(false), false)] + #[cfg_attr(coverage_nightly, coverage(off))] + fn enabled( + #[case] default_enable: bool, + #[case] enable: Option, + #[case] expected_enabled: bool, + ) { + // Arrange + let mut config = AppConfig::test(None).unwrap(); + config.health_check.default_enable = default_enable; + config.health_check.smtp.common.enable = enable; + + let context = AppContext::test(Some(config), None, None).unwrap(); + + // Act/Assert + assert_eq!(super::enabled(&context), expected_enabled); + } +} diff --git a/src/health_check/mod.rs b/src/health_check/mod.rs index 096168f5..65284b15 100644 --- a/src/health_check/mod.rs +++ b/src/health_check/mod.rs @@ -1,6 +1,8 @@ #[cfg(feature = "db-sql")] pub mod database; pub mod default; +#[cfg(feature = "email")] +pub mod email; pub mod registry; #[cfg(feature = "sidekiq")] pub mod sidekiq_enqueue; diff --git a/src/health_check/snapshots/roadster__health_check__default__tests__default_middleware@case_2.snap b/src/health_check/snapshots/roadster__health_check__default__tests__default_middleware@case_2.snap index 1bd9b8b9..3b0252f1 100644 --- a/src/health_check/snapshots/roadster__health_check__default__tests__default_middleware@case_2.snap +++ b/src/health_check/snapshots/roadster__health_check__default__tests__default_middleware@case_2.snap @@ -6,4 +6,5 @@ expression: health_checks 'db', 'sidekiq-enqueue', 'sidekiq-fetch', + 'smtp', ]