2026-01-15 19:32:33 +08:00
|
|
|
|
package api
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-01-17 16:19:36 +08:00
|
|
|
|
"easyvqd/internal/core/vqd"
|
2026-01-15 19:32:33 +08:00
|
|
|
|
"easyvqd/internal/web/api/static"
|
|
|
|
|
|
"expvar"
|
2026-01-17 16:19:36 +08:00
|
|
|
|
"git.lnton.com/lnton/pkg/orm"
|
2026-01-15 19:32:33 +08:00
|
|
|
|
statics "github.com/gin-contrib/static"
|
|
|
|
|
|
"log/slog"
|
|
|
|
|
|
"net/http"
|
|
|
|
|
|
"os"
|
|
|
|
|
|
"path/filepath"
|
|
|
|
|
|
"runtime"
|
|
|
|
|
|
"runtime/debug"
|
|
|
|
|
|
"sort"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"easyvqd/domain/version/versionapi"
|
|
|
|
|
|
|
|
|
|
|
|
localweb "easyvqd/pkg/web"
|
|
|
|
|
|
|
|
|
|
|
|
"git.lnton.com/lnton/pkg/web"
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
var startRuntime = time.Now()
|
|
|
|
|
|
|
2026-01-17 16:19:36 +08:00
|
|
|
|
// recordErr 记录错误
|
|
|
|
|
|
func recordErr(err error) {
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
panic(err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-15 19:32:33 +08:00
|
|
|
|
func setupRouter(r *gin.Engine, uc *Usecase) {
|
|
|
|
|
|
r.Use(
|
|
|
|
|
|
// 格式化输出到控制台,然后记录到日志
|
|
|
|
|
|
// 此处不做 recover,底层 http.server 也会 recover,但不会输出方便查看的格式
|
|
|
|
|
|
gin.CustomRecovery(func(c *gin.Context, err any) {
|
|
|
|
|
|
slog.Error("panic", "err", err, "stack", string(debug.Stack()))
|
|
|
|
|
|
c.AbortWithStatus(http.StatusInternalServerError)
|
|
|
|
|
|
}),
|
|
|
|
|
|
web.Metrics(),
|
|
|
|
|
|
web.Logger(),
|
|
|
|
|
|
// debug 环境中配合 debug 日志级别,记录请求体与响应体
|
|
|
|
|
|
web.LoggerWithBody(web.DefaultBodyLimit, func(_ *gin.Context) bool {
|
|
|
|
|
|
// true: 表示忽略记录日志
|
|
|
|
|
|
// !debug 表示非调试环境不记录
|
|
|
|
|
|
return !uc.Conf.Debug
|
|
|
|
|
|
}),
|
|
|
|
|
|
)
|
|
|
|
|
|
go web.CountGoroutines(10*time.Minute, 20)
|
|
|
|
|
|
|
|
|
|
|
|
auth := localweb.AuthMiddleware(uc.Conf.Server.HTTP.JwtSecret, uc.Conf.Plugin.HttpAPI+"/extensions/auth", "")
|
|
|
|
|
|
r.Any("/health", web.WrapH(uc.getHealth))
|
|
|
|
|
|
r.GET("/app/metrics/api", web.WrapH(uc.getMetricsAPI))
|
|
|
|
|
|
//快照
|
|
|
|
|
|
dir, _ := os.Getwd()
|
|
|
|
|
|
uploadsDir := filepath.Join(dir, "uploads")
|
|
|
|
|
|
r.Use(statics.Serve("/uploads", statics.LocalFile(uploadsDir, true)))
|
|
|
|
|
|
|
|
|
|
|
|
versionapi.Register(r, uc.Version, auth)
|
|
|
|
|
|
registerConfig(r, ConfigAPI{uc: uc, cfg: uc.Conf})
|
|
|
|
|
|
RegisterHostAPI(r, uc)
|
|
|
|
|
|
RegisterVqdTask(r, uc.VqdTaskAPI)
|
2026-01-17 16:19:36 +08:00
|
|
|
|
if !orm.GetEnabledAutoMigrate() {
|
|
|
|
|
|
recordErr(InitTemplate(uc))
|
|
|
|
|
|
}
|
2026-01-15 19:32:33 +08:00
|
|
|
|
r.NoRoute(func(ctx *gin.Context) {
|
|
|
|
|
|
p := ctx.Request.URL.Path
|
|
|
|
|
|
if strings.HasPrefix(p, "/web/") {
|
|
|
|
|
|
q := ctx.Request.URL.RawQuery
|
|
|
|
|
|
target := "/web/"
|
|
|
|
|
|
if q != "" {
|
|
|
|
|
|
target = target + "?" + q
|
|
|
|
|
|
}
|
|
|
|
|
|
ctx.Redirect(http.StatusTemporaryRedirect, target)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.HasPrefix(p, "/uploads/") {
|
|
|
|
|
|
q := ctx.Request.URL.RawQuery
|
|
|
|
|
|
target := "/uploads/"
|
|
|
|
|
|
if q != "" {
|
|
|
|
|
|
target = target + "?" + q
|
|
|
|
|
|
}
|
|
|
|
|
|
ctx.Redirect(http.StatusTemporaryRedirect, target)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if strings.HasPrefix(p, "/extensions/easyvqd") {
|
|
|
|
|
|
// 改为前缀替换并在当前请求内重新分发,而不是重定向
|
|
|
|
|
|
newPath := strings.TrimPrefix(p, "/extensions/easyvqd")
|
|
|
|
|
|
ctx.Request.URL.Path = newPath
|
|
|
|
|
|
r.HandleContext(ctx)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
ctx.AbortWithStatus(http.StatusNotFound)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 直接返回静态文件系统中的入口页
|
|
|
|
|
|
r.StaticFS("/web/", static.FileSystem())
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type getHealthOutput struct {
|
|
|
|
|
|
Version string `json:"version"`
|
|
|
|
|
|
StartAt time.Time `json:"start_at"`
|
|
|
|
|
|
GitBranch string `json:"git_branch"`
|
|
|
|
|
|
GitHash string `json:"git_hash"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (uc *Usecase) getHealth(_ *gin.Context, _ *struct{}) (getHealthOutput, error) {
|
|
|
|
|
|
return getHealthOutput{
|
|
|
|
|
|
Version: uc.Conf.BuildVersion,
|
|
|
|
|
|
GitBranch: strings.Trim(expvar.Get("git_branch").String(), `"`),
|
|
|
|
|
|
GitHash: strings.Trim(expvar.Get("git_hash").String(), `"`),
|
|
|
|
|
|
StartAt: startRuntime,
|
|
|
|
|
|
}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type getMetricsAPIOutput struct {
|
|
|
|
|
|
RealTimeRequests int64 `json:"real_time_requests"` // 实时请求数
|
|
|
|
|
|
TotalRequests int64 `json:"total_requests"` // 总请求数
|
|
|
|
|
|
TotalResponses int64 `json:"total_responses"` // 总响应数
|
|
|
|
|
|
RequestTop []KV `json:"request_top"` // 请求TOP
|
|
|
|
|
|
StatusCodeTop []KV `json:"status_code_top"` // 状态码TOP
|
|
|
|
|
|
Goroutines any `json:"goroutines"` // 协程数量
|
|
|
|
|
|
NumGC uint32 `json:"num_gc"` // gc 次数
|
|
|
|
|
|
SysAlloc uint64 `json:"sys_alloc"` // 内存占用
|
|
|
|
|
|
StartAt string `json:"start_at"` // 运行时间
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (uc *Usecase) getMetricsAPI(_ *gin.Context, _ *struct{}) (*getMetricsAPIOutput, error) {
|
|
|
|
|
|
req := expvar.Get("request").(*expvar.Int).Value()
|
|
|
|
|
|
reqs := expvar.Get("requests").(*expvar.Int).Value()
|
|
|
|
|
|
resps := expvar.Get("responses").(*expvar.Int).Value()
|
|
|
|
|
|
urls := expvar.Get(`requestURLs`).(*expvar.Map)
|
|
|
|
|
|
status := expvar.Get(`statusCodes`).(*expvar.Map)
|
|
|
|
|
|
u := sortExpvarMap(urls, 15)
|
|
|
|
|
|
s := sortExpvarMap(status, 15)
|
|
|
|
|
|
g := expvar.Get("goroutine_num").(expvar.Func)
|
|
|
|
|
|
|
|
|
|
|
|
var stats runtime.MemStats
|
|
|
|
|
|
runtime.ReadMemStats(&stats)
|
|
|
|
|
|
|
|
|
|
|
|
return &getMetricsAPIOutput{
|
|
|
|
|
|
RealTimeRequests: req,
|
|
|
|
|
|
TotalRequests: reqs,
|
|
|
|
|
|
TotalResponses: resps,
|
|
|
|
|
|
RequestTop: u,
|
|
|
|
|
|
StatusCodeTop: s,
|
|
|
|
|
|
Goroutines: g(),
|
|
|
|
|
|
NumGC: stats.NumGC,
|
|
|
|
|
|
SysAlloc: stats.Sys,
|
|
|
|
|
|
StartAt: startRuntime.Format(time.DateTime),
|
|
|
|
|
|
}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type KV struct {
|
|
|
|
|
|
Key string
|
|
|
|
|
|
Value int64
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-17 16:19:36 +08:00
|
|
|
|
func InitTemplate(uc *Usecase) error {
|
|
|
|
|
|
cfg := uc.Conf
|
2026-01-23 18:05:36 +08:00
|
|
|
|
inPlan := vqd.VqdTimeTemplate{
|
2026-01-17 16:19:36 +08:00
|
|
|
|
Enable: true,
|
|
|
|
|
|
IsDefault: true,
|
2026-01-23 18:05:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
inTemplate := vqd.VqdTaskTemplate{
|
|
|
|
|
|
|
|
|
|
|
|
Name: "默认",
|
|
|
|
|
|
Des: "默认诊断模板。",
|
|
|
|
|
|
IsDefault: true,
|
2026-01-17 16:19:36 +08:00
|
|
|
|
VqdConfig: vqd.VqdConfig{
|
|
|
|
|
|
Enable: true,
|
|
|
|
|
|
FrmNum: cfg.VqdConfig.FrmNum,
|
|
|
|
|
|
IsDeepLearn: cfg.VqdConfig.IsDeepLearn,
|
|
|
|
|
|
},
|
|
|
|
|
|
VqdLgtDark: vqd.VqdLgtDark{
|
|
|
|
|
|
Enable: true,
|
|
|
|
|
|
DarkThr: cfg.VqdLgtDark.DarkThr,
|
|
|
|
|
|
LgtThr: cfg.VqdLgtDark.LgtThr,
|
|
|
|
|
|
LgtDarkAbnNumRatio: cfg.VqdLgtDark.LgtDarkAbnNumRatio,
|
|
|
|
|
|
},
|
|
|
|
|
|
VqdBlue: vqd.VqdBlue{
|
|
|
|
|
|
Enable: true,
|
|
|
|
|
|
BlueThr: cfg.VqdBlue.BlueThr,
|
|
|
|
|
|
BlueAbnNumRatio: cfg.VqdBlue.BlueAbnNumRatio,
|
|
|
|
|
|
},
|
|
|
|
|
|
VqdClarity: vqd.VqdClarity{
|
|
|
|
|
|
Enable: true,
|
|
|
|
|
|
ClarityThr: cfg.VqdClarity.ClarityThr,
|
|
|
|
|
|
ClarityAbnNumRatio: cfg.VqdClarity.ClarityAbnNumRatio,
|
|
|
|
|
|
},
|
|
|
|
|
|
VqdShark: vqd.VqdShark{
|
|
|
|
|
|
Enable: true,
|
|
|
|
|
|
SharkThr: cfg.VqdShark.SharkThr,
|
|
|
|
|
|
SharkAbnNumRatio: cfg.VqdShark.SharkAbnNumRatio,
|
|
|
|
|
|
},
|
|
|
|
|
|
VqdFreeze: vqd.VqdFreeze{
|
|
|
|
|
|
Enable: true,
|
|
|
|
|
|
FreezeThr: cfg.VqdFreeze.FreezeThr,
|
|
|
|
|
|
FreezeAbnNumRatio: cfg.VqdFreeze.FreezeAbnNumRatio,
|
|
|
|
|
|
},
|
|
|
|
|
|
VqdColor: vqd.VqdColor{
|
|
|
|
|
|
Enable: true,
|
|
|
|
|
|
ColorThr: cfg.VqdColor.ColorThr,
|
|
|
|
|
|
ColorAbnNumRatio: cfg.VqdColor.ColorAbnNumRatio,
|
|
|
|
|
|
},
|
|
|
|
|
|
VqdOcclusion: vqd.VqdOcclusion{
|
|
|
|
|
|
Enable: true,
|
|
|
|
|
|
OcclusionThr: cfg.VqdOcclusion.OcclusionThr,
|
|
|
|
|
|
OcclusionAbnNumRatio: cfg.VqdOcclusion.OcclusionAbnNumRatio,
|
|
|
|
|
|
},
|
|
|
|
|
|
VqdNoise: vqd.VqdNoise{
|
|
|
|
|
|
Enable: true,
|
|
|
|
|
|
NoiseThr: cfg.VqdNoise.NoiseThr,
|
|
|
|
|
|
NoiseAbnNumRatio: cfg.VqdNoise.NoiseAbnNumRatio,
|
|
|
|
|
|
},
|
|
|
|
|
|
VqdContrast: vqd.VqdContrast{
|
|
|
|
|
|
Enable: true,
|
|
|
|
|
|
CtraLowThr: cfg.VqdContrast.CtraLowThr,
|
|
|
|
|
|
CtraHighThr: cfg.VqdContrast.CtraHighThr,
|
|
|
|
|
|
CtraAbnNumRatio: cfg.VqdContrast.CtraAbnNumRatio,
|
|
|
|
|
|
},
|
|
|
|
|
|
VqdMosaic: vqd.VqdMosaic{
|
|
|
|
|
|
Enable: true,
|
|
|
|
|
|
MosaicThr: cfg.VqdMosaic.MosaicThr,
|
|
|
|
|
|
MosaicAbnNumRatio: cfg.VqdMosaic.MosaicAbnNumRatio,
|
|
|
|
|
|
},
|
|
|
|
|
|
VqdFlower: vqd.VqdFlower{
|
|
|
|
|
|
Enable: true,
|
|
|
|
|
|
FlowerThr: cfg.VqdFlower.FlowerThr,
|
|
|
|
|
|
FlowerAbnNumRatio: cfg.VqdFlower.FlowerAbnNumRatio,
|
|
|
|
|
|
MosaicThr: cfg.VqdFlower.MosaicThr,
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
2026-01-23 18:05:36 +08:00
|
|
|
|
inTemplate.ID = 1
|
|
|
|
|
|
inTemplate.Model.CreatedAt = orm.Time{Time: time.Now()}
|
|
|
|
|
|
if err := uc.VqdTaskCore.FirstOrCreateTaskTemplate(&inTemplate); err != nil {
|
|
|
|
|
|
slog.Error("FirstOrCreateTaskTemplate", "err", err)
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
inPlan.Name = "每天"
|
|
|
|
|
|
inPlan.Model.ID = 1
|
|
|
|
|
|
inPlan.Model.CreatedAt = orm.Time{Time: time.Now()}
|
|
|
|
|
|
inPlan.Plans = "111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"
|
|
|
|
|
|
inPlan.Des = "每天分析"
|
|
|
|
|
|
if err := uc.VqdTaskCore.FirstOrCreateTimeTemplate(&inPlan); err != nil {
|
|
|
|
|
|
slog.Error("FirstOrCreateTimeTemplate", "err", err)
|
2026-01-17 16:19:36 +08:00
|
|
|
|
return err
|
|
|
|
|
|
}
|
2026-01-23 18:05:36 +08:00
|
|
|
|
inPlan.Name = "工作日"
|
|
|
|
|
|
inPlan.Model.ID = 2
|
|
|
|
|
|
inPlan.Model.CreatedAt = orm.Time{Time: time.Now()}
|
|
|
|
|
|
inPlan.Plans = "111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000"
|
|
|
|
|
|
inPlan.Des = "工作日分析"
|
|
|
|
|
|
if err := uc.VqdTaskCore.FirstOrCreateTimeTemplate(&inPlan); err != nil {
|
|
|
|
|
|
slog.Error("FirstOrCreateTimeTemplate", "err", err)
|
2026-01-17 16:19:36 +08:00
|
|
|
|
return err
|
|
|
|
|
|
}
|
2026-01-23 18:05:36 +08:00
|
|
|
|
inPlan.Name = "双休日"
|
|
|
|
|
|
inPlan.Model.ID = 3
|
|
|
|
|
|
inPlan.Model.CreatedAt = orm.Time{Time: time.Now()}
|
|
|
|
|
|
inPlan.Plans = "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000111111111111111111111111111111111111111111111111"
|
|
|
|
|
|
inPlan.Des = "休息日分析"
|
|
|
|
|
|
if err := uc.VqdTaskCore.FirstOrCreateTimeTemplate(&inPlan); err != nil {
|
|
|
|
|
|
slog.Error("FirstOrCreateTimeTemplate", "err", err)
|
2026-01-17 16:19:36 +08:00
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
2026-01-15 19:32:33 +08:00
|
|
|
|
func sortExpvarMap(data *expvar.Map, top int) []KV {
|
|
|
|
|
|
kvs := make([]KV, 0, 8)
|
|
|
|
|
|
data.Do(func(kv expvar.KeyValue) {
|
|
|
|
|
|
kvs = append(kvs, KV{
|
|
|
|
|
|
Key: kv.Key,
|
|
|
|
|
|
Value: kv.Value.(*expvar.Int).Value(),
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
sort.Slice(kvs, func(i, j int) bool {
|
|
|
|
|
|
return kvs[i].Value > kvs[j].Value
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
idx := top
|
|
|
|
|
|
if l := len(kvs); l < top {
|
|
|
|
|
|
idx = len(kvs)
|
|
|
|
|
|
}
|
|
|
|
|
|
return kvs[:idx]
|
|
|
|
|
|
}
|