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 "/" }