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);