144 lines
4.0 KiB
Go
144 lines
4.0 KiB
Go
|
|
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)
|
||
|
|
}
|