Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
218 lines
5.8 KiB
Go
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 "/"
|
|
}
|