nginx-ui/api/nginx/status.go
2025-04-10 03:36:32 +00:00

452 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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