package api import ( "easyvqd/internal/core/vqd" "easyvqd/internal/web/api/static" "expvar" "git.lnton.com/lnton/pkg/orm" 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() // recordErr 记录错误 func recordErr(err error) { if err != nil { panic(err) } } 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) if !orm.GetEnabledAutoMigrate() { recordErr(InitTemplate(uc)) } 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 } func InitTemplate(uc *Usecase) error { cfg := uc.Conf in := vqd.VqdTaskTemplate{ Enable: true, IsDefault: true, 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, }, } in.Name = "每天" in.Model.ID = 1 in.Model.CreatedAt = orm.Time{Time: time.Now()} in.Plans = "111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111" in.Des = "每天分析,启用全部分析模块。" if err := uc.VqdTaskCore.FirstOrCreateTemplate(&in); err != nil { slog.Error("FirstOrCreateTemplate", "err", err) return err } in.Name = "工作日" in.Model.ID = 2 in.Model.CreatedAt = orm.Time{Time: time.Now()} in.Plans = "111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000" in.Des = "工作日分析,启用全部分析模块。" if err := uc.VqdTaskCore.FirstOrCreateTemplate(&in); err != nil { slog.Error("FirstOrCreateTemplate", "err", err) return err } in.Name = "双休日" in.Model.ID = 3 in.Model.CreatedAt = orm.Time{Time: time.Now()} in.Plans = "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000111111111111111111111111111111111111111111111111" in.Des = "休息日分析,启用全部分析模块。" if err := uc.VqdTaskCore.FirstOrCreateTemplate(&in); err != nil { slog.Error("FirstOrCreateTemplate", "err", err) return err } return nil } 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] }