mirror of
https://github.com/perfect-panel/ppanel-web.git
synced 2026-08-29 05:52:08 -04:00
200 lines
7.8 KiB
Rust
200 lines
7.8 KiB
Rust
use std::sync::Arc;
|
|
|
|
use axum::routing::get;
|
|
use tracing_subscriber::layer::SubscriberExt as _;
|
|
use tracing_subscriber::util::SubscriberInitExt as _;
|
|
use tracing_subscriber::Layer as _;
|
|
use tracing_subscriber::EnvFilter;
|
|
|
|
pub mod adapter;
|
|
pub mod cache;
|
|
pub mod config;
|
|
pub mod db;
|
|
pub mod handler;
|
|
pub mod middleware;
|
|
pub mod migration;
|
|
pub mod model;
|
|
pub mod queue;
|
|
pub mod repository;
|
|
pub mod scheduler;
|
|
pub mod service;
|
|
pub mod tracing_otel;
|
|
|
|
async fn health() -> &'static str {
|
|
"ok"
|
|
}
|
|
|
|
/// Initialise the tracing subscriber from `LogConfig`.
|
|
///
|
|
/// Mirrors the Go `LogConf` initialisation logic:
|
|
/// - mode "console" (or empty / unrecognised) → stdout
|
|
/// - mode "file" → daily-rotating file in `path/`
|
|
/// - mode "volume" → daily-rotating file in `path/{service_name}/{hostname}/`
|
|
/// encoding "json" uses JSON format; anything else uses the default pretty format.
|
|
///
|
|
/// If `otel` is true the OpenTelemetry bridge layer is added so every tracing
|
|
/// span is also exported through the global OTel provider (set up by
|
|
/// `tracing_otel::init_otel` before this call).
|
|
fn init_tracing(cfg: &config::LogConfig, otel: bool) {
|
|
let filter = EnvFilter::builder()
|
|
.parse_lossy(format!("ppanel_backend={}", cfg.level));
|
|
|
|
match cfg.mode.as_str() {
|
|
"file" | "volume" => {
|
|
let dir = if cfg.mode == "volume" {
|
|
format!("{}/{}/{}", cfg.path, cfg.service_name, hostname())
|
|
} else {
|
|
cfg.path.clone()
|
|
};
|
|
if let Err(e) = std::fs::create_dir_all(&dir) {
|
|
eprintln!("failed to create log directory {dir}: {e}");
|
|
}
|
|
let file_appender = tracing_appender::rolling::daily(&dir, "app.log");
|
|
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
|
|
std::mem::forget(guard);
|
|
|
|
if cfg.encoding == "json" {
|
|
let sub = tracing_subscriber::registry()
|
|
.with(tracing_subscriber::fmt::layer()
|
|
.json()
|
|
.with_writer(non_blocking)
|
|
.with_filter(filter));
|
|
if otel {
|
|
sub.with(tracing_opentelemetry::layer()).init();
|
|
} else {
|
|
sub.init();
|
|
}
|
|
} else {
|
|
let sub = tracing_subscriber::registry()
|
|
.with(tracing_subscriber::fmt::layer()
|
|
.with_writer(non_blocking)
|
|
.with_filter(filter));
|
|
if otel {
|
|
sub.with(tracing_opentelemetry::layer()).init();
|
|
} else {
|
|
sub.init();
|
|
}
|
|
}
|
|
}
|
|
_ => {
|
|
// "console" or default
|
|
if cfg.encoding == "json" {
|
|
let sub = tracing_subscriber::registry()
|
|
.with(tracing_subscriber::fmt::layer()
|
|
.json()
|
|
.with_filter(filter));
|
|
if otel {
|
|
sub.with(tracing_opentelemetry::layer()).init();
|
|
} else {
|
|
sub.init();
|
|
}
|
|
} else {
|
|
let sub = tracing_subscriber::registry()
|
|
.with(tracing_subscriber::fmt::layer()
|
|
.with_filter(filter));
|
|
if otel {
|
|
sub.with(tracing_opentelemetry::layer()).init();
|
|
} else {
|
|
sub.init();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns the machine hostname, falling back to `"unknown"`.
|
|
fn hostname() -> String {
|
|
std::env::var("HOSTNAME")
|
|
.or_else(|_| {
|
|
// Try reading /etc/hostname on Linux.
|
|
std::fs::read_to_string("/etc/hostname").map(|s| s.trim().to_string())
|
|
})
|
|
.unwrap_or_else(|_| "unknown".to_string())
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() {
|
|
// ── Load configuration ──────────────────────────────────────────────
|
|
let cfg = Arc::new(config::Config::load());
|
|
|
|
// ── Initialise OpenTelemetry provider (before tracing subscriber) ───
|
|
// Guard must stay alive for the process lifetime to flush pending spans.
|
|
let _otel_guard = tracing_otel::init_otel(&cfg.trace);
|
|
let has_otel = _otel_guard.is_some();
|
|
|
|
// ── Initialise tracing subscriber from LogConfig ────────────────────
|
|
init_tracing(&cfg.logger, has_otel);
|
|
tracing::info!(host = %cfg.host, port = %cfg.port, "configuration loaded");
|
|
|
|
// ── Initialise database ─────────────────────────────────────────────
|
|
let db = db::init_pool(cfg.database_config())
|
|
.await
|
|
.expect("failed to connect to database");
|
|
tracing::info!("database connected");
|
|
|
|
// ── Run pending migrations ──────────────────────────────────────────
|
|
migration::run_migrations(&db)
|
|
.await
|
|
.expect("database migration failed");
|
|
tracing::info!("database migrations applied");
|
|
|
|
// ── Seed initial admin account if needed ────────────────────────────
|
|
migration::create_admin_user(&db, &cfg.administrator.email, &cfg.administrator.password)
|
|
.await
|
|
.expect("failed to create admin user");
|
|
|
|
// ── Initialise Redis cache ──────────────────────────────────────────
|
|
let cache = cache::Cache::new(&cfg.redis)
|
|
.await
|
|
.expect("failed to connect to redis");
|
|
let cache = std::sync::Arc::new(cache);
|
|
tracing::info!("redis connected");
|
|
|
|
// ── Build queue client ──────────────────────────────────────────────
|
|
let queue_client = queue::client::QueueClient::new(&queue::redis_url(&cfg.redis))
|
|
.await
|
|
.expect("failed to connect asynq queue client");
|
|
tracing::info!("queue client connected");
|
|
|
|
// ── Build repositories & router ─────────────────────────────────────
|
|
let repos = std::sync::Arc::new(repository::Repositories::new(db));
|
|
let queue_repos = Arc::clone(&repos);
|
|
let state = handler::AppState {
|
|
repos,
|
|
config: cfg.clone(),
|
|
cache,
|
|
queue: queue_client,
|
|
};
|
|
let app = handler::routes::register_routes(state).route("/health", get(health));
|
|
|
|
// ── Start background services ───────────────────────────────────────
|
|
let _scheduler = scheduler::Service::start(&cfg)
|
|
.await
|
|
.expect("failed to start scheduler");
|
|
|
|
let mut consumer = queue::Service::new(&cfg, queue_repos)
|
|
.await
|
|
.expect("failed to start queue consumer");
|
|
|
|
let addr = format!("{}:{}", cfg.host, cfg.port);
|
|
let listener = tokio::net::TcpListener::bind(&addr)
|
|
.await
|
|
.unwrap_or_else(|e| panic!("failed to bind {addr}: {e}"));
|
|
tracing::info!(
|
|
"listening on {}",
|
|
listener.local_addr().expect("listener bound")
|
|
);
|
|
|
|
// Block on HTTP server; on shutdown also stop the consumer.
|
|
axum::serve(listener, app)
|
|
.with_graceful_shutdown(async move {
|
|
consumer.shutdown().await.unwrap_or_else(|e| {
|
|
tracing::error!("queue consumer shutdown error: {e}");
|
|
});
|
|
})
|
|
.await
|
|
.unwrap_or_else(|e| panic!("server error: {e}"));
|
|
|
|
// scheduler stops automatically via Drop when `_scheduler` goes out of scope
|
|
}
|