- 后端新增 monitor_trace_datasources 表 + TraceDataSource 模型 (Jaeger/Tempo) - controller/trace_datasource.go — CRUD + TestConnection + SetDefault (7 路由) - controller/trace_proxy.go — Jaeger API 代理 + 响应格式转换 (Trace/服务/Span) - GET /traces — Trace 列表 (代理 Jaeger /api/traces) - GET /traces/services — 服务列表 + P95/P99/errorRate 聚合 - GET /traces/:traceId — Trace 详情 (Span 树转换) - Demo 数据兜底: 无数据源时返回模拟数据 (6 Trace + 5 服务 + 5 Span) - 前端 traceApi 从 mock 改真实 HTTP 调用 - 前端新增 trace-datasource.ts 数据源管理 API - ROADMAP/TDD_PLAN/PROJECT_EVALUATION 文档同步更新 - 综合评分 90→91, 功能完整性 92→94 (mock API 清零)
222 lines
6.8 KiB
Go
222 lines
6.8 KiB
Go
package controller
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"ezops/model"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// TraceDSHandler 链路追踪数据源管理 // Logan.Li
|
|
type TraceDSHandler struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
func NewTraceDSHandler(db *gorm.DB) *TraceDSHandler {
|
|
return &TraceDSHandler{db: db}
|
|
}
|
|
|
|
// List GET /api/v1/trace-datasources?type=jaeger
|
|
func (h *TraceDSHandler) List(c *gin.Context) {
|
|
q := h.db.Order("is_default DESC, id ASC")
|
|
if t := c.Query("type"); t != "" {
|
|
q = q.Where("type = ?", t)
|
|
}
|
|
var sources []model.TraceDataSource
|
|
if err := q.Find(&sources).Error; err != nil {
|
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
|
return
|
|
}
|
|
if sources == nil {
|
|
sources = []model.TraceDataSource{}
|
|
}
|
|
c.JSON(200, model.Response{Code: 200, Data: sources})
|
|
}
|
|
|
|
// Get GET /api/v1/trace-datasources/:id
|
|
func (h *TraceDSHandler) Get(c *gin.Context) {
|
|
var ds model.TraceDataSource
|
|
if err := h.db.First(&ds, c.Param("id")).Error; err != nil {
|
|
c.JSON(404, model.Response{Code: 404, Message: "数据源不存在"})
|
|
return
|
|
}
|
|
c.JSON(200, model.Response{Code: 200, Data: ds})
|
|
}
|
|
|
|
// Create POST /api/v1/trace-datasources
|
|
func (h *TraceDSHandler) Create(c *gin.Context) {
|
|
var req model.CreateTraceDSReq
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(400, model.Response{Code: 400, Message: err.Error()})
|
|
return
|
|
}
|
|
req.Type = strings.ToLower(req.Type)
|
|
if req.Type != "jaeger" && req.Type != "tempo" {
|
|
c.JSON(400, model.Response{Code: 400, Message: "type 必须为 jaeger 或 tempo"})
|
|
return
|
|
}
|
|
if req.IsDefault {
|
|
h.db.Model(&model.TraceDataSource{}).Where("type = ? AND is_default = ?", req.Type, true).Update("is_default", false)
|
|
}
|
|
ds := model.TraceDataSource{
|
|
Name: req.Name, Type: req.Type, URL: strings.TrimRight(req.URL, "/"),
|
|
BasicAuth: req.BasicAuth, BasicAuthUser: req.BasicAuthUser, BasicAuthPass: req.BasicAuthPass,
|
|
SkipTLSVerify: req.SkipTLSVerify, IsDefault: req.IsDefault, Status: 1,
|
|
}
|
|
if err := h.db.Create(&ds).Error; err != nil {
|
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
|
return
|
|
}
|
|
c.JSON(200, model.Response{Code: 200, Message: "创建成功", Data: ds})
|
|
}
|
|
|
|
// Update PUT /api/v1/trace-datasources/:id
|
|
func (h *TraceDSHandler) Update(c *gin.Context) {
|
|
var ds model.TraceDataSource
|
|
if err := h.db.First(&ds, c.Param("id")).Error; err != nil {
|
|
c.JSON(404, model.Response{Code: 404, Message: "数据源不存在"})
|
|
return
|
|
}
|
|
var req model.UpdateTraceDSReq
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(400, model.Response{Code: 400, Message: err.Error()})
|
|
return
|
|
}
|
|
updates := map[string]interface{}{}
|
|
if req.Name != nil {
|
|
updates["name"] = *req.Name
|
|
}
|
|
if req.Type != nil {
|
|
updates["type"] = strings.ToLower(*req.Type)
|
|
}
|
|
if req.URL != nil {
|
|
updates["url"] = strings.TrimRight(*req.URL, "/")
|
|
}
|
|
if req.BasicAuth != nil {
|
|
updates["basic_auth"] = *req.BasicAuth
|
|
}
|
|
if req.BasicAuthUser != nil {
|
|
updates["basic_auth_user"] = *req.BasicAuthUser
|
|
}
|
|
if req.BasicAuthPass != nil && *req.BasicAuthPass != "" {
|
|
updates["basic_auth_pass"] = *req.BasicAuthPass
|
|
}
|
|
if req.SkipTLSVerify != nil {
|
|
updates["skip_tls_verify"] = *req.SkipTLSVerify
|
|
}
|
|
if req.IsDefault != nil {
|
|
if *req.IsDefault {
|
|
h.db.Model(&model.TraceDataSource{}).Where("type = ? AND is_default = ? AND id != ?", ds.Type, true, ds.ID).Update("is_default", false)
|
|
}
|
|
updates["is_default"] = *req.IsDefault
|
|
}
|
|
if req.Status != nil {
|
|
updates["status"] = *req.Status
|
|
}
|
|
if len(updates) == 0 {
|
|
c.JSON(200, model.Response{Code: 200, Data: ds})
|
|
return
|
|
}
|
|
if err := h.db.Model(&ds).Updates(updates).Error; err != nil {
|
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
|
return
|
|
}
|
|
h.db.First(&ds, ds.ID)
|
|
c.JSON(200, model.Response{Code: 200, Message: "更新成功", Data: ds})
|
|
}
|
|
|
|
// Delete DELETE /api/v1/trace-datasources/:id
|
|
func (h *TraceDSHandler) Delete(c *gin.Context) {
|
|
if err := h.db.Delete(&model.TraceDataSource{}, c.Param("id")).Error; err != nil {
|
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
|
return
|
|
}
|
|
c.JSON(200, model.Response{Code: 200, Message: "删除成功"})
|
|
}
|
|
|
|
// SetDefault POST /api/v1/trace-datasources/:id/default
|
|
func (h *TraceDSHandler) SetDefault(c *gin.Context) {
|
|
var ds model.TraceDataSource
|
|
if err := h.db.First(&ds, c.Param("id")).Error; err != nil {
|
|
c.JSON(404, model.Response{Code: 404, Message: "数据源不存在"})
|
|
return
|
|
}
|
|
h.db.Model(&model.TraceDataSource{}).Where("type = ? AND is_default = ?", ds.Type, true).Update("is_default", false)
|
|
h.db.Model(&ds).Update("is_default", true)
|
|
c.JSON(200, model.Response{Code: 200, Message: "设置成功"})
|
|
}
|
|
|
|
// TestConnection POST /api/v1/trace-datasources/test
|
|
func (h *TraceDSHandler) TestConnection(c *gin.Context) {
|
|
var req model.TestTraceDSReq
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
// 允许传 id 来测试已有数据源
|
|
id := c.Query("id")
|
|
if id != "" {
|
|
var ds model.TraceDataSource
|
|
if e := h.db.First(&ds, id).Error; e == nil {
|
|
req.URL = ds.URL
|
|
req.Type = ds.Type
|
|
req.BasicAuth = ds.BasicAuth
|
|
req.BasicAuthUser = ds.BasicAuthUser
|
|
req.BasicAuthPass = ds.BasicAuthPass
|
|
req.SkipTLSVerify = ds.SkipTLSVerify
|
|
}
|
|
}
|
|
if req.URL == "" {
|
|
c.JSON(400, model.Response{Code: 400, Message: "url 不能为空"})
|
|
return
|
|
}
|
|
}
|
|
url := strings.TrimRight(req.URL, "/")
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
if req.SkipTLSVerify {
|
|
client.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
|
|
}
|
|
// Jaeger/Tempo 都有 /api/services 端点
|
|
target := url + "/api/services"
|
|
httpReq, _ := http.NewRequest("GET", target, nil)
|
|
if req.BasicAuth && req.BasicAuthUser != "" {
|
|
httpReq.SetBasicAuth(req.BasicAuthUser, req.BasicAuthPass)
|
|
}
|
|
resp, err := client.Do(httpReq)
|
|
if err != nil {
|
|
c.JSON(200, model.Response{Code: 200, Data: map[string]interface{}{"ok": false, "error": err.Error()}})
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
body, _ := io.ReadAll(resp.Body)
|
|
if resp.StatusCode != 200 {
|
|
c.JSON(200, model.Response{Code: 200, Data: map[string]interface{}{"ok": false, "error": "HTTP " + resp.Status}})
|
|
return
|
|
}
|
|
// 尝试解析 JSON 验证有效性
|
|
var tmp interface{}
|
|
if json.Unmarshal(body, &tmp) != nil {
|
|
c.JSON(200, model.Response{Code: 200, Data: map[string]interface{}{"ok": false, "error": "响应非 JSON"}})
|
|
return
|
|
}
|
|
c.JSON(200, model.Response{Code: 200, Data: map[string]interface{}{"ok": true, "message": "连接成功"}})
|
|
}
|
|
|
|
// getDefaultTraceDS 获取默认链路追踪数据源
|
|
func (h *TraceDSHandler) getDefaultTraceDS() (*model.TraceDataSource, error) {
|
|
var ds model.TraceDataSource
|
|
err := h.db.Where("is_default = ? AND status = 1", true).First(&ds).Error
|
|
if err != nil {
|
|
// fallback: 取第一个启用的
|
|
err = h.db.Where("status = 1").First(&ds).Error
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &ds, nil
|
|
}
|