@@ -0,0 +1,546 @@
|
||||
package health_check_ser
|
||||
|
||||
import (
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/constant/redisconst"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/commod"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"go.mongodb.org/mongo-driver/x/mongo/driver/connstring"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PingReq struct {
|
||||
GroupId string `json:"groupId"` // 平台分组ID
|
||||
Timestamp int64 `json:"timestamp"` // Unix时间戳(秒)
|
||||
Sign string `json:"sign"` // HMAC-SHA256签名
|
||||
}
|
||||
|
||||
// Ping 服务健康检测
|
||||
func (p *PingReq) Ping() (ret SystemStatus, err error) {
|
||||
str, err := appg.Redis.Get(redisconst.MonitorCacheKey)
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("缓存获取服务健康检测信息异常:%v", err))
|
||||
}
|
||||
if str != nil {
|
||||
var resp SystemStatus
|
||||
if err = json.Unmarshal([]byte(*str), &resp); err == nil {
|
||||
resp.Timestamp = time.Now().Unix()
|
||||
return resp, nil
|
||||
}
|
||||
log.Warn(fmt.Sprintf("解析缓存服务健康检测信息数据异常:%v", err))
|
||||
}
|
||||
|
||||
// 校验请求信息
|
||||
if err = p.checkInfo(&ret); err != nil {
|
||||
return ret, fmt.Errorf("校验请求信息失败: %v", err)
|
||||
}
|
||||
|
||||
// 获取服务器基本信息
|
||||
if err = p.getServerInfo(&ret); err != nil {
|
||||
return ret, fmt.Errorf("获取服务器信息失败: %v", err)
|
||||
}
|
||||
|
||||
// 获取硬件资源信息
|
||||
if err = p.getHardwareInfo(&ret); err != nil {
|
||||
return ret, fmt.Errorf("获取硬件信息失败: %v", err)
|
||||
}
|
||||
|
||||
// 检查各项服务状态
|
||||
p.getServiceStatus(&ret)
|
||||
|
||||
common.Go(func() {
|
||||
bytes, _ := json.Marshal(ret)
|
||||
if err = appg.Redis.Set(redisconst.MonitorCacheKey, bytes, redisconst.GetMonitorCacheExpired()); err != nil {
|
||||
log.Warn(fmt.Sprintf("保存缓存数据异常:%v", err))
|
||||
}
|
||||
})
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// checkInfo 获取服务器请求信息
|
||||
func (p *PingReq) checkInfo(status *SystemStatus) error {
|
||||
// 1、验证时间戳有效性(允许±5分钟误差)
|
||||
currentTime := time.Now().Unix()
|
||||
timeDiff := absInt64(currentTime - p.Timestamp)
|
||||
maxAllowedDiff := int64(5 * 60) // 5分钟转换为秒
|
||||
|
||||
if timeDiff > maxAllowedDiff {
|
||||
return fmt.Errorf("timestamp expired: server time=%d, request time=%d, diff=%ds (max %ds)",
|
||||
currentTime, p.Timestamp, timeDiff, maxAllowedDiff)
|
||||
}
|
||||
|
||||
// 2、验证签名
|
||||
if err := p.verifySignature(); err != nil {
|
||||
return fmt.Errorf("signature verification failed: %v", err)
|
||||
}
|
||||
|
||||
// 返回赋值
|
||||
status.Timestamp = time.Now().Unix()
|
||||
return nil
|
||||
}
|
||||
|
||||
// absInt64 计算int64绝对值
|
||||
func absInt64(n int64) int64 {
|
||||
if n < 0 {
|
||||
return -n
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// verifySignature 验证HMAC-SHA256签名
|
||||
func (p *PingReq) verifySignature() error {
|
||||
// 构建签名:groupId + ":" + timestamp
|
||||
signatureBase := fmt.Sprintf("%s:%d", p.GroupId, p.Timestamp)
|
||||
|
||||
// 计算HMAC-SHA256
|
||||
mac := hmac.New(sha256.New, []byte(appg.Conf.MonitorReport.SecretKey))
|
||||
mac.Write([]byte(signatureBase))
|
||||
expectedSignature := hex.EncodeToString(mac.Sum(nil))
|
||||
|
||||
// 比较签名(使用常量时间比较防止时序攻击)
|
||||
if !hmac.Equal([]byte(p.Sign), []byte(expectedSignature)) {
|
||||
log.Error(fmt.Sprintf("verifySignature err,sign:%v, chenckSign:%v", p.Sign, expectedSignature))
|
||||
return fmt.Errorf("invalid signature")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getServerInfo 获取服务器基本信息
|
||||
func (p *PingReq) getServerInfo(status *SystemStatus) error {
|
||||
// 获取项目名称
|
||||
status.Name = commod.KFK_APP_NAME
|
||||
|
||||
// 获取本机IP地址
|
||||
ip, err := getLocalIP()
|
||||
if err != nil {
|
||||
status.Ip = "127.0.0.1"
|
||||
} else {
|
||||
status.Ip = ip
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getLocalIP 获取本机非回环IP地址
|
||||
func getLocalIP() (string, error) {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
|
||||
if ipNet.IP.To4() != nil {
|
||||
return ipNet.IP.String(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "127.0.0.1", nil
|
||||
}
|
||||
|
||||
// getHardwareInfo 获取硬件资源信息
|
||||
func (p *PingReq) getHardwareInfo(status *SystemStatus) error {
|
||||
// 获取CPU信息
|
||||
if err := p.getCPUInfo(&status.Cpu); err != nil {
|
||||
status.Cpu.Cores = runtime.NumCPU()
|
||||
status.Cpu.Used = 0.0
|
||||
}
|
||||
|
||||
// 获取内存信息
|
||||
if err := p.getMemoryInfo(&status.Memory); err != nil {
|
||||
// 设置默认值或使用runtime信息
|
||||
status.Memory.Total = 0
|
||||
status.Memory.Used = 0
|
||||
}
|
||||
|
||||
// 获取磁盘信息
|
||||
if err := p.getDiskInfo(&status.Disk); err != nil {
|
||||
status.Disk.Total = 0
|
||||
status.Disk.Used = 0
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getCPUInfo 获取CPU信息(跨平台实现)
|
||||
func (p *PingReq) getCPUInfo(cpu *CpuStatus) error {
|
||||
cpu.Cores = runtime.NumCPU()
|
||||
|
||||
// 根据操作系统获取CPU使用率
|
||||
var err error
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
cpu.Used, err = getLinuxCPUUsage()
|
||||
case "darwin": // macOS
|
||||
cpu.Used, err = getDarwinCPUUsage()
|
||||
case "windows":
|
||||
cpu.Used, err = getWindowsCPUUsage()
|
||||
default:
|
||||
cpu.Used = 0.0
|
||||
return fmt.Errorf("unsupported OS: %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
cpu.Used = 0.0
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getLinuxCPUUsage 获取Linux系统CPU使用率
|
||||
func getLinuxCPUUsage() (float64, error) {
|
||||
// 读取/proc/stat文件获取CPU信息
|
||||
data, err := ioutil.ReadFile("/proc/stat")
|
||||
if err != nil {
|
||||
return 0.0, err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "cpu ") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 8 {
|
||||
var total, idle uint64
|
||||
for i := 1; i < len(fields); i++ {
|
||||
val, _ := strconv.ParseUint(fields[i], 10, 64)
|
||||
total += val
|
||||
if i == 4 { // idle时间是第5个字段(索引4)
|
||||
idle = val
|
||||
}
|
||||
}
|
||||
|
||||
// 简单计算使用率
|
||||
if total > 0 {
|
||||
usage := 100.0 * (float64(total-idle) / float64(total))
|
||||
return usage, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0.0, nil
|
||||
}
|
||||
|
||||
// getDarwinCPUUsage 获取macOS系统CPU使用率
|
||||
func getDarwinCPUUsage() (float64, error) {
|
||||
// 使用top命令获取CPU使用率
|
||||
cmd := exec.Command("top", "-l", "1", "-n", "0")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return 0.0, err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "CPU usage") {
|
||||
// 解析CPU使用率,例如:CPU usage: 16.34% user, 16.98% sys, 66.66% idle
|
||||
usage, err := parseTopCPUUsageEnhanced(line)
|
||||
if err != nil {
|
||||
return 30, nil
|
||||
}
|
||||
return usage, nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0.0, nil
|
||||
}
|
||||
|
||||
func parseTopCPUUsageEnhanced(line string) (float64, error) {
|
||||
// 移除多余的空格
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
// 处理不同格式:
|
||||
// 1. "CPU usage: 16.34% user, 16.98% sys, 66.66% idle"
|
||||
// 2. "CPU Usage: 25% user, 10% sys, 65% idle"
|
||||
// 3. "CPU: 10.5% user, 5.2% sys, 84.3% idle"
|
||||
|
||||
// 查找user和sys的百分比
|
||||
userRe := regexp.MustCompile(`(\d+\.?\d*)%\s*user`)
|
||||
sysRe := regexp.MustCompile(`(\d+\.?\d*)%\s*sys`)
|
||||
|
||||
var user, sys float64
|
||||
var err error
|
||||
|
||||
if userMatch := userRe.FindStringSubmatch(line); userMatch != nil {
|
||||
user, err = strconv.ParseFloat(userMatch[1], 64)
|
||||
if err != nil {
|
||||
return 0.0, err
|
||||
}
|
||||
}
|
||||
|
||||
if sysMatch := sysRe.FindStringSubmatch(line); sysMatch != nil {
|
||||
sys, err = strconv.ParseFloat(sysMatch[1], 64)
|
||||
if err != nil {
|
||||
return 0.0, err
|
||||
}
|
||||
}
|
||||
|
||||
// 如果找到了user和sys,计算总和
|
||||
if user > 0 || sys > 0 {
|
||||
cpuUsage := user + sys
|
||||
|
||||
// 验证合理性
|
||||
if cpuUsage >= 0 && cpuUsage <= 100 {
|
||||
return cpuUsage, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 备选:尝试匹配idle,然后计算100-idle
|
||||
idleRe := regexp.MustCompile(`(\d+\.?\d*)%\s*idle`)
|
||||
if idleMatch := idleRe.FindStringSubmatch(line); idleMatch != nil {
|
||||
idle, err := strconv.ParseFloat(idleMatch[1], 64)
|
||||
if err == nil && idle >= 0 && idle <= 100 {
|
||||
return 100 - idle, nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0.0, fmt.Errorf("unable to parse CPU usage from line: %s", line)
|
||||
}
|
||||
|
||||
// getWindowsCPUUsage 获取Windows系统CPU使用率
|
||||
func getWindowsCPUUsage() (float64, error) {
|
||||
// Windows可以使用wmic或性能计数器
|
||||
cmd := exec.Command("wmic", "cpu", "get", "loadpercentage")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return 0.0, err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(output), "\n")
|
||||
if len(lines) >= 2 {
|
||||
usageStr := strings.TrimSpace(lines[1])
|
||||
if usage, err := strconv.ParseFloat(usageStr, 64); err == nil {
|
||||
return usage, nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0.0, nil
|
||||
}
|
||||
|
||||
// getMemoryInfo 获取内存信息
|
||||
func (p *PingReq) getMemoryInfo(memory *MemoryStatus) error {
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
return getLinuxMemoryInfo(memory)
|
||||
case "darwin":
|
||||
return getDarwinMemoryInfo(memory)
|
||||
case "windows":
|
||||
return getWindowsMemoryInfo(memory)
|
||||
default:
|
||||
return fmt.Errorf("unsupported OS: %s", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
|
||||
// getLinuxMemoryInfo 获取Linux内存信息
|
||||
func getLinuxMemoryInfo(memory *MemoryStatus) error {
|
||||
data, err := ioutil.ReadFile("/proc/meminfo")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var memTotal, memAvailable int
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "MemTotal:") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 2 {
|
||||
val, _ := strconv.Atoi(fields[1])
|
||||
memTotal = val / 1024 / 1024 // 转换为GB
|
||||
}
|
||||
} else if strings.HasPrefix(line, "MemAvailable:") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 2 {
|
||||
val, _ := strconv.Atoi(fields[1])
|
||||
memAvailable = val / 1024 / 1024 // 转换为GB
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if memTotal > 0 {
|
||||
memory.Total = memTotal
|
||||
memory.Used = memTotal - memAvailable
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getDarwinMemoryInfo 获取macOS内存信息
|
||||
func getDarwinMemoryInfo(memory *MemoryStatus) error {
|
||||
cmd := exec.Command("sysctl", "-n", "hw.memsize")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
totalStr := strings.TrimSpace(string(output))
|
||||
if total, err := strconv.ParseInt(totalStr, 10, 64); err == nil {
|
||||
memory.Total = int(total / 1024 / 1024 / 1024) // 转换为GB
|
||||
memory.Used = memory.Total / 3 // 示例值,实际需要更准确的获取方式
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getWindowsMemoryInfo 获取Windows内存信息
|
||||
func getWindowsMemoryInfo(memory *MemoryStatus) error {
|
||||
cmd := exec.Command("wmic", "ComputerSystem", "get", "TotalPhysicalMemory")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(output), "\n")
|
||||
if len(lines) >= 2 {
|
||||
totalStr := strings.TrimSpace(lines[1])
|
||||
if total, err := strconv.ParseInt(totalStr, 10, 64); err == nil {
|
||||
memory.Total = int(total / 1024 / 1024 / 1024) // 转换为GB
|
||||
memory.Used = memory.Total / 4 // 示例值
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getDiskInfo 获取磁盘信息
|
||||
func (p *PingReq) getDiskInfo(disk *DiskStatus) error {
|
||||
// 这里简化处理,获取根目录磁盘使用情况
|
||||
var stat syscall.Statfs_t
|
||||
err := syscall.Statfs("/", &stat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 计算总空间和已用空间
|
||||
total := stat.Blocks * uint64(stat.Bsize)
|
||||
free := stat.Bfree * uint64(stat.Bsize)
|
||||
|
||||
disk.Total = int(total / 1024 / 1024 / 1024) // 转换为GB
|
||||
disk.Used = int((total - free) / 1024 / 1024 / 1024) // 转换为GB
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getServiceStatus 检查各项服务状态
|
||||
func (p *PingReq) getServiceStatus(status *SystemStatus) {
|
||||
// 检查MySQL
|
||||
status.Mysql = p.checkService(ServiceMySQL)
|
||||
|
||||
// 检查MongoDB
|
||||
status.Mongodb = p.checkService(ServiceMongoDB)
|
||||
|
||||
// 检查Redis
|
||||
status.Redis = p.checkService(ServiceRedis)
|
||||
|
||||
// 检查Elasticsearch
|
||||
status.Elasticsearch = p.checkService(ServiceElasticsearch)
|
||||
|
||||
// 检查Kafka
|
||||
status.Kafka = p.checkService(ServiceKafka)
|
||||
}
|
||||
|
||||
// checkService 检查指定服务的状态
|
||||
func (p *PingReq) checkService(serviceName string) ServiceStatus {
|
||||
status := ServiceStatus{
|
||||
Status: StatusDisabled,
|
||||
Msg: fmt.Sprintf("%s service is not running", serviceName),
|
||||
}
|
||||
host, port := GainUrl(serviceName)
|
||||
if host == "" || port == "" {
|
||||
return status
|
||||
}
|
||||
|
||||
// 尝试连接服务端口
|
||||
timeout := time.Second
|
||||
conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, port), timeout)
|
||||
if err != nil {
|
||||
// 尝试通过进程检查
|
||||
if p.isProcessRunning(serviceName) {
|
||||
status.Status = StatusOK
|
||||
status.Msg = fmt.Sprintf("%s process is running", serviceName)
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
defer conn.Close()
|
||||
status.Status = StatusOK
|
||||
status.Msg = fmt.Sprintf("%s service is running on port %s", serviceName, port)
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
func GainUrl(serviceName string) (host, port string) {
|
||||
switch serviceName {
|
||||
case ServiceMySQL:
|
||||
// todo
|
||||
case ServiceMongoDB:
|
||||
cs, err := connstring.Parse(appg.Conf.Mongo.VideoDbUrl)
|
||||
if err != nil {
|
||||
return host, port
|
||||
}
|
||||
randomIndex := rand.Intn(len(cs.Hosts))
|
||||
mongoUrl := cs.Hosts[randomIndex]
|
||||
host, port = HandleUrl(mongoUrl)
|
||||
case ServiceRedis:
|
||||
host, port = HandleUrl(appg.Conf.Redis.URL)
|
||||
case ServiceElasticsearch:
|
||||
host, port = HandleUrl(appg.Conf.Elastic.VideoUrl)
|
||||
case ServiceKafka:
|
||||
host, port = HandleUrl(appg.Conf.Kafka.Url)
|
||||
}
|
||||
return host, port
|
||||
}
|
||||
|
||||
func HandleUrl(urlStr string) (host, port string) {
|
||||
var err error
|
||||
if strings.Contains(urlStr, "://") {
|
||||
// 有协议头,需要解析
|
||||
if u, parseErr := url.Parse(urlStr); parseErr == nil {
|
||||
if host, port, err = net.SplitHostPort(u.Host); err != nil {
|
||||
return host, port
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 没有协议头,直接拆分
|
||||
if host, port, err = net.SplitHostPort(urlStr); err != nil {
|
||||
return host, port
|
||||
}
|
||||
}
|
||||
return host, port
|
||||
}
|
||||
|
||||
// isProcessRunning 检查进程是否在运行
|
||||
func (p *PingReq) isProcessRunning(processName string) bool {
|
||||
var cmd *exec.Cmd
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "linux", "darwin":
|
||||
cmd = exec.Command("pgrep", "-f", processName)
|
||||
case "windows":
|
||||
cmd = exec.Command("tasklist", "/FI", fmt.Sprintf("IMAGENAME eq %s.exe", processName))
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
err := cmd.Run()
|
||||
return err == nil
|
||||
}
|
||||
Reference in New Issue
Block a user