Files
web_hgtv_server/app/service/healthser/health.go
T
rootandClaude Opus 5.5 7974a42301 Initial commit
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 11:07:42 +08:00

218 lines
5.8 KiB
Go

package healthser
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"math"
"net/http"
"os"
"runtime"
"strings"
"time"
"videoweb/app/api/proto"
"videoweb/app/appg"
"videoweb/pub/common/log"
"videoweb/pub/common/netutil"
"videoweb/pub/common/srvdef"
"videoweb/pub/mongos"
"videoweb/pub/redis"
"github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/disk"
"github.com/shirou/gopsutil/v3/mem"
"go.mongodb.org/mongo-driver/bson"
)
const (
timestampTolerance = 5 * time.Minute
bytesInGB = 1024 * 1024 * 1024
healthGroupID = "69840d508591cdac5a3defcb"
healthSecretKey = "5e47c292e775ebe8bb0d79057cd17778"
)
func HealthCheck(ctx context.Context, req *proto.HealthCheckRequest) (*proto.HealthCheckData, *srvdef.SrvErr) {
if req.GroupID != healthGroupID {
log.Warn("health check invalid groupId", log.Any("groupId", req.GroupID))
return nil, srvdef.New(srvdef.ErrParamError, "invalid groupId")
}
if !isTimestampValid(req.Timestamp) {
log.Warn("health check timestamp expired", log.Any("timestamp", req.Timestamp))
return nil, srvdef.New(srvdef.ErrParamError, "timestamp expired")
}
if !verifySign(req.GroupID, req.Timestamp, healthSecretKey, req.Sign) {
log.Warn("health check invalid sign", log.Any("groupId", req.GroupID))
return nil, srvdef.New(srvdef.ErrParamError, "invalid sign")
}
data := collectServerMetrics()
data.Timestamp = time.Now().Unix()
data.Mysql = disabledStatus("mysql not used")
data.Kafka = disabledStatus("kafka not used")
data.Redis = pingRedis()
data.Mongodb = pingMongo(ctx)
data.Elasticsearch = pingElasticsearch(ctx)
return data, nil
}
func verifySign(groupID string, ts int64, secret string, sign string) bool {
payload := fmt.Sprintf("%s:%d", groupID, ts)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(payload))
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(strings.ToLower(sign)), []byte(expected))
}
func isTimestampValid(ts int64) bool {
now := time.Now().Unix()
diff := now - ts
if diff < 0 {
diff = -diff
}
return diff <= int64(timestampTolerance.Seconds())
}
func disabledStatus(msg string) *proto.ComponentStatus {
return &proto.ComponentStatus{Status: "disabled", Msg: msg}
}
func okStatus() *proto.ComponentStatus {
return &proto.ComponentStatus{Status: "ok", Msg: "success"}
}
func pingRedis() *proto.ComponentStatus {
cli := redis.Client()
if cli == nil {
return &proto.ComponentStatus{Status: "err", Msg: "redis client not initialized"}
}
if err := cli.Ping().Err(); err != nil {
log.Warn("health check redis ping failed", log.E(err))
return &proto.ComponentStatus{Status: "err", Msg: err.Error()}
}
status := okStatus()
status.Master = appg.Conf.Redis.Addr
return status
}
func pingMongo(ctx context.Context) *proto.ComponentStatus {
if mongos.Shop == nil {
return &proto.ComponentStatus{Status: "err", Msg: "mongo client not initialized"}
}
pingCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
if err := mongos.Shop.Database().RunCommand(pingCtx, bson.D{{Key: "ping", Value: 1}}).Err(); err != nil {
log.Warn("health check mongo ping failed", log.E(err))
return &proto.ComponentStatus{Status: "err", Msg: err.Error()}
}
return okStatus()
}
func pingElasticsearch(ctx context.Context) *proto.ComponentStatus {
addr := strings.TrimSpace(appg.Conf.Es.Addr)
if addr == "" {
return disabledStatus("elasticsearch not configured")
}
url := strings.TrimSuffix(addr, "/") + "/_cluster/health"
reqCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, url, nil)
if err != nil {
log.Warn("health check es request build failed", log.E(err))
return &proto.ComponentStatus{Status: "err", Msg: err.Error()}
}
if appg.Conf.Es.User != "" || appg.Conf.Es.Pass != "" {
req.SetBasicAuth(appg.Conf.Es.User, appg.Conf.Es.Pass)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Warn("health check es request failed", log.E(err))
return &proto.ComponentStatus{Status: "err", Msg: err.Error()}
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return &proto.ComponentStatus{Status: "err", Msg: fmt.Sprintf("es status %d", resp.StatusCode)}
}
return okStatus()
}
func collectServerMetrics() *proto.HealthCheckData {
name, err := os.Hostname()
if err != nil || name == "" {
name = "app-server"
}
ip := netutil.GetLocalIP()
if ip == "" {
ip = "127.0.0.1"
}
cores := runtime.NumCPU()
usedCores := calcUsedCores(cores)
memMetrics := memoryMetrics()
diskMetrics := diskMetrics()
return &proto.HealthCheckData{
ServerName: name,
ServerIP: ip,
CPU: proto.HealthCPUMetrics{
Cores: cores,
Used: usedCores,
},
Memory: memMetrics,
Disk: diskMetrics,
}
}
func calcUsedCores(cores int) float64 {
usage, err := cpu.Percent(200*time.Millisecond, false)
if err != nil || len(usage) == 0 {
if err != nil {
log.Warn("health check cpu percent failed", log.E(err))
}
return 0
}
used := float64(cores) * usage[0] / 100
return math.Round(used*10) / 10
}
func memoryMetrics() proto.HealthMemoryMetrics {
vm, err := mem.VirtualMemory()
if err != nil {
log.Warn("health check memory stats failed", log.E(err))
return proto.HealthMemoryMetrics{}
}
return proto.HealthMemoryMetrics{
Total: bytesToGB(vm.Total),
Used: bytesToGB(vm.Used),
}
}
func diskMetrics() proto.HealthMemoryMetrics {
path := rootPath()
du, err := disk.Usage(path)
if err != nil {
log.Warn("health check disk stats failed", log.E(err))
return proto.HealthMemoryMetrics{}
}
return proto.HealthMemoryMetrics{
Total: bytesToGB(du.Total),
Used: bytesToGB(du.Used),
}
}
func bytesToGB(value uint64) int64 {
if value == 0 {
return 0
}
return int64(math.Round(float64(value) / bytesInGB))
}
func rootPath() string {
if runtime.GOOS == "windows" {
return "C:\\"
}
return "/"
}