diff --git a/Cargo.lock b/Cargo.lock index 93e1af5..25c00ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -653,6 +653,31 @@ dependencies = [ "tower-service", ] +[[package]] +name = "axum-extra" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d" +dependencies = [ + "axum 0.8.4", + "axum-core 0.5.2", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "serde", + "serde_html_form", + "serde_path_to_error", + "tower 0.5.2", + "tower-layer", + "tower-service", +] + [[package]] name = "axum-login" version = "0.16.0" @@ -2293,6 +2318,7 @@ dependencies = [ "anyhow", "async-trait", "axum 0.8.4", + "axum-extra", "chrono", "clap", "dashmap", @@ -7521,6 +7547,19 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "serde_html_form" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d2de91cf02bbc07cde38891769ccd5d4f073d22a40683aa4bc7a95781aaa2c4" +dependencies = [ + "form_urlencoded", + "indexmap 2.7.1", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_json" version = "1.0.125" diff --git a/easytier-contrib/easytier-uptime/.env b/easytier-contrib/easytier-uptime/.env new file mode 100644 index 0000000..a38dd97 --- /dev/null +++ b/easytier-contrib/easytier-uptime/.env @@ -0,0 +1,17 @@ +# Development Environment Configuration +SERVER_HOST=127.0.0.1 +SERVER_PORT=8080 +DATABASE_PATH=uptime.db +DATABASE_MAX_CONNECTIONS=5 +HEALTH_CHECK_INTERVAL=60 +HEALTH_CHECK_TIMEOUT=15 +HEALTH_CHECK_RETRIES=2 +RUST_LOG=debug +LOG_LEVEL=debug +CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080 +CORS_ALLOWED_METHODS=GET,POST,PUT,DELETE,OPTIONS +CORS_ALLOWED_HEADERS=content-type,authorization +NODE_ENV=development +API_BASE_URL=/api +ENABLE_COMPRESSION=true +ENABLE_CORS=true \ No newline at end of file diff --git a/easytier-contrib/easytier-uptime/Cargo.toml b/easytier-contrib/easytier-uptime/Cargo.toml index 559dc33..8536ca8 100644 --- a/easytier-contrib/easytier-uptime/Cargo.toml +++ b/easytier-contrib/easytier-uptime/Cargo.toml @@ -15,6 +15,7 @@ uuid = { version = "1.0", features = ["v4", "serde"] } # Axum web framework axum = { version = "0.8.4", features = ["macros"] } +axum-extra = { version = "0.10", features = ["query"] } tower-http = { version = "0.6", features = ["cors", "compression-full"] } tower = "0.5" diff --git a/easytier-contrib/easytier-uptime/frontend/src/App.vue b/easytier-contrib/easytier-uptime/frontend/src/App.vue index 9978b10..eeb5bba 100644 --- a/easytier-contrib/easytier-uptime/frontend/src/App.vue +++ b/easytier-contrib/easytier-uptime/frontend/src/App.vue @@ -1,5 +1,5 @@ @@ -570,4 +691,28 @@ onMounted(() => { background-color: #fafafa; border-top: 1px solid #ebeef5; } + +.tag-option { + display: inline-block; + padding: 2px 6px; + border-radius: 4px; + font-size: 12px; +} + +:deep(.el-table__body-wrapper) { + overflow-x: auto !important; + overflow-y: hidden !important; + height: auto !important; +} + +:deep(.el-card__body) { + overflow: visible !important; +} + +:deep(.el-table__body-wrapper .el-scrollbar__wrap) { + overflow-x: auto !important; + overflow-y: hidden !important; + height: auto !important; + max-height: none !important; +} diff --git a/easytier-contrib/easytier-uptime/frontend/vite.config.js b/easytier-contrib/easytier-uptime/frontend/vite.config.js index 2b83c87..bc8f810 100644 --- a/easytier-contrib/easytier-uptime/frontend/vite.config.js +++ b/easytier-contrib/easytier-uptime/frontend/vite.config.js @@ -18,11 +18,11 @@ export default defineConfig({ server: { proxy: { '/api': { - target: 'http://localhost:8080', + target: 'http://localhost:11030', changeOrigin: true, }, '/health': { - target: 'http://localhost:8080', + target: 'http://localhost:11030', changeOrigin: true, } } diff --git a/easytier-contrib/easytier-uptime/src/api/handlers.rs b/easytier-contrib/easytier-uptime/src/api/handlers.rs index 6ad937f..6c26661 100644 --- a/easytier-contrib/easytier-uptime/src/api/handlers.rs +++ b/easytier-contrib/easytier-uptime/src/api/handlers.rs @@ -1,6 +1,6 @@ use std::ops::{Div, Mul}; -use axum::extract::{Path, Query, State}; +use axum::extract::{Path, State}; use axum::Json; use sea_orm::{ ColumnTrait, Condition, EntityTrait, IntoActiveModel, ModelTrait, Order, PaginatorTrait, @@ -16,6 +16,7 @@ use crate::api::{ use crate::db::entity::{self, health_records, shared_nodes}; use crate::db::{operations::*, Db}; use crate::health_checker_manager::HealthCheckerManager; +use axum_extra::extract::Query; use std::sync::Arc; #[derive(Clone)] @@ -60,6 +61,35 @@ pub async fn get_nodes( ); } + // 标签过滤(支持单标签与多标签 OR) + let mut filtered_ids: Option> = None; + if !filters.tags.is_empty() { + let ids_any = + NodeOperations::filter_node_ids_by_tags_any(&app_state.db, &filters.tags).await?; + filtered_ids = match filtered_ids { + Some(mut existing) => { + // 合并去重 + existing.extend(ids_any); + existing.sort(); + existing.dedup(); + Some(existing) + } + None => Some(ids_any), + }; + } + if let Some(ids) = filtered_ids { + if ids.is_empty() { + return Ok(Json(ApiResponse::success(PaginatedResponse { + items: vec![], + total: 0, + page, + per_page, + total_pages: 0, + }))); + } + query = query.filter(entity::shared_nodes::Column::Id.is_in(ids)); + } + let total = query.clone().count(app_state.db.orm_db()).await?; let nodes = query .order_by_asc(entity::shared_nodes::Column::Id) @@ -71,6 +101,13 @@ pub async fn get_nodes( let mut node_responses: Vec = nodes.into_iter().map(NodeResponse::from).collect(); let total_pages = total.div_ceil(per_page as u64); + // 补充标签 + let ids: Vec = node_responses.iter().map(|n| n.id).collect(); + let tags_map = NodeOperations::get_nodes_tags_map(&app_state.db, &ids).await?; + for n in &mut node_responses { + n.tags = tags_map.get(&n.id).cloned().unwrap_or_default(); + } + // 为每个节点添加健康状态信息 for node_response in &mut node_responses { if let Some(mut health_record) = app_state @@ -99,7 +136,6 @@ pub async fn get_nodes( // remove sensitive information node_responses.iter_mut().for_each(|node| { - tracing::info!("node: {:?}", node); node.network_name = None; node.network_secret = None; @@ -161,7 +197,10 @@ pub async fn get_node( .await? .ok_or_else(|| ApiError::NotFound(format!("Node with id {} not found", id)))?; - Ok(Json(ApiResponse::success(NodeResponse::from(node)))) + let mut resp = NodeResponse::from(node); + resp.tags = NodeOperations::get_node_tags(&app_state.db, resp.id).await?; + + Ok(Json(ApiResponse::success(resp))) } pub async fn get_node_health( @@ -325,6 +364,39 @@ pub async fn admin_get_nodes( ); } + // 标签过滤(支持单标签与多标签 OR) + let mut filtered_ids: Option> = None; + if let Some(tag) = filters.tag { + let ids = NodeOperations::filter_node_ids_by_tag(&app_state.db, &tag).await?; + filtered_ids = Some(ids); + } + if let Some(tags) = filters.tags { + if !tags.is_empty() { + let ids_any = NodeOperations::filter_node_ids_by_tags_any(&app_state.db, &tags).await?; + filtered_ids = match filtered_ids { + Some(mut existing) => { + existing.extend(ids_any); + existing.sort(); + existing.dedup(); + Some(existing) + } + None => Some(ids_any), + }; + } + } + if let Some(ids) = filtered_ids { + if ids.is_empty() { + return Ok(Json(ApiResponse::success(PaginatedResponse { + items: vec![], + total: 0, + page, + per_page, + total_pages: 0, + }))); + } + query = query.filter(entity::shared_nodes::Column::Id.is_in(ids)); + } + let total = query.clone().count(app_state.db.orm_db()).await?; let nodes = query @@ -334,7 +406,14 @@ pub async fn admin_get_nodes( .all(app_state.db.orm_db()) .await?; - let node_responses: Vec = nodes.into_iter().map(NodeResponse::from).collect(); + let mut node_responses: Vec = nodes.into_iter().map(NodeResponse::from).collect(); + + // 补充标签 + let ids: Vec = node_responses.iter().map(|n| n.id).collect(); + let tags_map = NodeOperations::get_nodes_tags_map(&app_state.db, &ids).await?; + for n in &mut node_responses { + n.tags = tags_map.get(&n.id).cloned().unwrap_or_default(); + } let total_pages = (total as f64 / per_page as f64).ceil() as u32; @@ -366,7 +445,10 @@ pub async fn admin_approve_node( .exec(app_state.db.orm_db()) .await?; - Ok(Json(ApiResponse::success(NodeResponse::from(updated_node)))) + let mut resp = NodeResponse::from(updated_node); + resp.tags = NodeOperations::get_node_tags(&app_state.db, resp.id).await?; + + Ok(Json(ApiResponse::success(resp))) } pub async fn admin_update_node( @@ -432,7 +514,15 @@ pub async fn admin_update_node( .exec(app_state.db.orm_db()) .await?; - Ok(Json(ApiResponse::success(NodeResponse::from(updated_node)))) + // 更新标签 + if let Some(tags) = request.tags { + NodeOperations::set_node_tags(&app_state.db, updated_node.id, tags).await?; + } + + let mut resp = NodeResponse::from(updated_node); + resp.tags = NodeOperations::get_node_tags(&app_state.db, resp.id).await?; + + Ok(Json(ApiResponse::success(resp))) } pub async fn admin_revoke_approval( @@ -454,7 +544,10 @@ pub async fn admin_revoke_approval( .exec(app_state.db.orm_db()) .await?; - Ok(Json(ApiResponse::success(NodeResponse::from(updated_node)))) + let mut resp = NodeResponse::from(updated_node); + resp.tags = NodeOperations::get_node_tags(&app_state.db, resp.id).await?; + + Ok(Json(ApiResponse::success(resp))) } pub async fn admin_delete_node( @@ -505,3 +598,10 @@ fn verify_admin_token(headers: &HeaderMap) -> ApiResult<()> { Ok(()) } + +pub async fn get_all_tags( + State(app_state): State, +) -> ApiResult>>> { + let tags = NodeOperations::get_all_tags(&app_state.db).await?; + Ok(Json(ApiResponse::success(tags))) +} diff --git a/easytier-contrib/easytier-uptime/src/api/models.rs b/easytier-contrib/easytier-uptime/src/api/models.rs index abf4348..6182b19 100644 --- a/easytier-contrib/easytier-uptime/src/api/models.rs +++ b/easytier-contrib/easytier-uptime/src/api/models.rs @@ -162,6 +162,9 @@ pub struct UpdateNodeRequest { #[validate(email)] pub mail: Option, + + // 标签字段(仅管理员可用) + pub tags: Option>, } #[derive(Debug, Serialize, Deserialize)] @@ -198,6 +201,7 @@ pub struct NodeResponse { pub qq_number: Option, pub wechat: Option, pub mail: Option, + pub tags: Vec, } impl From for NodeResponse { @@ -247,6 +251,7 @@ impl From for NodeResponse { } else { Some(node.mail) }, + tags: Vec::new(), } } } @@ -281,6 +286,8 @@ pub struct NodeFilterParams { pub is_active: Option, pub protocol: Option, pub search: Option, + #[serde(default)] + pub tags: Vec, } #[derive(Debug, Serialize, Deserialize)] @@ -313,4 +320,6 @@ pub struct AdminNodeFilterParams { pub is_approved: Option, pub protocol: Option, pub search: Option, + pub tag: Option, + pub tags: Option>, } diff --git a/easytier-contrib/easytier-uptime/src/api/routes.rs b/easytier-contrib/easytier-uptime/src/api/routes.rs index e4e07d1..d7c49a9 100644 --- a/easytier-contrib/easytier-uptime/src/api/routes.rs +++ b/easytier-contrib/easytier-uptime/src/api/routes.rs @@ -6,7 +6,7 @@ use tower_http::cors::CorsLayer; use super::handlers::AppState; use super::handlers::{ admin_approve_node, admin_delete_node, admin_get_nodes, admin_login, admin_revoke_approval, - admin_update_node, admin_verify_token, create_node, get_node, get_node_health, + admin_update_node, admin_verify_token, create_node, get_all_tags, get_node, get_node_health, get_node_health_stats, get_nodes, health_check, }; use crate::api::{get_node_connect_url, test_connection}; @@ -38,6 +38,7 @@ pub fn create_routes() -> Router { .route("/node/{id}", get(get_node_connect_url)) .route("/health", get(health_check)) .route("/api/nodes", get(get_nodes).post(create_node)) + .route("/api/tags", get(get_all_tags)) .route("/api/test_connection", post(test_connection)) .route("/api/nodes/{id}/health", get(get_node_health)) .route("/api/nodes/{id}/health/stats", get(get_node_health_stats)) diff --git a/easytier-contrib/easytier-uptime/src/config.rs b/easytier-contrib/easytier-uptime/src/config.rs index e3c9811..f11e01b 100644 --- a/easytier-contrib/easytier-uptime/src/config.rs +++ b/easytier-contrib/easytier-uptime/src/config.rs @@ -2,6 +2,8 @@ use std::env; use std::net::{IpAddr, SocketAddr}; use std::path::PathBuf; +use easytier::common::config::{ConsoleLoggerConfig, FileLoggerConfig, LoggingConfig}; + #[derive(Debug, Clone)] pub struct AppConfig { pub server: ServerConfig, @@ -32,12 +34,6 @@ pub struct HealthCheckConfig { pub max_retries: u32, } -#[derive(Debug, Clone)] -pub struct LoggingConfig { - pub level: String, - pub rust_log: String, -} - #[derive(Debug, Clone)] pub struct CorsConfig { pub allowed_origins: Vec, @@ -100,8 +96,14 @@ impl AppConfig { }; let logging_config = LoggingConfig { - level: env::var("LOG_LEVEL").unwrap_or_else(|_| "info".to_string()), - rust_log: env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()), + file_logger: Some(FileLoggerConfig { + level: Some(env::var("LOG_LEVEL").unwrap_or_else(|_| "info".to_string())), + file: Some("easytier-uptime.log".to_string()), + ..Default::default() + }), + console_logger: Some(ConsoleLoggerConfig { + level: Some(env::var("LOG_LEVEL").unwrap_or_else(|_| "info".to_string())), + }), }; let cors_config = CorsConfig { @@ -161,8 +163,14 @@ impl AppConfig { max_retries: 3, }, logging: LoggingConfig { - level: "info".to_string(), - rust_log: "info".to_string(), + file_logger: Some(FileLoggerConfig { + level: Some("info".to_string()), + file: Some("easytier-uptime.log".to_string()), + ..Default::default() + }), + console_logger: Some(ConsoleLoggerConfig { + level: Some("info".to_string()), + }), }, cors: CorsConfig { allowed_origins: vec![ diff --git a/easytier-contrib/easytier-uptime/src/db/entity/mod.rs b/easytier-contrib/easytier-uptime/src/db/entity/mod.rs index 8b427a8..05f4150 100644 --- a/easytier-contrib/easytier-uptime/src/db/entity/mod.rs +++ b/easytier-contrib/easytier-uptime/src/db/entity/mod.rs @@ -3,4 +3,5 @@ pub mod prelude; pub mod health_records; +pub mod node_tags; pub mod shared_nodes; diff --git a/easytier-contrib/easytier-uptime/src/db/entity/node_tags.rs b/easytier-contrib/easytier-uptime/src/db/entity/node_tags.rs new file mode 100644 index 0000000..941eb52 --- /dev/null +++ b/easytier-contrib/easytier-uptime/src/db/entity/node_tags.rs @@ -0,0 +1,32 @@ +//! `SeaORM` Entity for node tags + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "node_tags")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub node_id: i32, + pub tag: String, + pub created_at: DateTimeWithTimeZone, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::shared_nodes::Entity", + from = "Column::NodeId", + to = "super::shared_nodes::Column::Id" + )] + SharedNodes, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::SharedNodes.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/easytier-contrib/easytier-uptime/src/db/entity/prelude.rs b/easytier-contrib/easytier-uptime/src/db/entity/prelude.rs index c1b7ed1..a291dae 100644 --- a/easytier-contrib/easytier-uptime/src/db/entity/prelude.rs +++ b/easytier-contrib/easytier-uptime/src/db/entity/prelude.rs @@ -1,4 +1,5 @@ //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0 pub use super::health_records::Entity as HealthRecords; +pub use super::node_tags::Entity as NodeTags; pub use super::shared_nodes::Entity as SharedNodes; diff --git a/easytier-contrib/easytier-uptime/src/db/entity/shared_nodes.rs b/easytier-contrib/easytier-uptime/src/db/entity/shared_nodes.rs index e5baa11..38de3ea 100644 --- a/easytier-contrib/easytier-uptime/src/db/entity/shared_nodes.rs +++ b/easytier-contrib/easytier-uptime/src/db/entity/shared_nodes.rs @@ -33,6 +33,9 @@ pub struct Model { pub enum Relation { #[sea_orm(has_many = "super::health_records::Entity")] HealthRecords, + // add relation to node_tags + #[sea_orm(has_many = "super::node_tags::Entity")] + NodeTags, } impl Related for Entity { @@ -41,4 +44,10 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::NodeTags.def() + } +} + impl ActiveModelBehavior for ActiveModel {} diff --git a/easytier-contrib/easytier-uptime/src/db/operations.rs b/easytier-contrib/easytier-uptime/src/db/operations.rs index 94af0f5..9ff5d66 100644 --- a/easytier-contrib/easytier-uptime/src/db/operations.rs +++ b/easytier-contrib/easytier-uptime/src/db/operations.rs @@ -4,6 +4,7 @@ use crate::db::Db; use crate::db::HealthStats; use crate::db::HealthStatus; use sea_orm::*; +use std::collections::{HashMap, HashSet}; /// 节点管理操作 pub struct NodeOperations; @@ -229,6 +230,128 @@ impl HealthOperations { Ok(result.rows_affected) } } +impl NodeOperations { + /// 获取节点的全部标签 + pub async fn get_node_tags(db: &Db, node_id: i32) -> Result, DbErr> { + let tags = node_tags::Entity::find() + .filter(node_tags::Column::NodeId.eq(node_id)) + .all(db.orm_db()) + .await?; + Ok(tags.into_iter().map(|m| m.tag).collect()) + } + + /// 批量获取节点的标签映射 + pub async fn get_nodes_tags_map( + db: &Db, + node_ids: &[i32], + ) -> Result>, DbErr> { + if node_ids.is_empty() { + return Ok(HashMap::new()); + } + let tags = node_tags::Entity::find() + .filter(node_tags::Column::NodeId.is_in(node_ids.to_vec())) + .order_by_asc(node_tags::Column::NodeId) + .all(db.orm_db()) + .await?; + let mut map: HashMap> = HashMap::new(); + for t in tags { + map.entry(t.node_id).or_default().push(t.tag); + } + Ok(map) + } + + /// 使用标签过滤节点(返回节点ID) + pub async fn filter_node_ids_by_tag(db: &Db, tag: &str) -> Result, DbErr> { + let tagged = node_tags::Entity::find() + .filter(node_tags::Column::Tag.eq(tag)) + .all(db.orm_db()) + .await?; + Ok(tagged.into_iter().map(|m| m.node_id).collect()) + } + + /// 设置节点标签(替换为给定集合) + pub async fn set_node_tags(db: &Db, node_id: i32, tags: Vec) -> Result<(), DbErr> { + // 去重与清理空白 + let mut set: HashSet = HashSet::new(); + for tag in tags.into_iter() { + let trimmed = tag.trim(); + if !trimmed.is_empty() { + set.insert(trimmed.to_string()); + } + } + + // 取出当前标签 + let existing = node_tags::Entity::find() + .filter(node_tags::Column::NodeId.eq(node_id)) + .all(db.orm_db()) + .await?; + + let existing_set: HashSet = existing.iter().map(|m| m.tag.clone()).collect(); + + // 需要删除的 + let to_delete: Vec = existing + .iter() + .filter(|m| !set.contains(&m.tag)) + .map(|m| m.id) + .collect(); + + // 需要新增的 + let to_insert: Vec = set + .into_iter() + .filter(|t| !existing_set.contains(t)) + .collect(); + + // 执行删除 + if !to_delete.is_empty() { + node_tags::Entity::delete_many() + .filter(node_tags::Column::Id.is_in(to_delete)) + .exec(db.orm_db()) + .await?; + } + + // 执行新增 + for t in to_insert { + let now = chrono::Utc::now().fixed_offset(); + let am = node_tags::ActiveModel { + id: NotSet, + node_id: Set(node_id), + tag: Set(t), + created_at: Set(now), + }; + node_tags::Entity::insert(am).exec(db.orm_db()).await?; + } + + Ok(()) + } + + // 新增:获取所有唯一标签(按字母排序) + pub async fn get_all_tags(db: &Db) -> Result, DbErr> { + let rows = node_tags::Entity::find().all(db.orm_db()).await?; + let mut set: HashSet = HashSet::new(); + for r in rows { + set.insert(r.tag); + } + let mut list: Vec = set.into_iter().collect(); + list.sort(); + Ok(list) + } + + // 新增:使用多标签(OR 语义)过滤节点,返回匹配的节点ID + pub async fn filter_node_ids_by_tags_any(db: &Db, tags: &[String]) -> Result, DbErr> { + if tags.is_empty() { + return Ok(vec![]); + } + let tagged = node_tags::Entity::find() + .filter(node_tags::Column::Tag.is_in(tags.to_vec())) + .all(db.orm_db()) + .await?; + let mut set: HashSet = HashSet::new(); + for m in tagged { + set.insert(m.node_id); + } + Ok(set.into_iter().collect()) + } +} #[cfg(test)] mod tests { diff --git a/easytier-contrib/easytier-uptime/src/main.rs b/easytier-contrib/easytier-uptime/src/main.rs index 910814c..8177513 100644 --- a/easytier-contrib/easytier-uptime/src/main.rs +++ b/easytier-contrib/easytier-uptime/src/main.rs @@ -11,6 +11,7 @@ use api::routes::create_routes; use clap::Parser; use config::AppConfig; use db::{operations::NodeOperations, Db}; +use easytier::utils::init_logger; use health_checker::HealthChecker; use health_checker_manager::HealthCheckerManager; use std::env; @@ -36,18 +37,7 @@ async fn main() -> anyhow::Result<()> { let config = AppConfig::default(); // 初始化日志 - tracing_subscriber::fmt() - .with_max_level(match config.logging.level.as_str() { - "debug" => tracing::Level::DEBUG, - "info" => tracing::Level::INFO, - "warn" => tracing::Level::WARN, - "error" => tracing::Level::ERROR, - _ => tracing::Level::INFO, - }) - .with_target(false) - .with_thread_ids(true) - .with_env_filter(EnvFilter::new("easytier_uptime")) - .init(); + let _ = init_logger(&config.logging, false); // 解析命令行参数 let args = Args::parse(); diff --git a/easytier-contrib/easytier-uptime/src/migrator/m20250101_000002_create_node_tags.rs b/easytier-contrib/easytier-uptime/src/migrator/m20250101_000002_create_node_tags.rs new file mode 100644 index 0000000..6fa6f46 --- /dev/null +++ b/easytier-contrib/easytier-uptime/src/migrator/m20250101_000002_create_node_tags.rs @@ -0,0 +1,119 @@ +use sea_orm_migration::{prelude::*, schema::*}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[derive(DeriveIden)] +enum NodeTags { + Table, + Id, + NodeId, + Tag, + CreatedAt, +} + +#[derive(DeriveIden)] +enum SharedNodes { + Table, + Id, +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // 创建 node_tags 表 + manager + .create_table( + Table::create() + .table(NodeTags::Table) + .if_not_exists() + .col(pk_auto(NodeTags::Id).not_null()) + .col(integer(NodeTags::NodeId).not_null()) + .col(string(NodeTags::Tag).not_null()) + .col( + timestamp_with_time_zone(NodeTags::CreatedAt) + .not_null() + .default(Expr::current_timestamp()), + ) + .foreign_key( + ForeignKey::create() + .name("fk_node_tags_node") + .from(NodeTags::Table, NodeTags::NodeId) + .to(SharedNodes::Table, SharedNodes::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + // 索引:NodeId + manager + .create_index( + Index::create() + .name("idx_node_tags_node") + .table(NodeTags::Table) + .col(NodeTags::NodeId) + .to_owned(), + ) + .await?; + + // 索引:Tag + manager + .create_index( + Index::create() + .name("idx_node_tags_tag") + .table(NodeTags::Table) + .col(NodeTags::Tag) + .to_owned(), + ) + .await?; + + // 唯一索引:每个节点的标签唯一 + manager + .create_index( + Index::create() + .name("uniq_node_tag_per_node") + .table(NodeTags::Table) + .col(NodeTags::NodeId) + .col(NodeTags::Tag) + .unique() + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // 先删除索引 + manager + .drop_index( + Index::drop() + .name("idx_node_tags_node") + .table(NodeTags::Table) + .to_owned(), + ) + .await?; + manager + .drop_index( + Index::drop() + .name("idx_node_tags_tag") + .table(NodeTags::Table) + .to_owned(), + ) + .await?; + manager + .drop_index( + Index::drop() + .name("uniq_node_tag_per_node") + .table(NodeTags::Table) + .to_owned(), + ) + .await?; + + manager + .drop_table(Table::drop().table(NodeTags::Table).to_owned()) + .await + } +} diff --git a/easytier-contrib/easytier-uptime/src/migrator/mod.rs b/easytier-contrib/easytier-uptime/src/migrator/mod.rs index 1d492e9..3754bd5 100644 --- a/easytier-contrib/easytier-uptime/src/migrator/mod.rs +++ b/easytier-contrib/easytier-uptime/src/migrator/mod.rs @@ -1,12 +1,16 @@ use sea_orm_migration::prelude::*; mod m20250101_000001_create_tables; +mod m20250101_000002_create_node_tags; pub struct Migrator; #[async_trait::async_trait] impl MigratorTrait for Migrator { fn migrations() -> Vec> { - vec![Box::new(m20250101_000001_create_tables::Migration)] + vec![ + Box::new(m20250101_000001_create_tables::Migration), + Box::new(m20250101_000002_create_node_tags::Migration), + ] } } diff --git a/easytier/src/common/config.rs b/easytier/src/common/config.rs index 85d17e7..c9f4745 100644 --- a/easytier/src/common/config.rs +++ b/easytier/src/common/config.rs @@ -321,9 +321,9 @@ pub struct ConsoleLoggerConfig { #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, derive_builder::Builder)] pub struct LoggingConfig { #[builder(setter(into, strip_option), default = None)] - file_logger: Option, + pub file_logger: Option, #[builder(setter(into, strip_option), default = None)] - console_logger: Option, + pub console_logger: Option, } impl LoggingConfigLoader for &LoggingConfig { diff --git a/easytier/src/connector/manual.rs b/easytier/src/connector/manual.rs index 6d2696d..a25f0fb 100644 --- a/easytier/src/connector/manual.rs +++ b/easytier/src/connector/manual.rs @@ -16,7 +16,7 @@ use tokio::{ use crate::{ common::{dns::socket_addrs, join_joinset_background, PeerId}, - peers::peer_conn::PeerConnId, + peers::{peer_conn::PeerConnId, peer_map::PeerMap}, proto::{ api::instance::{ Connector, ConnectorManageRpc, ConnectorStatus, ListConnectorRequest, @@ -194,16 +194,22 @@ impl ManualConnectorManager { tracing::warn!("peer manager is gone, exit"); break; }; - for x in pm.get_peer_map().get_alive_conns().iter().map(|x| { - x.tunnel - .clone() - .unwrap_or_default() - .remote_addr - .unwrap_or_default() - .to_string() - }) { - data.alive_conn_urls.insert(x); - } + let fill_alive_urls_with_peer_map = |peer_map: &PeerMap| { + for x in peer_map.get_alive_conns().iter().map(|x| { + x.tunnel + .clone() + .unwrap_or_default() + .remote_addr + .unwrap_or_default() + .to_string() + }) { + data.alive_conn_urls.insert(x); + } + }; + + fill_alive_urls_with_peer_map(&pm.get_peer_map()); + fill_alive_urls_with_peer_map(&pm.get_foreign_network_client().get_peer_map()); + continue; } Err(RecvError::Closed) => { diff --git a/easytier/src/peers/peer_ospf_route.rs b/easytier/src/peers/peer_ospf_route.rs index 75da7b0..4756af0 100644 --- a/easytier/src/peers/peer_ospf_route.rs +++ b/easytier/src/peers/peer_ospf_route.rs @@ -2306,7 +2306,7 @@ impl RouteSessionManager { service_impl.update_foreign_network_owner_map(); } - tracing::info!( + tracing::debug!( "handling sync_route_info rpc: from_peer_id: {:?}, is_initiator: {:?}, peer_infos: {:?}, conn_bitmap: {:?}, synced_route_info: {:?} session: {:?}, new_route_table: {:?}", from_peer_id, is_initiator, peer_infos, conn_bitmap, service_impl.synced_route_info, session, service_impl.route_table);