package health import ( "context" "fmt" "net/http" "runtime/debug" "strings" "time" "git.apinb.com/ops/files/internal/config" "git.apinb.com/ops/files/internal/impl" "git.apinb.com/ops/files/internal/jobs" "git.apinb.com/ops/files/internal/runtimeinfo" "git.apinb.com/ops/files/internal/storage" "github.com/gin-gonic/gin" "gorm.io/gorm" ) type CheckResult struct { Name string `json:"name"` Required bool `json:"required"` Ready bool `json:"ready"` Message string `json:"message,omitempty"` } type Readiness struct { Ready bool `json:"ready"` CheckedAt time.Time `json:"checked_at"` Checks []CheckResult `json:"checks,omitempty"` Version string `json:"version"` } type FileCounts struct { Total int64 `json:"total"` Ready int64 `json:"ready"` Pending int64 `json:"pending"` } func Evaluate(ctx context.Context) Readiness { result, _ := evaluate(ctx) return result } func evaluate(ctx context.Context) (Readiness, storage.RuntimeStatus) { result := Readiness{Ready: true, CheckedAt: time.Now().UTC(), Version: runtimeVersion()} result.add("config", true, config.Validate()) result.add("database", true, pingDatabase(ctx, impl.DBService)) storageStatus, storageErr := impl.StorageService.Status(ctx) if storageErr == nil && !storageStatus.Writable { storageErr = fmt.Errorf("文件存储当前不可写") } result.add("storage", true, storageErr) worker := jobs.Status() if worker.Running { result.add("pending_upload_cleanup", true, nil) } else { result.add("pending_upload_cleanup", true, fmt.Errorf("过期上传清理任务未运行")) } integrity := jobs.IntegrityStatusSnapshot() if integrity.Running { result.add("integrity_worker", true, nil) } else { result.add("integrity_worker", true, fmt.Errorf("文件完整性任务未运行")) } if integrity.Missing+integrity.SizeMismatch+integrity.ETagMismatch > 0 { result.add("file_integrity", false, fmt.Errorf("发现文件缺失或校验不一致")) } else { result.add("file_integrity", false, nil) } return result, storageStatus } func runtimeVersion() string { info, ok := debug.ReadBuildInfo() if !ok || strings.TrimSpace(info.Main.Version) == "" { return "unknown" } return info.Main.Version } func Ready(c *gin.Context) { result := Evaluate(c.Request.Context()) status := http.StatusOK if !result.Ready { status = http.StatusServiceUnavailable } c.JSON(status, gin.H{"ready": result.Ready}) } func Status(c *gin.Context) { result, storageStatus := evaluate(c.Request.Context()) status := http.StatusOK if !result.Ready { status = http.StatusServiceUnavailable } counts, countErr := fileCounts(c.Request.Context()) if countErr != nil { result.add("file_ledger", true, countErr) status = http.StatusServiceUnavailable } c.JSON(status, gin.H{ "readiness": result, "workers": gin.H{ "pending_upload_cleanup": jobs.Status(), "integrity": jobs.IntegrityStatusSnapshot(), }, "storage": storageStatus, "disk": storageStatus.Disk, "files": counts, "activity": runtimeinfo.Snapshot(), }) } func fileCounts(ctx context.Context) (FileCounts, error) { var counts FileCounts err := impl.DBService.WithContext(ctx).Raw(` SELECT COUNT(*) AS total, COUNT(*) FILTER (WHERE status = 'ready') AS ready, COUNT(*) FILTER (WHERE status = 'pending') AS pending FROM files_object WHERE deleted_at IS NULL`).Scan(&counts).Error return counts, err } func (r *Readiness) add(name string, required bool, err error) { check := CheckResult{Name: name, Required: required, Ready: err == nil} if err != nil { check.Message = strings.TrimSpace(err.Error()) if required { r.Ready = false } } r.Checks = append(r.Checks, check) } func pingDatabase(ctx context.Context, db *gorm.DB) error { if db == nil { return fmt.Errorf("数据库未初始化") } sqlDB, err := db.DB() if err != nil { return err } checkCtx, cancel := context.WithTimeout(ctx, 2*time.Second) defer cancel() return sqlDB.PingContext(checkCtx) }