mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2025-05-11 02:15:48 +02:00
452 lines
12 KiB
Go
452 lines
12 KiB
Go
// 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
|
||
}
|