use crate::api::{
config::ApiConfig,
jobs::{InMemoryJobQueue, JobQueue, ScanExecutor},
middleware::rate_limit::PerKeyRateLimiter,
models::response::ProgressMessage,
};
use crate::db::DatabasePool;
use anyhow::Result;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Instant;
use tokio::sync::broadcast;
pub struct AppState {
pub config: Arc<ApiConfig>,
pub job_queue: Arc<dyn JobQueue>,
pub executor: Arc<ScanExecutor>,
pub progress_tx: broadcast::Sender<ProgressMessage>,
pub start_time: Instant,
pub stats: Arc<tokio::sync::RwLock<ApiStats>>,
pub rate_limiter: Arc<PerKeyRateLimiter>,
pub db_pool: Option<Arc<DatabasePool>>,
pub policy_dir: Option<PathBuf>,
}
#[derive(Debug, Clone, Default)]
pub struct ApiStats {
pub total_requests: u64,
pub total_scans: u64,
pub completed_scans: u64,
pub failed_scans: u64,
pub total_scan_duration_ms: u64,
pub requests_last_hour: Vec<(Instant, u64)>,
pub total_response_time_ms: u64,
pub total_responses: u64,
pub scans_24h: u64,
pub scans_7d: u64,
}
impl ApiStats {
pub fn increment_requests(&mut self) {
self.total_requests += 1;
let now = Instant::now();
self.requests_last_hour.push((now, 1));
if let Some(one_hour_ago) = now.checked_sub(std::time::Duration::from_secs(3600)) {
self.requests_last_hour.retain(|(t, _)| *t > one_hour_ago);
}
}
pub fn increment_scans(&mut self) {
self.total_scans += 1;
}
pub fn record_completed_scan(&mut self, duration_ms: u64) {
self.completed_scans += 1;
self.total_scan_duration_ms += duration_ms;
}
pub fn record_failed_scan(&mut self) {
self.failed_scans += 1;
}
pub fn avg_scan_duration(&self) -> f64 {
if self.completed_scans > 0 {
self.total_scan_duration_ms as f64 / self.completed_scans as f64
} else {
0.0
}
}
pub fn requests_in_last_hour(&self) -> u64 {
self.requests_last_hour.iter().map(|(_, c)| c).sum()
}
pub fn avg_response_time_ms(&self) -> f64 {
if self.total_responses > 0 {
self.total_response_time_ms as f64 / self.total_responses as f64
} else {
0.0
}
}
pub fn scans_last_24h(&self) -> u64 {
self.scans_24h
}
pub fn scans_last_7d(&self) -> u64 {
self.scans_7d
}
}
impl AppState {
pub fn new(config: ApiConfig) -> Result<Self> {
let config = Arc::new(config);
let job_queue: Arc<dyn JobQueue> =
Arc::new(InMemoryJobQueue::new(config.job_queue_capacity));
let executor = Arc::new(ScanExecutor::new(
job_queue.clone(),
config.max_concurrent_scans,
));
let progress_tx = executor.progress_broadcaster();
let rate_limiter = Arc::new(PerKeyRateLimiter::new(config.rate_limit_per_minute));
Ok(Self {
config,
job_queue,
executor,
progress_tx,
start_time: Instant::now(),
stats: Arc::new(tokio::sync::RwLock::new(ApiStats::default())),
rate_limiter,
db_pool: None,
policy_dir: None,
})
}
pub async fn start_executor(self: Arc<Self>) -> Result<()> {
let executor = self.executor.clone();
tokio::spawn(async move {
if let Err(e) = executor.start().await {
tracing::error!("Executor error: {}", e);
}
});
Ok(())
}
pub fn uptime_seconds(&self) -> u64 {
self.start_time.elapsed().as_secs()
}
pub async fn active_scans(&self) -> usize {
self.job_queue.active_jobs_count().await.unwrap_or(0)
}
pub async fn queued_scans(&self) -> usize {
self.job_queue.queue_length().await.unwrap_or(0)
}
pub fn subscribe_progress(&self) -> broadcast::Receiver<ProgressMessage> {
self.progress_tx.subscribe()
}
pub async fn record_request(&self) {
let mut stats = self.stats.write().await;
stats.increment_requests();
}
pub async fn record_scan(&self) {
let mut stats = self.stats.write().await;
stats.increment_scans();
}
pub async fn record_completed(&self, duration_ms: u64) {
let mut stats = self.stats.write().await;
stats.record_completed_scan(duration_ms);
}
pub async fn record_failed(&self) {
let mut stats = self.stats.write().await;
stats.record_failed_scan();
}
pub async fn get_stats(&self) -> ApiStats {
self.stats.read().await.clone()
}
pub fn with_db_pool(mut self, pool: Arc<DatabasePool>) -> Self {
self.db_pool = Some(pool);
self
}
pub fn with_policy_dir(mut self, dir: PathBuf) -> Self {
self.policy_dir = Some(dir);
self
}
}