status.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. // GetDetailedStatus API 实现
  2. // 该功能用于解决 Issue #850,提供类似宝塔面板的 Nginx 负载监控功能
  3. // 返回详细的 Nginx 状态信息,包括请求统计、连接数、工作进程等数据
  4. package nginx
  5. import (
  6. "fmt"
  7. "io"
  8. "math"
  9. "net/http"
  10. "os"
  11. "os/exec"
  12. "regexp"
  13. "runtime"
  14. "strconv"
  15. "strings"
  16. "time"
  17. "github.com/0xJacky/Nginx-UI/internal/nginx"
  18. "github.com/gin-gonic/gin"
  19. "github.com/shirou/gopsutil/v4/process"
  20. "github.com/uozi-tech/cosy/logger"
  21. )
  22. // NginxPerformanceInfo 存储 Nginx 性能相关信息
  23. type NginxPerformanceInfo struct {
  24. // 基本状态信息
  25. Active int `json:"active"` // 活动连接数
  26. Accepts int `json:"accepts"` // 总握手次数
  27. Handled int `json:"handled"` // 总连接次数
  28. Requests int `json:"requests"` // 总请求数
  29. Reading int `json:"reading"` // 读取客户端请求数
  30. Writing int `json:"writing"` // 响应数
  31. Waiting int `json:"waiting"` // 驻留进程(等待请求)
  32. // 进程相关信息
  33. Workers int `json:"workers"` // 工作进程数
  34. Master int `json:"master"` // 主进程数
  35. Cache int `json:"cache"` // 缓存管理进程数
  36. Other int `json:"other"` // 其他Nginx相关进程数
  37. CPUUsage float64 `json:"cpu_usage"` // CPU 使用率
  38. MemoryUsage float64 `json:"memory_usage"` // 内存使用率(MB)
  39. // 配置信息
  40. WorkerProcesses int `json:"worker_processes"` // worker_processes 配置
  41. WorkerConnections int `json:"worker_connections"` // worker_connections 配置
  42. }
  43. // GetDetailedStatus 获取 Nginx 详细状态信息
  44. func GetDetailedStatus(c *gin.Context) {
  45. // 检查 Nginx 是否运行
  46. pidPath := nginx.GetPIDPath()
  47. running := true
  48. if fileInfo, err := os.Stat(pidPath); err != nil || fileInfo.Size() == 0 {
  49. running = false
  50. c.JSON(http.StatusOK, gin.H{
  51. "running": false,
  52. "message": "Nginx is not running",
  53. })
  54. return
  55. }
  56. // 获取 stub_status 模块数据
  57. stubStatusInfo, err := getStubStatusInfo()
  58. if err != nil {
  59. logger.Warn("Failed to get stub_status info:", err)
  60. }
  61. // 获取进程信息
  62. processInfo, err := getNginxProcessInfo()
  63. if err != nil {
  64. logger.Warn("Failed to get process info:", err)
  65. }
  66. // 获取配置信息
  67. configInfo, err := getNginxConfigInfo()
  68. if err != nil {
  69. logger.Warn("Failed to get config info:", err)
  70. }
  71. // 组合所有信息
  72. info := NginxPerformanceInfo{
  73. Active: stubStatusInfo["active"],
  74. Accepts: stubStatusInfo["accepts"],
  75. Handled: stubStatusInfo["handled"],
  76. Requests: stubStatusInfo["requests"],
  77. Reading: stubStatusInfo["reading"],
  78. Writing: stubStatusInfo["writing"],
  79. Waiting: stubStatusInfo["waiting"],
  80. Workers: processInfo["workers"].(int),
  81. Master: processInfo["master"].(int),
  82. Cache: processInfo["cache"].(int),
  83. Other: processInfo["other"].(int),
  84. CPUUsage: processInfo["cpu_usage"].(float64),
  85. MemoryUsage: processInfo["memory_usage"].(float64),
  86. WorkerProcesses: configInfo["worker_processes"],
  87. WorkerConnections: configInfo["worker_connections"],
  88. }
  89. c.JSON(http.StatusOK, gin.H{
  90. "running": running,
  91. "info": info,
  92. })
  93. }
  94. // StreamDetailedStatus 使用 SSE 流式推送 Nginx 详细状态信息
  95. func StreamDetailedStatus(c *gin.Context) {
  96. // 设置 SSE 的响应头
  97. c.Header("Content-Type", "text/event-stream")
  98. c.Header("Cache-Control", "no-cache")
  99. c.Header("Connection", "keep-alive")
  100. c.Header("Access-Control-Allow-Origin", "*")
  101. // 创建上下文,当客户端断开连接时取消
  102. ctx := c.Request.Context()
  103. // 为防止 goroutine 泄漏,创建一个计时器通道
  104. ticker := time.NewTicker(5 * time.Second)
  105. defer ticker.Stop()
  106. // 立即发送一次初始数据
  107. sendPerformanceData(c)
  108. // 使用 goroutine 定期发送数据
  109. for {
  110. select {
  111. case <-ticker.C:
  112. // 发送性能数据
  113. if err := sendPerformanceData(c); err != nil {
  114. logger.Warn("Error sending SSE data:", err)
  115. return
  116. }
  117. case <-ctx.Done():
  118. // 客户端断开连接或请求被取消
  119. logger.Debug("Client closed connection")
  120. return
  121. }
  122. }
  123. }
  124. // sendPerformanceData 发送一次性能数据
  125. func sendPerformanceData(c *gin.Context) error {
  126. // 检查 Nginx 是否运行
  127. pidPath := nginx.GetPIDPath()
  128. running := true
  129. if fileInfo, err := os.Stat(pidPath); err != nil || fileInfo.Size() == 0 {
  130. running = false
  131. // 发送 Nginx 未运行的状态
  132. c.SSEvent("message", gin.H{
  133. "running": false,
  134. "message": "Nginx is not running",
  135. })
  136. // 刷新缓冲区,确保数据立即发送
  137. c.Writer.Flush()
  138. return nil
  139. }
  140. // 获取性能数据
  141. stubStatusInfo, err := getStubStatusInfo()
  142. if err != nil {
  143. logger.Warn("Failed to get stub_status info:", err)
  144. }
  145. processInfo, err := getNginxProcessInfo()
  146. if err != nil {
  147. logger.Warn("Failed to get process info:", err)
  148. }
  149. configInfo, err := getNginxConfigInfo()
  150. if err != nil {
  151. logger.Warn("Failed to get config info:", err)
  152. }
  153. // 组合所有信息
  154. info := NginxPerformanceInfo{
  155. Active: stubStatusInfo["active"],
  156. Accepts: stubStatusInfo["accepts"],
  157. Handled: stubStatusInfo["handled"],
  158. Requests: stubStatusInfo["requests"],
  159. Reading: stubStatusInfo["reading"],
  160. Writing: stubStatusInfo["writing"],
  161. Waiting: stubStatusInfo["waiting"],
  162. Workers: processInfo["workers"].(int),
  163. Master: processInfo["master"].(int),
  164. Cache: processInfo["cache"].(int),
  165. Other: processInfo["other"].(int),
  166. CPUUsage: processInfo["cpu_usage"].(float64),
  167. MemoryUsage: processInfo["memory_usage"].(float64),
  168. WorkerProcesses: configInfo["worker_processes"],
  169. WorkerConnections: configInfo["worker_connections"],
  170. }
  171. // 发送 SSE 事件
  172. c.SSEvent("message", gin.H{
  173. "running": running,
  174. "info": info,
  175. })
  176. // 刷新缓冲区,确保数据立即发送
  177. c.Writer.Flush()
  178. return nil
  179. }
  180. // 获取 stub_status 模块数据
  181. func getStubStatusInfo() (map[string]int, error) {
  182. result := map[string]int{
  183. "active": 0, "accepts": 0, "handled": 0, "requests": 0,
  184. "reading": 0, "writing": 0, "waiting": 0,
  185. }
  186. // 默认尝试访问 stub_status 页面
  187. statusURL := "http://localhost/stub_status"
  188. // 创建 HTTP 客户端
  189. client := &http.Client{
  190. Timeout: 5 * time.Second,
  191. }
  192. // 发送请求获取 stub_status 数据
  193. resp, err := client.Get(statusURL)
  194. if err != nil {
  195. return result, fmt.Errorf("failed to get stub status: %v", err)
  196. }
  197. defer resp.Body.Close()
  198. // 读取响应内容
  199. body, err := io.ReadAll(resp.Body)
  200. if err != nil {
  201. return result, fmt.Errorf("failed to read response body: %v", err)
  202. }
  203. // 解析响应内容
  204. statusContent := string(body)
  205. // 匹配活动连接数
  206. activeRe := regexp.MustCompile(`Active connections:\s+(\d+)`)
  207. if matches := activeRe.FindStringSubmatch(statusContent); len(matches) > 1 {
  208. result["active"], _ = strconv.Atoi(matches[1])
  209. }
  210. // 匹配请求统计信息
  211. serverRe := regexp.MustCompile(`(\d+)\s+(\d+)\s+(\d+)`)
  212. if matches := serverRe.FindStringSubmatch(statusContent); len(matches) > 3 {
  213. result["accepts"], _ = strconv.Atoi(matches[1])
  214. result["handled"], _ = strconv.Atoi(matches[2])
  215. result["requests"], _ = strconv.Atoi(matches[3])
  216. }
  217. // 匹配读写等待数
  218. connRe := regexp.MustCompile(`Reading:\s+(\d+)\s+Writing:\s+(\d+)\s+Waiting:\s+(\d+)`)
  219. if matches := connRe.FindStringSubmatch(statusContent); len(matches) > 3 {
  220. result["reading"], _ = strconv.Atoi(matches[1])
  221. result["writing"], _ = strconv.Atoi(matches[2])
  222. result["waiting"], _ = strconv.Atoi(matches[3])
  223. }
  224. return result, nil
  225. }
  226. // 获取 Nginx 进程信息
  227. func getNginxProcessInfo() (map[string]interface{}, error) {
  228. result := map[string]interface{}{
  229. "workers": 0,
  230. "master": 0,
  231. "cache": 0,
  232. "other": 0,
  233. "cpu_usage": 0.0,
  234. "memory_usage": 0.0,
  235. }
  236. // 查找所有 Nginx 进程
  237. processes, err := process.Processes()
  238. if err != nil {
  239. return result, fmt.Errorf("failed to get processes: %v", err)
  240. }
  241. totalMemory := 0.0
  242. workerCount := 0
  243. masterCount := 0
  244. cacheCount := 0
  245. otherCount := 0
  246. nginxProcesses := []*process.Process{}
  247. // 获取系统CPU核心数
  248. numCPU := runtime.NumCPU()
  249. // 获取Nginx主进程的PID
  250. var masterPID int32 = -1
  251. for _, p := range processes {
  252. name, err := p.Name()
  253. if err != nil {
  254. continue
  255. }
  256. cmdline, err := p.Cmdline()
  257. if err != nil {
  258. continue
  259. }
  260. // 检查是否是Nginx主进程
  261. if strings.Contains(strings.ToLower(name), "nginx") &&
  262. (strings.Contains(cmdline, "master process") ||
  263. !strings.Contains(cmdline, "worker process")) &&
  264. p.Pid > 0 {
  265. masterPID = p.Pid
  266. masterCount++
  267. nginxProcesses = append(nginxProcesses, p)
  268. // 获取内存使用情况 - 使用RSS代替
  269. // 注意:理想情况下我们应该使用USS(仅包含进程独占内存),但gopsutil不直接支持
  270. mem, err := p.MemoryInfo()
  271. if err == nil && mem != nil {
  272. // 转换为 MB
  273. memoryUsage := float64(mem.RSS) / 1024 / 1024
  274. totalMemory += memoryUsage
  275. }
  276. break
  277. }
  278. }
  279. // 遍历所有进程,区分工作进程和其他Nginx进程
  280. for _, p := range processes {
  281. if p.Pid == masterPID {
  282. continue // 已经计算过主进程
  283. }
  284. name, err := p.Name()
  285. if err != nil {
  286. continue
  287. }
  288. // 只处理Nginx相关进程
  289. if !strings.Contains(strings.ToLower(name), "nginx") {
  290. continue
  291. }
  292. // 添加到Nginx进程列表
  293. nginxProcesses = append(nginxProcesses, p)
  294. // 获取父进程PID
  295. ppid, err := p.Ppid()
  296. if err != nil {
  297. continue
  298. }
  299. cmdline, err := p.Cmdline()
  300. if err != nil {
  301. continue
  302. }
  303. // 获取内存使用情况 - 使用RSS代替
  304. // 注意:理想情况下我们应该使用USS(仅包含进程独占内存),但gopsutil不直接支持
  305. mem, err := p.MemoryInfo()
  306. if err == nil && mem != nil {
  307. // 转换为 MB
  308. memoryUsage := float64(mem.RSS) / 1024 / 1024
  309. totalMemory += memoryUsage
  310. }
  311. // 区分工作进程、缓存进程和其他进程
  312. if ppid == masterPID || strings.Contains(cmdline, "worker process") {
  313. workerCount++
  314. } else if strings.Contains(cmdline, "cache") {
  315. cacheCount++
  316. } else {
  317. otherCount++
  318. }
  319. }
  320. // 重新计算CPU使用率,更接近top命令的计算方式
  321. // 首先进行初始CPU时间测量
  322. times1 := make(map[int32]float64)
  323. for _, p := range nginxProcesses {
  324. times, err := p.Times()
  325. if err == nil {
  326. // CPU时间 = 用户时间 + 系统时间
  327. times1[p.Pid] = times.User + times.System
  328. }
  329. }
  330. // 等待一小段时间
  331. time.Sleep(100 * time.Millisecond)
  332. // 再次测量CPU时间
  333. totalCPUPercent := 0.0
  334. for _, p := range nginxProcesses {
  335. times, err := p.Times()
  336. if err != nil {
  337. continue
  338. }
  339. // 计算CPU时间差
  340. currentTotal := times.User + times.System
  341. if previousTotal, ok := times1[p.Pid]; ok {
  342. // 计算这段时间内的CPU使用百分比(考虑多核)
  343. cpuDelta := currentTotal - previousTotal
  344. // 计算每秒CPU使用率(考虑采样时间)
  345. cpuPercent := (cpuDelta / 0.1) * 100.0 / float64(numCPU)
  346. totalCPUPercent += cpuPercent
  347. }
  348. }
  349. // 四舍五入到整数,更符合top显示方式
  350. totalCPUPercent = math.Round(totalCPUPercent)
  351. // 四舍五入内存使用量到两位小数
  352. totalMemory = math.Round(totalMemory*100) / 100
  353. result["workers"] = workerCount
  354. result["master"] = masterCount
  355. result["cache"] = cacheCount
  356. result["other"] = otherCount
  357. result["cpu_usage"] = totalCPUPercent
  358. result["memory_usage"] = totalMemory
  359. return result, nil
  360. }
  361. // 获取 Nginx 配置信息
  362. func getNginxConfigInfo() (map[string]int, error) {
  363. result := map[string]int{
  364. "worker_processes": 1,
  365. "worker_connections": 1024,
  366. }
  367. // 获取 worker_processes 配置
  368. cmd := exec.Command("nginx", "-T")
  369. output, err := cmd.CombinedOutput()
  370. if err != nil {
  371. return result, fmt.Errorf("failed to get nginx config: %v", err)
  372. }
  373. // 解析 worker_processes
  374. wpRe := regexp.MustCompile(`worker_processes\s+(\d+|auto);`)
  375. if matches := wpRe.FindStringSubmatch(string(output)); len(matches) > 1 {
  376. if matches[1] == "auto" {
  377. result["worker_processes"] = runtime.NumCPU()
  378. } else {
  379. result["worker_processes"], _ = strconv.Atoi(matches[1])
  380. }
  381. }
  382. // 解析 worker_connections
  383. wcRe := regexp.MustCompile(`worker_connections\s+(\d+);`)
  384. if matches := wcRe.FindStringSubmatch(string(output)); len(matches) > 1 {
  385. result["worker_connections"], _ = strconv.Atoi(matches[1])
  386. }
  387. return result, nil
  388. }