基于 WEB-UI-Development-Guide 标准的整体升级,并更名 LokiDeploy → opsx-loki。 前端(Gin+React+shadcn/ui): - 按 02/07 章重构:页面域化拆分(无超 400 行文件)、PageContainer/ListQuery、 反馈分级(成功静默/失败 toast/危险确认)、401 单飞刷新+队列重放 - 暗色模式(next-themes)+ 语义 Token(--primary 品牌色/success/warning/info) - 部署向导(状态推导四步:环境→存储→Loki→采集,内嵌表单闭环) - 日志查询页:LogQL 标签构建器/拖拽直方图/日志上下文/Live Tail/查询历史 - 按用户+标签策略的查询授权(服务端强制合并策略) 后端(Go+Gin+JSON DB): - 简单用户管理(仅 admin,无 RBAC)+ TokenVersion 会话吊销 + 登录 IP 限流 - LDAP 接入:配置(AES 加密存储/hasPassword 脱敏)/测试连接/批量导入/登录 - 存储配置测试连接(SigV4 零依赖)+ 桶不存在自动创建 - Loki 三模式部署修复:SSH 密钥认证、3.x 配置模板、ruler 对象存储嵌套块、 凭证解密、孤儿 deploying 状态恢复、Grafana 部署(可选默认关)+面板导入 - DaemonSet 采集纯 YAML(去 helm 依赖)+ DNS 自愈回退 ClusterIP + 集群名标签 - 日志查询代理:cluster-svc 经 kube-apiserver proxy/url 直连 + cursor 翻页 - 统一 log 命名空间;部署就绪探活;Grafana 访问地址自动记录 修复:run.sh 增量编译检测、Sanitized 脱敏补全、LokiDeployment Update 白名单、 健康检查 CreatedAt 零值等 10+ 存量 bug。 数据兼容:DB 沿用旧 lokideploy.json(新名优先);localStorage 一次性迁移; HKDF 加密前缀保留(已加密凭证不受影响)。
274 lines
7.0 KiB
Go
274 lines
7.0 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"opsxloki/models"
|
|
"opsxloki/repository"
|
|
"opsxloki/utils"
|
|
)
|
|
|
|
type CollectorService struct{}
|
|
|
|
func NewCollectorService() *CollectorService {
|
|
return &CollectorService{}
|
|
}
|
|
|
|
type CreateCollectorRequest struct {
|
|
Name string
|
|
Type string
|
|
LokiDeploymentID uint
|
|
HostID *uint
|
|
ClusterID *uint
|
|
TargetDeployment string
|
|
Namespace string
|
|
LogPath string
|
|
Labels string
|
|
}
|
|
|
|
// fillAssociations 填充关联的 Loki / Host / Cluster(用于配置生成与展示)
|
|
func (s *CollectorService) fillAssociations(c *models.Collector) {
|
|
db := repository.GetDB()
|
|
db.RLock()
|
|
defer db.RUnlock()
|
|
for i := range db.Deployments {
|
|
if db.Deployments[i].ID == c.LokiDeploymentID {
|
|
d := db.Deployments[i]
|
|
c.LokiDeployment = &d
|
|
break
|
|
}
|
|
}
|
|
if c.HostID != nil {
|
|
for i := range db.Hosts {
|
|
if db.Hosts[i].ID == *c.HostID {
|
|
h := db.Hosts[i]
|
|
c.Host = &h
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if c.ClusterID != nil {
|
|
for i := range db.Clusters {
|
|
if db.Clusters[i].ID == *c.ClusterID {
|
|
cl := db.Clusters[i]
|
|
c.Cluster = &cl
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// generateConfigSnapshot 按类型生成配置快照
|
|
func (s *CollectorService) generateConfigSnapshot(c *models.Collector) string {
|
|
endpoint := ""
|
|
if c.LokiDeployment != nil {
|
|
endpoint = c.LokiDeployment.Endpoint
|
|
}
|
|
clusterName := "host"
|
|
if c.Cluster != nil && c.Cluster.Name != "" {
|
|
clusterName = c.Cluster.Name
|
|
}
|
|
switch c.Type {
|
|
case "host-promtail":
|
|
return PromtailConfig(endpoint, c.LogPath, "host", c.Labels)
|
|
case "k8s-daemonset":
|
|
return PromtailHelmValues(endpoint, clusterName, c.Labels)
|
|
case "k8s-fluentbit-sidecar":
|
|
host, port := parseLokiURL(endpoint)
|
|
app := c.TargetDeployment
|
|
ns := c.Namespace
|
|
if ns == "" {
|
|
ns = "default"
|
|
}
|
|
return FluentbitConfig(host, port, c.LogPath, app, ns)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (s *CollectorService) Create(req *CreateCollectorRequest) (*models.Collector, error) {
|
|
switch req.Type {
|
|
case "host-promtail", "k8s-daemonset", "k8s-fluentbit-sidecar":
|
|
default:
|
|
return nil, fmt.Errorf("不支持的采集器类型: %s", req.Type)
|
|
}
|
|
if req.LokiDeploymentID == 0 {
|
|
return nil, fmt.Errorf("请选择关联的 Loki 部署")
|
|
}
|
|
if req.Type == "host-promtail" && (req.HostID == nil || *req.HostID == 0) {
|
|
return nil, fmt.Errorf("主机采集器需要选择目标主机")
|
|
}
|
|
if (req.Type == "k8s-daemonset" || req.Type == "k8s-fluentbit-sidecar") && (req.ClusterID == nil || *req.ClusterID == 0) {
|
|
return nil, fmt.Errorf("K8s 采集器需要选择目标集群")
|
|
}
|
|
if req.Type == "k8s-fluentbit-sidecar" && req.TargetDeployment == "" {
|
|
return nil, fmt.Errorf("Sidecar 采集器需要填写目标 Deployment 名称")
|
|
}
|
|
|
|
refType := "cluster"
|
|
if req.Type == "host-promtail" {
|
|
refType = "host"
|
|
}
|
|
|
|
collector := models.Collector{
|
|
ID: repository.GetDB().NextID("collectors"),
|
|
Name: req.Name,
|
|
Type: req.Type,
|
|
LokiDeploymentID: req.LokiDeploymentID,
|
|
RefType: refType,
|
|
HostID: req.HostID,
|
|
ClusterID: req.ClusterID,
|
|
TargetDeployment: req.TargetDeployment,
|
|
Namespace: req.Namespace,
|
|
LogPath: req.LogPath,
|
|
Labels: req.Labels,
|
|
Status: "pending",
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
|
|
s.fillAssociations(&collector)
|
|
if collector.LokiDeployment == nil {
|
|
return nil, fmt.Errorf("关联的 Loki 部署不存在")
|
|
}
|
|
collector.Config = s.generateConfigSnapshot(&collector)
|
|
|
|
db := repository.GetDB()
|
|
db.Lock()
|
|
db.Collectors = append(db.Collectors, collector)
|
|
db.Unlock()
|
|
db.RequestSave()
|
|
|
|
go s.Install(collector.ID)
|
|
return &collector, nil
|
|
}
|
|
|
|
func (s *CollectorService) Get(id uint) (*models.Collector, error) {
|
|
db := repository.GetDB()
|
|
db.RLock()
|
|
var found *models.Collector
|
|
for i := range db.Collectors {
|
|
if db.Collectors[i].ID == id {
|
|
c := db.Collectors[i]
|
|
found = &c
|
|
break
|
|
}
|
|
}
|
|
db.RUnlock()
|
|
if found == nil {
|
|
return nil, utils.ErrNotFound
|
|
}
|
|
s.fillAssociations(found)
|
|
return found, nil
|
|
}
|
|
|
|
func (s *CollectorService) List(page, pageSize int) ([]models.Collector, int64, error) {
|
|
db := repository.GetDB()
|
|
db.RLock()
|
|
total := int64(len(db.Collectors))
|
|
start := (page - 1) * pageSize
|
|
if start < 0 || start >= len(db.Collectors) {
|
|
db.RUnlock()
|
|
return []models.Collector{}, total, nil
|
|
}
|
|
end := start + pageSize
|
|
if end > len(db.Collectors) {
|
|
end = len(db.Collectors)
|
|
}
|
|
result := make([]models.Collector, end-start)
|
|
copy(result, db.Collectors[start:end])
|
|
db.RUnlock()
|
|
for i := range result {
|
|
s.fillAssociations(&result[i])
|
|
}
|
|
return result, total, nil
|
|
}
|
|
|
|
func (s *CollectorService) Update(id uint, updates map[string]interface{}) error {
|
|
db := repository.GetDB()
|
|
db.Lock()
|
|
defer db.Unlock()
|
|
for i := range db.Collectors {
|
|
if db.Collectors[i].ID == id {
|
|
if v, ok := updates["name"].(string); ok && v != "" {
|
|
db.Collectors[i].Name = v
|
|
}
|
|
if v, ok := updates["status"].(string); ok && v != "" {
|
|
db.Collectors[i].Status = v
|
|
}
|
|
if v, ok := updates["statusMessage"].(string); ok {
|
|
db.Collectors[i].StatusMessage = v
|
|
}
|
|
if v, ok := updates["logPath"].(string); ok {
|
|
db.Collectors[i].LogPath = v
|
|
}
|
|
if v, ok := updates["labels"].(string); ok {
|
|
db.Collectors[i].Labels = v
|
|
}
|
|
db.Collectors[i].UpdatedAt = time.Now()
|
|
db.RequestSave()
|
|
return nil
|
|
}
|
|
}
|
|
return utils.ErrNotFound
|
|
}
|
|
|
|
func (s *CollectorService) Delete(id uint) error {
|
|
collector, err := s.Get(id)
|
|
if err != nil {
|
|
return utils.ErrNotFound
|
|
}
|
|
db := repository.GetDB()
|
|
db.Lock()
|
|
for i, c := range db.Collectors {
|
|
if c.ID == id {
|
|
db.Collectors = append(db.Collectors[:i], db.Collectors[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
db.Unlock()
|
|
db.RequestSave()
|
|
// 异步卸载(用拷贝的 collector,避免删除后取不到)
|
|
go func() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
|
defer cancel()
|
|
NewCollectorDeployer(collector, collector.LokiDeployment, nil).Uninstall(ctx)
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
// Install 异步部署采集器
|
|
func (s *CollectorService) Install(id uint) {
|
|
collector, err := s.Get(id)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if collector.LokiDeployment == nil {
|
|
s.Update(id, map[string]interface{}{"status": "error", "statusMessage": "关联的 Loki 不存在"})
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
|
defer cancel()
|
|
if err := NewCollectorDeployer(collector, collector.LokiDeployment, nil).Install(ctx); err != nil {
|
|
s.Update(id, map[string]interface{}{"status": "error", "statusMessage": err.Error()})
|
|
return
|
|
}
|
|
s.Update(id, map[string]interface{}{"status": "active", "statusMessage": "采集器已部署"})
|
|
}
|
|
|
|
// Uninstall 异步卸载采集器
|
|
func (s *CollectorService) Uninstall(id uint) {
|
|
collector, err := s.Get(id)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if collector.LokiDeployment == nil {
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
|
defer cancel()
|
|
NewCollectorDeployer(collector, collector.LokiDeployment, nil).Uninstall(ctx)
|
|
s.Update(id, map[string]interface{}{"status": "pending", "statusMessage": "采集器已卸载"})
|
|
}
|