package api import ( "easyvqd/internal/web/api/static" "expvar" 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() 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) 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 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] }