123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452 |
- // GetDetailedStatus API 实现
- // 该功能用于解决 Issue #850,提供类似宝塔面板的 Nginx 负载监控功能
- // 返回详细的 Nginx 状态信息,包括请求统计、连接数、工作进程等数据
- package nginx
- import (
- "fmt"
- "io"
- "math"
- "net/http"
- "os"
- "os/exec"
- "regexp"
- "runtime"
- "strconv"
- "strings"
- "time"
- "github.com/0xJacky/Nginx-UI/internal/nginx"
- "github.com/gin-gonic/gin"
- "github.com/shirou/gopsutil/v4/process"
- "github.com/uozi-tech/cosy/logger"
- )
- // NginxPerformanceInfo 存储 Nginx 性能相关信息
- type NginxPerformanceInfo struct {
- // 基本状态信息
- Active int `json:"active"` // 活动连接数
- Accepts int `json:"accepts"` // 总握手次数
- Handled int `json:"handled"` // 总连接次数
- Requests int `json:"requests"` // 总请求数
- Reading int `json:"reading"` // 读取客户端请求数
- Writing int `json:"writing"` // 响应数
- Waiting int `json:"waiting"` // 驻留进程(等待请求)
- // 进程相关信息
- Workers int `json:"workers"` // 工作进程数
- Master int `json:"master"` // 主进程数
- Cache int `json:"cache"` // 缓存管理进程数
- Other int `json:"other"` // 其他Nginx相关进程数
- CPUUsage float64 `json:"cpu_usage"` // CPU 使用率
- MemoryUsage float64 `json:"memory_usage"` // 内存使用率(MB)
- // 配置信息
- WorkerProcesses int `json:"worker_processes"` // worker_processes 配置
- WorkerConnections int `json:"worker_connections"` // worker_connections 配置
- }
- // GetDetailedStatus 获取 Nginx 详细状态信息
- func GetDetailedStatus(c *gin.Context) {
- // 检查 Nginx 是否运行
- pidPath := nginx.GetPIDPath()
- running := true
- if fileInfo, err := os.Stat(pidPath); err != nil || fileInfo.Size() == 0 {
- running = false
- c.JSON(http.StatusOK, gin.H{
- "running": false,
- "message": "Nginx is not running",
- })
- return
- }
- // 获取 stub_status 模块数据
- stubStatusInfo, err := getStubStatusInfo()
- if err != nil {
- logger.Warn("Failed to get stub_status info:", err)
- }
- // 获取进程信息
- processInfo, err := getNginxProcessInfo()
- if err != nil {
- logger.Warn("Failed to get process info:", err)
- }
- // 获取配置信息
- configInfo, err := getNginxConfigInfo()
- if err != nil {
- logger.Warn("Failed to get config info:", err)
- }
- // 组合所有信息
- info := NginxPerformanceInfo{
- Active: stubStatusInfo["active"],
- Accepts: stubStatusInfo["accepts"],
- Handled: stubStatusInfo["handled"],
- Requests: stubStatusInfo["requests"],
- Reading: stubStatusInfo["reading"],
- Writing: stubStatusInfo["writing"],
- Waiting: stubStatusInfo["waiting"],
- Workers: processInfo["workers"].(int),
- Master: processInfo["master"].(int),
- Cache: processInfo["cache"].(int),
- Other: processInfo["other"].(int),
- CPUUsage: processInfo["cpu_usage"].(float64),
- MemoryUsage: processInfo["memory_usage"].(float64),
- WorkerProcesses: configInfo["worker_processes"],
- WorkerConnections: configInfo["worker_connections"],
- }
- c.JSON(http.StatusOK, gin.H{
- "running": running,
- "info": info,
- })
- }
- // StreamDetailedStatus 使用 SSE 流式推送 Nginx 详细状态信息
- func StreamDetailedStatus(c *gin.Context) {
- // 设置 SSE 的响应头
- c.Header("Content-Type", "text/event-stream")
- c.Header("Cache-Control", "no-cache")
- c.Header("Connection", "keep-alive")
- c.Header("Access-Control-Allow-Origin", "*")
- // 创建上下文,当客户端断开连接时取消
- ctx := c.Request.Context()
- // 为防止 goroutine 泄漏,创建一个计时器通道
- ticker := time.NewTicker(5 * time.Second)
- defer ticker.Stop()
- // 立即发送一次初始数据
- sendPerformanceData(c)
- // 使用 goroutine 定期发送数据
- for {
- select {
- case <-ticker.C:
- // 发送性能数据
- if err := sendPerformanceData(c); err != nil {
- logger.Warn("Error sending SSE data:", err)
- return
- }
- case <-ctx.Done():
- // 客户端断开连接或请求被取消
- logger.Debug("Client closed connection")
- return
- }
- }
- }
- // sendPerformanceData 发送一次性能数据
- func sendPerformanceData(c *gin.Context) error {
- // 检查 Nginx 是否运行
- pidPath := nginx.GetPIDPath()
- running := true
- if fileInfo, err := os.Stat(pidPath); err != nil || fileInfo.Size() == 0 {
- running = false
- // 发送 Nginx 未运行的状态
- c.SSEvent("message", gin.H{
- "running": false,
- "message": "Nginx is not running",
- })
- // 刷新缓冲区,确保数据立即发送
- c.Writer.Flush()
- return nil
- }
- // 获取性能数据
- stubStatusInfo, err := getStubStatusInfo()
- if err != nil {
- logger.Warn("Failed to get stub_status info:", err)
- }
- processInfo, err := getNginxProcessInfo()
- if err != nil {
- logger.Warn("Failed to get process info:", err)
- }
- configInfo, err := getNginxConfigInfo()
- if err != nil {
- logger.Warn("Failed to get config info:", err)
- }
- // 组合所有信息
- info := NginxPerformanceInfo{
- Active: stubStatusInfo["active"],
- Accepts: stubStatusInfo["accepts"],
- Handled: stubStatusInfo["handled"],
- Requests: stubStatusInfo["requests"],
- Reading: stubStatusInfo["reading"],
- Writing: stubStatusInfo["writing"],
- Waiting: stubStatusInfo["waiting"],
- Workers: processInfo["workers"].(int),
- Master: processInfo["master"].(int),
- Cache: processInfo["cache"].(int),
- Other: processInfo["other"].(int),
- CPUUsage: processInfo["cpu_usage"].(float64),
- MemoryUsage: processInfo["memory_usage"].(float64),
- WorkerProcesses: configInfo["worker_processes"],
- WorkerConnections: configInfo["worker_connections"],
- }
- // 发送 SSE 事件
- c.SSEvent("message", gin.H{
- "running": running,
- "info": info,
- })
- // 刷新缓冲区,确保数据立即发送
- c.Writer.Flush()
- return nil
- }
- // 获取 stub_status 模块数据
- func getStubStatusInfo() (map[string]int, error) {
- result := map[string]int{
- "active": 0, "accepts": 0, "handled": 0, "requests": 0,
- "reading": 0, "writing": 0, "waiting": 0,
- }
- // 默认尝试访问 stub_status 页面
- statusURL := "http://localhost/stub_status"
- // 创建 HTTP 客户端
- client := &http.Client{
- Timeout: 5 * time.Second,
- }
- // 发送请求获取 stub_status 数据
- resp, err := client.Get(statusURL)
- if err != nil {
- return result, fmt.Errorf("failed to get stub status: %v", err)
- }
- defer resp.Body.Close()
- // 读取响应内容
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- return result, fmt.Errorf("failed to read response body: %v", err)
- }
- // 解析响应内容
- statusContent := string(body)
- // 匹配活动连接数
- activeRe := regexp.MustCompile(`Active connections:\s+(\d+)`)
- if matches := activeRe.FindStringSubmatch(statusContent); len(matches) > 1 {
- result["active"], _ = strconv.Atoi(matches[1])
- }
- // 匹配请求统计信息
- serverRe := regexp.MustCompile(`(\d+)\s+(\d+)\s+(\d+)`)
- if matches := serverRe.FindStringSubmatch(statusContent); len(matches) > 3 {
- result["accepts"], _ = strconv.Atoi(matches[1])
- result["handled"], _ = strconv.Atoi(matches[2])
- result["requests"], _ = strconv.Atoi(matches[3])
- }
- // 匹配读写等待数
- connRe := regexp.MustCompile(`Reading:\s+(\d+)\s+Writing:\s+(\d+)\s+Waiting:\s+(\d+)`)
- if matches := connRe.FindStringSubmatch(statusContent); len(matches) > 3 {
- result["reading"], _ = strconv.Atoi(matches[1])
- result["writing"], _ = strconv.Atoi(matches[2])
- result["waiting"], _ = strconv.Atoi(matches[3])
- }
- return result, nil
- }
- // 获取 Nginx 进程信息
- func getNginxProcessInfo() (map[string]interface{}, error) {
- result := map[string]interface{}{
- "workers": 0,
- "master": 0,
- "cache": 0,
- "other": 0,
- "cpu_usage": 0.0,
- "memory_usage": 0.0,
- }
- // 查找所有 Nginx 进程
- processes, err := process.Processes()
- if err != nil {
- return result, fmt.Errorf("failed to get processes: %v", err)
- }
- totalMemory := 0.0
- workerCount := 0
- masterCount := 0
- cacheCount := 0
- otherCount := 0
- nginxProcesses := []*process.Process{}
- // 获取系统CPU核心数
- numCPU := runtime.NumCPU()
- // 获取Nginx主进程的PID
- var masterPID int32 = -1
- for _, p := range processes {
- name, err := p.Name()
- if err != nil {
- continue
- }
- cmdline, err := p.Cmdline()
- if err != nil {
- continue
- }
- // 检查是否是Nginx主进程
- if strings.Contains(strings.ToLower(name), "nginx") &&
- (strings.Contains(cmdline, "master process") ||
- !strings.Contains(cmdline, "worker process")) &&
- p.Pid > 0 {
- masterPID = p.Pid
- masterCount++
- nginxProcesses = append(nginxProcesses, p)
- // 获取内存使用情况 - 使用RSS代替
- // 注意:理想情况下我们应该使用USS(仅包含进程独占内存),但gopsutil不直接支持
- mem, err := p.MemoryInfo()
- if err == nil && mem != nil {
- // 转换为 MB
- memoryUsage := float64(mem.RSS) / 1024 / 1024
- totalMemory += memoryUsage
- logger.Debug("Master进程内存使用(MB):", memoryUsage)
- }
- break
- }
- }
- // 遍历所有进程,区分工作进程和其他Nginx进程
- for _, p := range processes {
- if p.Pid == masterPID {
- continue // 已经计算过主进程
- }
- name, err := p.Name()
- if err != nil {
- continue
- }
- // 只处理Nginx相关进程
- if !strings.Contains(strings.ToLower(name), "nginx") {
- continue
- }
- // 添加到Nginx进程列表
- nginxProcesses = append(nginxProcesses, p)
- // 获取父进程PID
- ppid, err := p.Ppid()
- if err != nil {
- continue
- }
- cmdline, err := p.Cmdline()
- if err != nil {
- continue
- }
- // 获取内存使用情况 - 使用RSS代替
- // 注意:理想情况下我们应该使用USS(仅包含进程独占内存),但gopsutil不直接支持
- mem, err := p.MemoryInfo()
- if err == nil && mem != nil {
- // 转换为 MB
- memoryUsage := float64(mem.RSS) / 1024 / 1024
- totalMemory += memoryUsage
- }
- // 区分工作进程、缓存进程和其他进程
- if ppid == masterPID || strings.Contains(cmdline, "worker process") {
- workerCount++
- } else if strings.Contains(cmdline, "cache") {
- cacheCount++
- } else {
- otherCount++
- }
- }
- // 重新计算CPU使用率,更接近top命令的计算方式
- // 首先进行初始CPU时间测量
- times1 := make(map[int32]float64)
- for _, p := range nginxProcesses {
- times, err := p.Times()
- if err == nil {
- // CPU时间 = 用户时间 + 系统时间
- times1[p.Pid] = times.User + times.System
- }
- }
- // 等待一小段时间
- time.Sleep(100 * time.Millisecond)
- // 再次测量CPU时间
- totalCPUPercent := 0.0
- for _, p := range nginxProcesses {
- times, err := p.Times()
- if err != nil {
- continue
- }
- // 计算CPU时间差
- currentTotal := times.User + times.System
- if previousTotal, ok := times1[p.Pid]; ok {
- // 计算这段时间内的CPU使用百分比(考虑多核)
- cpuDelta := currentTotal - previousTotal
- // 计算每秒CPU使用率(考虑采样时间)
- cpuPercent := (cpuDelta / 0.1) * 100.0 / float64(numCPU)
- totalCPUPercent += cpuPercent
- }
- }
- // 四舍五入到整数,更符合top显示方式
- totalCPUPercent = math.Round(totalCPUPercent)
- // 四舍五入内存使用量到两位小数
- totalMemory = math.Round(totalMemory*100) / 100
- result["workers"] = workerCount
- result["master"] = masterCount
- result["cache"] = cacheCount
- result["other"] = otherCount
- result["cpu_usage"] = totalCPUPercent
- result["memory_usage"] = totalMemory
- return result, nil
- }
- // 获取 Nginx 配置信息
- func getNginxConfigInfo() (map[string]int, error) {
- result := map[string]int{
- "worker_processes": 1,
- "worker_connections": 1024,
- }
- // 获取 worker_processes 配置
- cmd := exec.Command("nginx", "-T")
- output, err := cmd.CombinedOutput()
- if err != nil {
- return result, fmt.Errorf("failed to get nginx config: %v", err)
- }
- // 解析 worker_processes
- wpRe := regexp.MustCompile(`worker_processes\s+(\d+|auto);`)
- if matches := wpRe.FindStringSubmatch(string(output)); len(matches) > 1 {
- if matches[1] == "auto" {
- result["worker_processes"] = runtime.NumCPU()
- } else {
- result["worker_processes"], _ = strconv.Atoi(matches[1])
- }
- }
- // 解析 worker_connections
- wcRe := regexp.MustCompile(`worker_connections\s+(\d+);`)
- if matches := wcRe.FindStringSubmatch(string(output)); len(matches) > 1 {
- result["worker_connections"], _ = strconv.Atoi(matches[1])
- }
- return result, nil
- }
|