fix(app): PrometheusHandle must be Data-wrapped before Fn closure capture

PrometheusHandle was moved into the HttpServer Fn closure but Fn
closures require Clone (not FnOnce). Wrap in web::Data before
cloning into the closure.
This commit is contained in:
ZhenYi 2026-04-21 23:05:54 +08:00
parent beae9bdea0
commit 4aaee59fa4

View File

@ -1,19 +1,19 @@
use actix_cors::Cors; use actix_cors::Cors;
use actix_web::cookie::time::Duration; use actix_web::cookie::time::Duration;
use actix_web::middleware::Logger; use actix_web::middleware::Logger;
use actix_web::{App, HttpResponse, HttpServer, cookie::Key, web}; use actix_web::{cookie::Key, web, App, HttpResponse, HttpServer};
use clap::Parser; use clap::Parser;
use db::cache::AppCache; use db::cache::AppCache;
use db::database::AppDatabase; use db::database::AppDatabase;
use sea_orm::ConnectionTrait;
use service::AppService;
use session::SessionMiddleware;
use session::config::{PersistentSession, SessionLifecycle, TtlExtensionPolicy};
use session::storage::RedisClusterSessionStore;
use observability::{ use observability::{
init_tracing_subscriber, install_recorder, prometheus_handler, spawn_http_metrics_poller, init_tracing_subscriber, install_recorder, prometheus_handler, spawn_http_metrics_poller,
MetricsMiddleware, HttpMetrics, TracingSpanMiddleware, HttpSnapshotGuard, HttpMetrics, HttpSnapshotGuard, MetricsMiddleware, TracingSpanMiddleware,
}; };
use sea_orm::ConnectionTrait;
use service::AppService;
use session::config::{PersistentSession, SessionLifecycle, TtlExtensionPolicy};
use session::storage::RedisClusterSessionStore;
use session::SessionMiddleware;
mod args; mod args;
@ -57,48 +57,40 @@ async fn main() -> anyhow::Result<()> {
let args = ServerArgs::parse(); let args = ServerArgs::parse();
let service = AppService::new(cfg.clone()).await?; let service = AppService::new(cfg.clone()).await?;
tracing::info!("AppService initialized"); tracing::info!("AppService initialized");
// Spawn background task: sync OpenRouter models immediately on startup,
// then every 10 minutes.
let _model_sync_handle = service.clone().start_sync_task(); let _model_sync_handle = service.clone().start_sync_task();
// Spawn background task: check workspace billing alerts every 30 minutes.
let _billing_alert_handle = service.clone().start_billing_alert_task(); let _billing_alert_handle = service.clone().start_billing_alert_task();
let (shutdown_tx, shutdown_rx) = tokio::sync::broadcast::channel::<()>(1); let (shutdown_tx, shutdown_rx) = tokio::sync::broadcast::channel::<()>(1);
let worker_service = service.clone(); let worker_service = service.clone();
let worker_handle = tokio::spawn(async move { let worker_handle =
worker_service tokio::spawn(async move { worker_service.start_room_workers(shutdown_rx).await });
.start_room_workers(shutdown_rx)
.await
});
// ── Phase 6: OTLP tracing ──────────────────────────────────────────────
let _otel_guard = if cfg.otel_enabled().unwrap_or(false) { let _otel_guard = if cfg.otel_enabled().unwrap_or(false) {
let endpoint = cfg.otel_endpoint().unwrap_or_else(|_| "http://localhost:4317".to_string()); let endpoint = cfg
let service_name = cfg.otel_service_name().unwrap_or_else(|_| "app".to_string()); .otel_endpoint()
let service_version = cfg.otel_service_version().unwrap_or_else(|_| "0.1.0".to_string()); .unwrap_or_else(|_| "http://localhost:4317".to_string());
let service_name = cfg
.otel_service_name()
.unwrap_or_else(|_| "app".to_string());
let service_version = cfg
.otel_service_version()
.unwrap_or_else(|_| "0.1.0".to_string());
tracing::info!(endpoint = %endpoint, service = %service_name, "OTLP tracing enabled"); tracing::info!(endpoint = %endpoint, service = %service_name, "OTLP tracing enabled");
let guard = observability::init_otlp( let guard =
&endpoint, observability::init_otlp(&endpoint, &service_name, &service_version, &log_level)
&service_name, .map_err(|e| anyhow::anyhow!("OTLP init failed: {}", e))?;
&service_version,
&log_level,
)
.map_err(|e| anyhow::anyhow!("OTLP init failed: {}", e))?;
guard guard
} else { } else {
None None
}; };
// ── Phase 6: Prometheus metrics ───────────────────────────────────────── let prometheus_handle = install_recorder();
install_recorder(); let prometheus_handle_data = web::Data::new(prometheus_handle);
let http_metrics = std::sync::Arc::new(HttpMetrics::new()); let http_metrics = std::sync::Arc::new(HttpMetrics::new());
let http_snapshot: HttpSnapshotGuard = let http_snapshot: HttpSnapshotGuard = std::sync::Arc::new(std::sync::RwLock::new(
std::sync::Arc::new(std::sync::RwLock::new( observability::HttpMetricsSnapshot::default(),
observability::HttpMetricsSnapshot::default(), ));
));
let http_snapshot_for_poller = http_snapshot.clone(); let http_snapshot_for_poller = http_snapshot.clone();
spawn_http_metrics_poller( spawn_http_metrics_poller(
http_metrics.clone(), http_metrics.clone(),
@ -147,6 +139,7 @@ async fn main() -> anyhow::Result<()> {
.app_data(web::Data::new(db.clone())) .app_data(web::Data::new(db.clone()))
.app_data(web::Data::new(cache.clone())) .app_data(web::Data::new(cache.clone()))
.app_data(http_snapshot_data.clone()) .app_data(http_snapshot_data.clone())
.app_data(prometheus_handle_data.clone())
.route("/health", web::get().to(health_check)) .route("/health", web::get().to(health_check))
.route("/metrics", web::get().to(prometheus_handler)) .route("/metrics", web::get().to(prometheus_handler))
.configure(api::route::init_routes) .configure(api::route::init_routes)