//! Attune Notifier Service - Real-time notification delivery use anyhow::Result; use attune_common::config::Config; use clap::Parser; use tracing::{error, info}; mod postgres_listener; mod service; mod subscriber_manager; mod websocket_server; use service::NotifierService; #[derive(Parser, Debug)] #[command(name = "attune-notifier")] #[command(about = "Attune Notifier Service - Real-time notifications", long_about = None)] struct Args { /// Path to configuration file #[arg(short, long)] config: Option, /// Log level (trace, debug, info, warn, error) #[arg(short, long, default_value = "info")] log_level: String, } #[tokio::main] async fn main() -> Result<()> { // Install HMAC-only JWT crypto provider (must be before any token operations) attune_common::auth::install_crypto_provider(); let args = Args::parse(); // Initialize tracing with specified log level let log_level = args .log_level .parse::() .unwrap_or(tracing::Level::INFO); tracing_subscriber::fmt() .with_max_level(log_level) .with_target(false) .with_thread_ids(true) .init(); info!("Starting Attune Notifier Service"); // Load configuration if let Some(config_path) = args.config { std::env::set_var("ATTUNE_CONFIG", config_path); } let config = Config::load()?; config.validate()?; info!("Configuration loaded successfully"); info!("Environment: {}", config.environment); info!("Database: {}", mask_password(&config.database.url)); let notifier_config = config .notifier .as_ref() .ok_or_else(|| anyhow::anyhow!("Notifier configuration not found in config file"))?; info!( "Listening on: {}:{}", notifier_config.host, notifier_config.port ); // Create and start the notifier service let service = NotifierService::new(config).await?; info!("Notifier Service initialized successfully"); // Set up graceful shutdown handler let service_clone = std::sync::Arc::new(service); let service_for_shutdown = service_clone.clone(); tokio::spawn(async move { tokio::signal::ctrl_c() .await .expect("Failed to listen for Ctrl+C"); info!("Received shutdown signal"); if let Err(e) = service_for_shutdown.shutdown().await { error!("Error during shutdown: {}", e); } }); // Start the service (blocks until shutdown) if let Err(e) = service_clone.start().await { error!("Notifier service error: {}", e); return Err(e); } info!("Attune Notifier Service stopped"); Ok(()) } /// Mask password in database URL for logging fn mask_password(url: &str) -> String { if let Some(at_pos) = url.rfind('@') { if let Some(colon_pos) = url[..at_pos].rfind(':') { let mut masked = url.to_string(); masked.replace_range(colon_pos + 1..at_pos, "****"); return masked; } } url.to_string() } #[cfg(test)] mod tests { use super::*; #[test] fn test_mask_password() { let url = "postgresql://user:password@localhost:5432/db"; let masked = mask_password(url); assert_eq!(masked, "postgresql://user:****@localhost:5432/db"); } #[test] fn test_mask_password_no_password() { let url = "postgresql://localhost:5432/db"; let masked = mask_password(url); assert_eq!(masked, "postgresql://localhost:5432/db"); } }