feat: added status check and control functions for the Nginx stub_status module and optimized the performance data acquisition logic

This commit is contained in:
Akino 2025-04-10 10:11:26 +00:00
parent 32d7c74835
commit 2d0961f1a3
No known key found for this signature in database
GPG key ID: FB2F74D193A40907
16 changed files with 720 additions and 543 deletions

View file

@ -4,107 +4,37 @@
package nginx
import (
"fmt"
"io"
"math"
"errors"
"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"
"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"` // 驻留进程(等待请求)
nginx.StubStatusData
// 进程相关信息
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
nginx.NginxProcessInfo
// 配置信息
WorkerProcesses int `json:"worker_processes"` // worker_processes 配置
WorkerConnections int `json:"worker_connections"` // worker_connections 配置
nginx.NginxConfigInfo
}
// 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,
})
// GetDetailStatus 获取 Nginx 详细状态信息
func GetDetailStatus(c *gin.Context) {
response := nginx.GetPerformanceData()
c.JSON(http.StatusOK, response)
}
// StreamDetailedStatus 使用 SSE 流式推送 Nginx 详细状态信息
func StreamDetailedStatus(c *gin.Context) {
// StreamDetailStatus 使用 SSE 流式推送 Nginx 详细状态信息
func StreamDetailStatus(c *gin.Context) {
// 设置 SSE 的响应头
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
@ -140,312 +70,63 @@ func StreamDetailedStatus(c *gin.Context) {
// 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"],
}
response := nginx.GetPerformanceData()
// 发送 SSE 事件
c.SSEvent("message", gin.H{
"running": running,
"info": info,
})
c.SSEvent("message", response)
// 刷新缓冲区,确保数据立即发送
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,
}
// CheckStubStatus 获取 Nginx stub_status 模块状态
func CheckStubStatus(c *gin.Context) {
stubStatus := nginx.GetStubStatus()
// 默认尝试访问 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
c.JSON(http.StatusOK, stubStatus)
}
// 获取 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,
// ToggleStubStatus 启用或禁用 stub_status 模块
func ToggleStubStatus(c *gin.Context) {
var json struct {
Enable bool `json:"enable"`
}
if !cosy.BindAndValid(c, &json) {
return
}
stubStatus := nginx.GetStubStatus()
// 如果当前状态与期望状态相同,则无需操作
if stubStatus.Enabled == json.Enable {
c.JSON(http.StatusOK, stubStatus)
return
}
var err error
if json.Enable {
err = nginx.EnableStubStatus()
} else {
err = nginx.DisableStubStatus()
}
// 查找所有 Nginx 进程
processes, err := process.Processes()
if err != nil {
return result, fmt.Errorf("failed to get processes: %v", err)
cosy.ErrHandler(c, err)
return
}
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
}
break
}
// 重新加载 Nginx 配置
reloadOutput := nginx.Reload()
if len(reloadOutput) > 0 && (strings.Contains(strings.ToLower(reloadOutput), "error") ||
strings.Contains(strings.ToLower(reloadOutput), "failed")) {
cosy.ErrHandler(c, errors.New("Reload Nginx failed"))
return
}
// 遍历所有进程区分工作进程和其他Nginx进程
for _, p := range processes {
if p.Pid == masterPID {
continue // 已经计算过主进程
}
// 检查操作后的状态
newStubStatus := nginx.GetStubStatus()
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
c.JSON(http.StatusOK, newStubStatus)
}