status.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  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. logger.Debug("Master进程内存使用(MB):", memoryUsage)
  276. }
  277. break
  278. }
  279. }
  280. // 遍历所有进程,区分工作进程和其他Nginx进程
  281. for _, p := range processes {
  282. if p.Pid == masterPID {
  283. continue // 已经计算过主进程
  284. }
  285. name, err := p.Name()
  286. if err != nil {
  287. continue
  288. }
  289. // 只处理Nginx相关进程
  290. if !strings.Contains(strings.ToLower(name), "nginx") {
  291. continue
  292. }
  293. // 添加到Nginx进程列表
  294. nginxProcesses = append(nginxProcesses, p)
  295. // 获取父进程PID
  296. ppid, err := p.Ppid()
  297. if err != nil {
  298. continue
  299. }
  300. cmdline, err := p.Cmdline()
  301. if err != nil {
  302. continue
  303. }
  304. // 获取内存使用情况 - 使用RSS代替
  305. // 注意:理想情况下我们应该使用USS(仅包含进程独占内存),但gopsutil不直接支持
  306. mem, err := p.MemoryInfo()
  307. if err == nil && mem != nil {
  308. // 转换为 MB
  309. memoryUsage := float64(mem.RSS) / 1024 / 1024
  310. totalMemory += memoryUsage
  311. }
  312. // 区分工作进程、缓存进程和其他进程
  313. if ppid == masterPID || strings.Contains(cmdline, "worker process") {
  314. workerCount++
  315. } else if strings.Contains(cmdline, "cache") {
  316. cacheCount++
  317. } else {
  318. otherCount++
  319. }
  320. }
  321. // 重新计算CPU使用率,更接近top命令的计算方式
  322. // 首先进行初始CPU时间测量
  323. times1 := make(map[int32]float64)
  324. for _, p := range nginxProcesses {
  325. times, err := p.Times()
  326. if err == nil {
  327. // CPU时间 = 用户时间 + 系统时间
  328. times1[p.Pid] = times.User + times.System
  329. }
  330. }
  331. // 等待一小段时间
  332. time.Sleep(100 * time.Millisecond)
  333. // 再次测量CPU时间
  334. totalCPUPercent := 0.0
  335. for _, p := range nginxProcesses {
  336. times, err := p.Times()
  337. if err != nil {
  338. continue
  339. }
  340. // 计算CPU时间差
  341. currentTotal := times.User + times.System
  342. if previousTotal, ok := times1[p.Pid]; ok {
  343. // 计算这段时间内的CPU使用百分比(考虑多核)
  344. cpuDelta := currentTotal - previousTotal
  345. // 计算每秒CPU使用率(考虑采样时间)
  346. cpuPercent := (cpuDelta / 0.1) * 100.0 / float64(numCPU)
  347. totalCPUPercent += cpuPercent
  348. }
  349. }
  350. // 四舍五入到整数,更符合top显示方式
  351. totalCPUPercent = math.Round(totalCPUPercent)
  352. // 四舍五入内存使用量到两位小数
  353. totalMemory = math.Round(totalMemory*100) / 100
  354. result["workers"] = workerCount
  355. result["master"] = masterCount
  356. result["cache"] = cacheCount
  357. result["other"] = otherCount
  358. result["cpu_usage"] = totalCPUPercent
  359. result["memory_usage"] = totalMemory
  360. return result, nil
  361. }
  362. // 获取 Nginx 配置信息
  363. func getNginxConfigInfo() (map[string]int, error) {
  364. result := map[string]int{
  365. "worker_processes": 1,
  366. "worker_connections": 1024,
  367. }
  368. // 获取 worker_processes 配置
  369. cmd := exec.Command("nginx", "-T")
  370. output, err := cmd.CombinedOutput()
  371. if err != nil {
  372. return result, fmt.Errorf("failed to get nginx config: %v", err)
  373. }
  374. // 解析 worker_processes
  375. wpRe := regexp.MustCompile(`worker_processes\s+(\d+|auto);`)
  376. if matches := wpRe.FindStringSubmatch(string(output)); len(matches) > 1 {
  377. if matches[1] == "auto" {
  378. result["worker_processes"] = runtime.NumCPU()
  379. } else {
  380. result["worker_processes"], _ = strconv.Atoi(matches[1])
  381. }
  382. }
  383. // 解析 worker_connections
  384. wcRe := regexp.MustCompile(`worker_connections\s+(\d+);`)
  385. if matches := wcRe.FindStringSubmatch(string(output)); len(matches) > 1 {
  386. result["worker_connections"], _ = strconv.Atoi(matches[1])
  387. }
  388. return result, nil
  389. }