mirror of
https://github.com/Xevion/banner.git
synced 2025-12-08 14:06:27 -06:00
feat: improve logging, solve lints, improve implementations, remove unused code, standardize things
This commit is contained in:
@@ -6,7 +6,7 @@ use axum::http::HeaderValue;
|
|||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
|
||||||
// use tracing::debug;
|
use tracing::{error, info};
|
||||||
|
|
||||||
/// Main Banner API client.
|
/// Main Banner API client.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -40,7 +40,13 @@ impl BannerApi {
|
|||||||
|
|
||||||
/// Sets up the API client by initializing session cookies.
|
/// Sets up the API client by initializing session cookies.
|
||||||
pub async fn setup(&self) -> Result<()> {
|
pub async fn setup(&self) -> Result<()> {
|
||||||
self.session_manager.setup().await
|
info!(base_url = self.base_url, "setting up banner api client");
|
||||||
|
let result = self.session_manager.setup().await;
|
||||||
|
match &result {
|
||||||
|
Ok(()) => info!("banner api client setup completed successfully"),
|
||||||
|
Err(e) => error!(error = ?e, "banner api client setup failed"),
|
||||||
|
}
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieves a list of terms from the Banner API.
|
/// Retrieves a list of terms from the Banner API.
|
||||||
|
|||||||
@@ -53,12 +53,12 @@ impl CourseScraper {
|
|||||||
expired_subjects.extend(self.get_expired_subjects(&other_subjects, term).await?);
|
expired_subjects.extend(self.get_expired_subjects(&other_subjects, term).await?);
|
||||||
|
|
||||||
if expired_subjects.is_empty() {
|
if expired_subjects.is_empty() {
|
||||||
info!("No expired subjects found, skipping scrape");
|
info!("no expired subjects found, skipping scrape");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Scraping {} subjects for term {}",
|
"scraping {} subjects for term {}",
|
||||||
expired_subjects.len(),
|
expired_subjects.len(),
|
||||||
term
|
term
|
||||||
);
|
);
|
||||||
@@ -66,7 +66,7 @@ impl CourseScraper {
|
|||||||
// Scrape each expired subject
|
// Scrape each expired subject
|
||||||
for subject in expired_subjects {
|
for subject in expired_subjects {
|
||||||
if let Err(e) = self.scrape_subject(&subject.code, term).await {
|
if let Err(e) = self.scrape_subject(&subject.code, term).await {
|
||||||
error!("Failed to scrape subject {}: {}", subject.code, e);
|
error!("failed to scrape subject {}: {}", subject.code, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rate limiting between subjects
|
// Rate limiting between subjects
|
||||||
@@ -138,7 +138,7 @@ impl CourseScraper {
|
|||||||
total_courses += course_count;
|
total_courses += course_count;
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"Retrieved {} courses for subject {} at offset {}",
|
"retrieved {} courses for subject {} at offset {}",
|
||||||
course_count, subject, offset
|
course_count, subject, offset
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -146,7 +146,7 @@ impl CourseScraper {
|
|||||||
for course in result.data.unwrap_or_default() {
|
for course in result.data.unwrap_or_default() {
|
||||||
if let Err(e) = self.store_course(&course).await {
|
if let Err(e) = self.store_course(&course).await {
|
||||||
error!(
|
error!(
|
||||||
"Failed to store course {}: {}",
|
"failed to store course {}: {}",
|
||||||
course.course_reference_number, e
|
course.course_reference_number, e
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -156,14 +156,14 @@ impl CourseScraper {
|
|||||||
if course_count >= MAX_PAGE_SIZE {
|
if course_count >= MAX_PAGE_SIZE {
|
||||||
if course_count > MAX_PAGE_SIZE {
|
if course_count > MAX_PAGE_SIZE {
|
||||||
warn!(
|
warn!(
|
||||||
"Course count {} exceeds max page size {}",
|
"course count {} exceeds max page size {}",
|
||||||
course_count, MAX_PAGE_SIZE
|
course_count, MAX_PAGE_SIZE
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
offset += MAX_PAGE_SIZE;
|
offset += MAX_PAGE_SIZE;
|
||||||
debug!(
|
debug!(
|
||||||
"Continuing to next page for subject {} at offset {}",
|
"continuing to next page for subject {} at offset {}",
|
||||||
subject, offset
|
subject, offset
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -176,7 +176,7 @@ impl CourseScraper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Scraped {} total courses for subject {}",
|
"scraped {} total courses for subject {}",
|
||||||
total_courses, subject
|
total_courses, subject
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -230,7 +230,7 @@ impl CourseScraper {
|
|||||||
.context("Failed to mark subject as scraped")?;
|
.context("Failed to mark subject as scraped")?;
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"Marked subject {} as scraped with {} courses, expiry: {:?}",
|
"marked subject {} as scraped with {} courses, expiry: {:?}",
|
||||||
subject, course_count, expiry
|
subject, course_count, expiry
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -35,10 +35,17 @@ impl SessionManager {
|
|||||||
|
|
||||||
/// Ensures a valid session is available, creating one if necessary
|
/// Ensures a valid session is available, creating one if necessary
|
||||||
pub fn ensure_session(&self) -> Result<String> {
|
pub fn ensure_session(&self) -> Result<String> {
|
||||||
|
let start_time = std::time::Instant::now();
|
||||||
let mut session_guard = self.current_session.lock().unwrap();
|
let mut session_guard = self.current_session.lock().unwrap();
|
||||||
|
|
||||||
if let Some(ref session) = *session_guard {
|
if let Some(ref session) = *session_guard {
|
||||||
if session.created_at.elapsed() < Self::SESSION_EXPIRY {
|
if session.created_at.elapsed() < Self::SESSION_EXPIRY {
|
||||||
|
let elapsed = start_time.elapsed();
|
||||||
|
debug!(
|
||||||
|
session_id = session.session_id,
|
||||||
|
elapsed = format!("{:.2?}", elapsed),
|
||||||
|
"reusing existing banner session"
|
||||||
|
);
|
||||||
return Ok(session.session_id.clone());
|
return Ok(session.session_id.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,7 +57,12 @@ impl SessionManager {
|
|||||||
created_at: Instant::now(),
|
created_at: Instant::now(),
|
||||||
});
|
});
|
||||||
|
|
||||||
debug!("Generated new Banner session: {}", session_id);
|
let elapsed = start_time.elapsed();
|
||||||
|
info!(
|
||||||
|
session_id = session_id,
|
||||||
|
elapsed = format!("{:.2?}", elapsed),
|
||||||
|
"generated new banner session"
|
||||||
|
);
|
||||||
Ok(session_id)
|
Ok(session_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +78,7 @@ impl SessionManager {
|
|||||||
|
|
||||||
/// Sets up initial session cookies by making required Banner API requests
|
/// Sets up initial session cookies by making required Banner API requests
|
||||||
pub async fn setup(&self) -> Result<()> {
|
pub async fn setup(&self) -> Result<()> {
|
||||||
info!("Setting up Banner session...");
|
info!("setting up banner session...");
|
||||||
|
|
||||||
let request_paths = ["/registration/registration", "/selfServiceMenu/data"];
|
let request_paths = ["/registration/registration", "/selfServiceMenu/data"];
|
||||||
|
|
||||||
@@ -90,7 +102,7 @@ impl SessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Note: Cookie validation would require additional setup in a real implementation
|
// Note: Cookie validation would require additional setup in a real implementation
|
||||||
debug!("Session setup complete");
|
debug!("session setup complete");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +162,7 @@ impl SessionManager {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("Successfully selected term: {}", term);
|
debug!("successfully selected term: {}", term);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ pub async fn gcal(
|
|||||||
{
|
{
|
||||||
Ok(course) => course,
|
Ok(course) => course,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(%e, crn, "Failed to fetch course data");
|
error!(%e, crn, "failed to fetch course data");
|
||||||
return Err(Error::from(e));
|
return Err(Error::from(e));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -44,7 +44,7 @@ pub async fn gcal(
|
|||||||
{
|
{
|
||||||
Ok(meeting_time) => meeting_time,
|
Ok(meeting_time) => meeting_time,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to get meeting times: {}", e);
|
error!("failed to get meeting times: {}", e);
|
||||||
return Err(Error::from(e));
|
return Err(Error::from(e));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
69
src/main.rs
69
src/main.rs
@@ -1,6 +1,6 @@
|
|||||||
use serenity::all::{ClientBuilder, GatewayIntents};
|
use serenity::all::{ClientBuilder, GatewayIntents};
|
||||||
use tokio::signal;
|
use tokio::signal;
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
use tracing_subscriber::{EnvFilter, FmtSubscriber};
|
use tracing_subscriber::{EnvFilter, FmtSubscriber};
|
||||||
|
|
||||||
use crate::app_state::AppState;
|
use crate::app_state::AppState;
|
||||||
@@ -39,14 +39,33 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.with_env_filter(filter)
|
.with_env_filter(filter)
|
||||||
|
.with_target(true)
|
||||||
.finish();
|
.finish();
|
||||||
tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
|
tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
|
||||||
|
|
||||||
|
// Log application startup context
|
||||||
|
info!(
|
||||||
|
version = env!("CARGO_PKG_VERSION"),
|
||||||
|
environment = if cfg!(debug_assertions) {
|
||||||
|
"development"
|
||||||
|
} else {
|
||||||
|
"production"
|
||||||
|
},
|
||||||
|
"starting banner system"
|
||||||
|
);
|
||||||
|
|
||||||
let config: Config = Figment::new()
|
let config: Config = Figment::new()
|
||||||
.merge(Env::prefixed("APP_"))
|
.merge(Env::prefixed("APP_"))
|
||||||
.extract()
|
.extract()
|
||||||
.expect("Failed to load config");
|
.expect("Failed to load config");
|
||||||
|
|
||||||
|
info!(
|
||||||
|
port = config.port,
|
||||||
|
shutdown_timeout = format!("{:.2?}", config.shutdown_timeout),
|
||||||
|
banner_base_url = config.banner_base_url,
|
||||||
|
"configuration loaded"
|
||||||
|
);
|
||||||
|
|
||||||
// Create BannerApi and AppState
|
// Create BannerApi and AppState
|
||||||
let banner_api =
|
let banner_api =
|
||||||
BannerApi::new(config.banner_base_url.clone()).expect("Failed to create BannerApi");
|
BannerApi::new(config.banner_base_url.clone()).expect("Failed to create BannerApi");
|
||||||
@@ -121,61 +140,67 @@ async fn main() {
|
|||||||
signal::ctrl_c()
|
signal::ctrl_c()
|
||||||
.await
|
.await
|
||||||
.expect("Failed to install CTRL+C signal handler");
|
.expect("Failed to install CTRL+C signal handler");
|
||||||
info!("Received CTRL+C, gracefully shutting down...");
|
info!("received ctrl+c, gracefully shutting down...");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Main application loop - wait for services or CTRL+C
|
// Main application loop - wait for services or CTRL+C
|
||||||
let mut exit_code = 0;
|
let mut exit_code = 0;
|
||||||
|
|
||||||
let join = |strings: Vec<String>| {
|
|
||||||
strings
|
|
||||||
.iter()
|
|
||||||
.map(|s| format!("\"{}\"", s))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ")
|
|
||||||
};
|
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
(service_name, result) = service_manager.run() => {
|
(service_name, result) = service_manager.run() => {
|
||||||
// A service completed unexpectedly
|
// A service completed unexpectedly
|
||||||
match result {
|
match result {
|
||||||
ServiceResult::GracefulShutdown => {
|
ServiceResult::GracefulShutdown => {
|
||||||
info!(service = service_name, "Service completed gracefully");
|
info!(service = service_name, "service completed gracefully");
|
||||||
}
|
}
|
||||||
ServiceResult::NormalCompletion => {
|
ServiceResult::NormalCompletion => {
|
||||||
warn!(service = service_name, "Service completed unexpectedly");
|
warn!(service = service_name, "service completed unexpectedly");
|
||||||
exit_code = 1;
|
exit_code = 1;
|
||||||
}
|
}
|
||||||
ServiceResult::Error(e) => {
|
ServiceResult::Error(e) => {
|
||||||
error!(service = service_name, "Service failed: {e}");
|
error!(service = service_name, error = ?e, "service failed");
|
||||||
exit_code = 1;
|
exit_code = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown remaining services
|
// Shutdown remaining services
|
||||||
match service_manager.shutdown(shutdown_timeout).await {
|
match service_manager.shutdown(shutdown_timeout).await {
|
||||||
Ok(()) => {
|
Ok(elapsed) => {
|
||||||
debug!("Graceful shutdown complete");
|
info!(
|
||||||
|
remaining = format!("{:.2?}", shutdown_timeout - elapsed),
|
||||||
|
"graceful shutdown complete"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Err(pending_services) => {
|
Err(pending_services) => {
|
||||||
warn!(
|
warn!(
|
||||||
"Graceful shutdown elapsed - the following service(s) did not complete: {}",
|
pending_count = pending_services.len(),
|
||||||
join(pending_services)
|
pending_services = ?pending_services,
|
||||||
|
"graceful shutdown elapsed - {} service(s) did not complete",
|
||||||
|
pending_services.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Non-zero exit code, default to 2 if not set
|
||||||
exit_code = if exit_code == 0 { 2 } else { exit_code };
|
exit_code = if exit_code == 0 { 2 } else { exit_code };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = ctrl_c => {
|
_ = ctrl_c => {
|
||||||
// User requested shutdown
|
// User requested shutdown
|
||||||
|
info!("user requested shutdown via ctrl+c");
|
||||||
match service_manager.shutdown(shutdown_timeout).await {
|
match service_manager.shutdown(shutdown_timeout).await {
|
||||||
Ok(()) => {
|
Ok(elapsed) => {
|
||||||
debug!("Graceful shutdown complete");
|
info!(
|
||||||
|
remaining = format!("{:.2?}", shutdown_timeout - elapsed),
|
||||||
|
"graceful shutdown complete"
|
||||||
|
);
|
||||||
|
info!("graceful shutdown complete");
|
||||||
}
|
}
|
||||||
Err(pending_services) => {
|
Err(pending_services) => {
|
||||||
warn!(
|
warn!(
|
||||||
"Graceful shutdown elapsed - the following service(s) did not complete: {}",
|
pending_count = pending_services.len(),
|
||||||
join(pending_services)
|
pending_services = ?pending_services,
|
||||||
|
"graceful shutdown elapsed - {} service(s) did not complete",
|
||||||
|
pending_services.len()
|
||||||
);
|
);
|
||||||
exit_code = 2;
|
exit_code = 2;
|
||||||
}
|
}
|
||||||
@@ -183,6 +208,6 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
info!(exit_code = exit_code, "Shutdown complete");
|
info!(exit_code, "application shutdown complete");
|
||||||
std::process::exit(exit_code);
|
std::process::exit(exit_code);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use super::Service;
|
use super::Service;
|
||||||
use serenity::Client;
|
use serenity::Client;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tracing::{error, warn};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
/// Discord bot service implementation
|
/// Discord bot service implementation
|
||||||
pub struct BotService {
|
pub struct BotService {
|
||||||
@@ -28,11 +28,11 @@ impl Service for BotService {
|
|||||||
async fn run(&mut self) -> Result<(), anyhow::Error> {
|
async fn run(&mut self) -> Result<(), anyhow::Error> {
|
||||||
match self.client.start().await {
|
match self.client.start().await {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
warn!(service = "bot", "Stopped early.");
|
debug!(service = "bot", "stopped early.");
|
||||||
Err(anyhow::anyhow!("bot stopped early"))
|
Err(anyhow::anyhow!("bot stopped early"))
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(service = "bot", "Error: {e:?}");
|
error!(service = "bot", "error: {e:?}");
|
||||||
Err(e.into())
|
Err(e.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::collections::HashMap;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use tracing::{error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
use crate::services::{Service, ServiceResult, run_service};
|
use crate::services::{Service, ServiceResult, run_service};
|
||||||
|
|
||||||
@@ -30,12 +30,21 @@ impl ServiceManager {
|
|||||||
|
|
||||||
/// Spawn all registered services
|
/// Spawn all registered services
|
||||||
pub fn spawn_all(&mut self) {
|
pub fn spawn_all(&mut self) {
|
||||||
|
let service_count = self.registered_services.len();
|
||||||
|
let service_names: Vec<_> = self.registered_services.keys().cloned().collect();
|
||||||
|
|
||||||
for (name, service) in self.registered_services.drain() {
|
for (name, service) in self.registered_services.drain() {
|
||||||
let shutdown_rx = self.shutdown_tx.subscribe();
|
let shutdown_rx = self.shutdown_tx.subscribe();
|
||||||
let handle = tokio::spawn(run_service(service, shutdown_rx));
|
let handle = tokio::spawn(run_service(service, shutdown_rx));
|
||||||
self.running_services.insert(name, handle);
|
self.running_services.insert(name, handle);
|
||||||
}
|
}
|
||||||
info!("Spawned {} services", self.running_services.len());
|
|
||||||
|
info!(
|
||||||
|
service_count,
|
||||||
|
services = ?service_names,
|
||||||
|
"spawned {} services",
|
||||||
|
service_count
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run all services until one completes or fails
|
/// Run all services until one completes or fails
|
||||||
@@ -49,7 +58,7 @@ impl ServiceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"ServiceManager running {} services",
|
"servicemanager running {} services",
|
||||||
self.running_services.len()
|
self.running_services.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -70,7 +79,7 @@ impl ServiceManager {
|
|||||||
return (completed_name.clone(), result);
|
return (completed_name.clone(), result);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(service = completed_name, "Service task panicked: {e}");
|
error!(service = completed_name, "service task panicked: {e}");
|
||||||
return (
|
return (
|
||||||
completed_name.clone(),
|
completed_name.clone(),
|
||||||
ServiceResult::Error(anyhow::anyhow!("Task panic: {e}")),
|
ServiceResult::Error(anyhow::anyhow!("Task panic: {e}")),
|
||||||
@@ -84,82 +93,65 @@ impl ServiceManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shutdown all services gracefully with a timeout
|
/// Shutdown all services gracefully with a timeout.
|
||||||
/// Returns Ok(()) if all services shut down, or Err(Vec<String>) with names of services that timed out
|
///
|
||||||
pub async fn shutdown(mut self, timeout: Duration) -> Result<(), Vec<String>> {
|
/// If any service fails to shutdown, it will return an error containing the names of the services that failed to shutdown.
|
||||||
if self.running_services.is_empty() {
|
/// If all services shutdown successfully, the function will return the duration elapsed.
|
||||||
info!("No services to shutdown");
|
pub async fn shutdown(&mut self, timeout: Duration) -> Result<Duration, Vec<String>> {
|
||||||
return Ok(());
|
let service_count = self.running_services.len();
|
||||||
}
|
let service_names: Vec<_> = self.running_services.keys().cloned().collect();
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Shutting down {} services with {}s timeout",
|
service_count,
|
||||||
self.running_services.len(),
|
services = ?service_names,
|
||||||
timeout.as_secs()
|
timeout = format!("{:.2?}", timeout),
|
||||||
|
"shutting down {} services with {:?} timeout",
|
||||||
|
service_count,
|
||||||
|
timeout
|
||||||
);
|
);
|
||||||
|
|
||||||
// Signal all services to shutdown
|
// Send shutdown signal to all services
|
||||||
let _ = self.shutdown_tx.send(());
|
let _ = self.shutdown_tx.send(());
|
||||||
|
|
||||||
// Wait for all services to complete with timeout
|
// Wait for all services to complete
|
||||||
let shutdown_result = tokio::time::timeout(timeout, async {
|
let start_time = std::time::Instant::now();
|
||||||
let mut completed = Vec::new();
|
let mut pending_services = Vec::new();
|
||||||
let mut failed = Vec::new();
|
|
||||||
|
|
||||||
while !self.running_services.is_empty() {
|
for (name, handle) in self.running_services.drain() {
|
||||||
let mut to_remove = Vec::new();
|
match tokio::time::timeout(timeout, handle).await {
|
||||||
|
Ok(Ok(_)) => {
|
||||||
for (name, handle) in &mut self.running_services {
|
debug!(service = name, "service shutdown completed");
|
||||||
if handle.is_finished() {
|
|
||||||
to_remove.push(name.clone());
|
|
||||||
}
|
}
|
||||||
}
|
Ok(Err(e)) => {
|
||||||
|
warn!(service = name, error = ?e, "service shutdown failed");
|
||||||
for name in to_remove {
|
pending_services.push(name);
|
||||||
let handle = self.running_services.remove(&name).unwrap();
|
|
||||||
match handle.await {
|
|
||||||
Ok(ServiceResult::GracefulShutdown) => {
|
|
||||||
completed.push(name);
|
|
||||||
}
|
|
||||||
Ok(ServiceResult::NormalCompletion) => {
|
|
||||||
warn!(service = name, "Service completed normally during shutdown");
|
|
||||||
completed.push(name);
|
|
||||||
}
|
|
||||||
Ok(ServiceResult::Error(e)) => {
|
|
||||||
error!(service = name, "Service error during shutdown: {e}");
|
|
||||||
failed.push(name);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!(service = name, "Service panic during shutdown: {e}");
|
|
||||||
failed.push(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.running_services.is_empty() {
|
|
||||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(completed, failed)
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match shutdown_result {
|
|
||||||
Ok((completed, failed)) => {
|
|
||||||
if !completed.is_empty() {
|
|
||||||
info!("Services shutdown completed: {}", completed.join(", "));
|
|
||||||
}
|
|
||||||
if !failed.is_empty() {
|
|
||||||
warn!("Services had errors during shutdown: {}", failed.join(", "));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
// Timeout occurred - return names of services that didn't complete
|
warn!(service = name, "service shutdown timed out");
|
||||||
let pending_services: Vec<String> = self.running_services.keys().cloned().collect();
|
pending_services.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let elapsed = start_time.elapsed();
|
||||||
|
if pending_services.is_empty() {
|
||||||
|
info!(
|
||||||
|
service_count,
|
||||||
|
elapsed = format!("{:.2?}", elapsed),
|
||||||
|
"services shutdown completed: {}",
|
||||||
|
service_names.join(", ")
|
||||||
|
);
|
||||||
|
Ok(elapsed)
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
pending_count = pending_services.len(),
|
||||||
|
pending_services = ?pending_services,
|
||||||
|
elapsed = format!("{:.2?}", elapsed),
|
||||||
|
"services shutdown completed with {} pending: {}",
|
||||||
|
pending_services.len(),
|
||||||
|
pending_services.join(", ")
|
||||||
|
);
|
||||||
Err(pending_services)
|
Err(pending_services)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,16 +33,16 @@ pub async fn run_service(
|
|||||||
mut shutdown_rx: broadcast::Receiver<()>,
|
mut shutdown_rx: broadcast::Receiver<()>,
|
||||||
) -> ServiceResult {
|
) -> ServiceResult {
|
||||||
let name = service.name();
|
let name = service.name();
|
||||||
info!(service = name, "Service started");
|
info!(service = name, "service started");
|
||||||
|
|
||||||
let work = async {
|
let work = async {
|
||||||
match service.run().await {
|
match service.run().await {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
warn!(service = name, "Service completed unexpectedly");
|
warn!(service = name, "service completed unexpectedly");
|
||||||
ServiceResult::NormalCompletion
|
ServiceResult::NormalCompletion
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(service = name, "Service failed: {e}");
|
error!(service = name, "service failed: {e}");
|
||||||
ServiceResult::Error(e)
|
ServiceResult::Error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,18 +51,18 @@ pub async fn run_service(
|
|||||||
tokio::select! {
|
tokio::select! {
|
||||||
result = work => result,
|
result = work => result,
|
||||||
_ = shutdown_rx.recv() => {
|
_ = shutdown_rx.recv() => {
|
||||||
info!(service = name, "Shutting down...");
|
info!(service = name, "shutting down...");
|
||||||
let start_time = std::time::Instant::now();
|
let start_time = std::time::Instant::now();
|
||||||
|
|
||||||
match service.shutdown().await {
|
match service.shutdown().await {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
let elapsed = start_time.elapsed();
|
let elapsed = start_time.elapsed();
|
||||||
info!(service = name, "Shutdown completed in {elapsed:.2?}");
|
info!(service = name, "shutdown completed in {elapsed:.2?}");
|
||||||
ServiceResult::GracefulShutdown
|
ServiceResult::GracefulShutdown
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let elapsed = start_time.elapsed();
|
let elapsed = start_time.elapsed();
|
||||||
error!(service = name, "Shutdown failed after {elapsed:.2?}: {e}");
|
error!(service = name, "shutdown failed after {elapsed:.2?}: {e}");
|
||||||
ServiceResult::Error(e)
|
ServiceResult::Error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,13 +36,13 @@ impl Service for WebService {
|
|||||||
info!(
|
info!(
|
||||||
service = "web",
|
service = "web",
|
||||||
link = format!("http://localhost:{}", addr.port()),
|
link = format!("http://localhost:{}", addr.port()),
|
||||||
"Starting web server",
|
"starting web server",
|
||||||
);
|
);
|
||||||
|
|
||||||
let listener = TcpListener::bind(addr).await?;
|
let listener = TcpListener::bind(addr).await?;
|
||||||
debug!(
|
debug!(
|
||||||
service = "web",
|
service = "web",
|
||||||
"Web server listening on {}",
|
"web server listening on {}",
|
||||||
format!("http://{}", addr)
|
format!("http://{}", addr)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -56,12 +56,12 @@ impl Service for WebService {
|
|||||||
let _ = shutdown_rx.recv().await;
|
let _ = shutdown_rx.recv().await;
|
||||||
debug!(
|
debug!(
|
||||||
service = "web",
|
service = "web",
|
||||||
"Received shutdown signal, starting graceful shutdown"
|
"received shutdown signal, starting graceful shutdown"
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
info!(service = "web", "Web server stopped");
|
info!(service = "web", "web server stopped");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ impl Service for WebService {
|
|||||||
} else {
|
} else {
|
||||||
warn!(
|
warn!(
|
||||||
service = "web",
|
service = "web",
|
||||||
"No shutdown channel found, cannot trigger graceful shutdown"
|
"no shutdown channel found, cannot trigger graceful shutdown"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
use axum::{Router, extract::State, response::Json, routing::get};
|
use axum::{Router, extract::State, response::Json, routing::get};
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use tracing::{debug, info};
|
||||||
|
|
||||||
/// Shared application state for web server
|
/// Shared application state for web server
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -15,6 +16,7 @@ pub struct BannerState {
|
|||||||
pub fn create_banner_router(state: BannerState) -> Router {
|
pub fn create_banner_router(state: BannerState) -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(root))
|
.route("/", get(root))
|
||||||
|
.route("/health", get(health))
|
||||||
.route("/status", get(status))
|
.route("/status", get(status))
|
||||||
.route("/metrics", get(metrics))
|
.route("/metrics", get(metrics))
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
@@ -22,6 +24,7 @@ pub fn create_banner_router(state: BannerState) -> Router {
|
|||||||
|
|
||||||
/// Root endpoint - shows API info
|
/// Root endpoint - shows API info
|
||||||
async fn root() -> Json<Value> {
|
async fn root() -> Json<Value> {
|
||||||
|
debug!("root endpoint accessed");
|
||||||
Json(json!({
|
Json(json!({
|
||||||
"message": "Banner Discord Bot API",
|
"message": "Banner Discord Bot API",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
@@ -33,6 +36,15 @@ async fn root() -> Json<Value> {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Health check endpoint
|
||||||
|
async fn health() -> Json<Value> {
|
||||||
|
info!("health check requested");
|
||||||
|
Json(json!({
|
||||||
|
"status": "healthy",
|
||||||
|
"timestamp": chrono::Utc::now().to_rfc3339()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
/// Status endpoint showing bot and system status
|
/// Status endpoint showing bot and system status
|
||||||
async fn status(State(_state): State<BannerState>) -> Json<Value> {
|
async fn status(State(_state): State<BannerState>) -> Json<Value> {
|
||||||
// For now, return basic status without accessing private fields
|
// For now, return basic status without accessing private fields
|
||||||
|
|||||||
Reference in New Issue
Block a user