Browse Source

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

Akino 2 months ago
parent
commit
2d0961f1a3

+ 3 - 7
api/nginx/control.go

@@ -1,10 +1,10 @@
 package nginx
 
 import (
+	"net/http"
+
 	"github.com/0xJacky/Nginx-UI/internal/nginx"
 	"github.com/gin-gonic/gin"
-	"net/http"
-	"os"
 )
 
 func Reload(c *gin.Context) {
@@ -31,13 +31,9 @@ func Restart(c *gin.Context) {
 }
 
 func Status(c *gin.Context) {
-	pidPath := nginx.GetPIDPath()
 	lastOutput := nginx.GetLastOutput()
 
-	running := true
-	if fileInfo, err := os.Stat(pidPath); err != nil || fileInfo.Size() == 0 { // fileInfo.Size() == 0 no process id
-		running = false
-	}
+	running := nginx.IsNginxRunning()
 
 	c.JSON(http.StatusOK, gin.H{
 		"running": running,

+ 6 - 2
api/nginx/router.go

@@ -14,9 +14,13 @@ func InitRouter(r *gin.RouterGroup) {
 	r.POST("nginx/test", Test)
 	r.GET("nginx/status", Status)
 	// 获取 Nginx 详细状态信息,包括连接数、进程信息等(Issue #850)
-	r.GET("nginx/detailed_status", GetDetailedStatus)
+	r.GET("nginx/detail_status", GetDetailStatus)
 	// 使用SSE推送Nginx详细状态信息
-	r.GET("nginx/detailed_status/stream", StreamDetailedStatus)
+	r.GET("nginx/detail_status/stream", StreamDetailStatus)
+	// 获取 stub_status 模块状态
+	r.GET("nginx/stub_status", CheckStubStatus)
+	// 启用或禁用 stub_status 模块
+	r.POST("nginx/stub_status", ToggleStubStatus)
 	r.POST("nginx_log", nginx_log.GetNginxLogPage)
 	r.GET("nginx/directives", GetDirectives)
 }

+ 44 - 363
api/nginx/status.go

@@ -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,
-	}
-
-	// 默认尝试访问 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)
+// CheckStubStatus 获取 Nginx stub_status 模块状态
+func CheckStubStatus(c *gin.Context) {
+	stubStatus := nginx.GetStubStatus()
 
-	// 匹配活动连接数
-	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"`
 	}
 
-	// 查找所有 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
-			}
-
-			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
-		}
+	if !cosy.BindAndValid(c, &json) {
+		return
 	}
 
-	// 等待一小段时间
-	time.Sleep(100 * time.Millisecond)
-
-	// 再次测量CPU时间
-	totalCPUPercent := 0.0
-	for _, p := range nginxProcesses {
-		times, err := p.Times()
-		if err != nil {
-			continue
-		}
+	stubStatus := nginx.GetStubStatus()
 
-		// 计算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
-		}
+	// 如果当前状态与期望状态相同,则无需操作
+	if stubStatus.Enabled == json.Enable {
+		c.JSON(http.StatusOK, stubStatus)
+		return
 	}
 
-	// 四舍五入到整数,更符合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,
+	var err error
+	if json.Enable {
+		err = nginx.EnableStubStatus()
+	} else {
+		err = nginx.DisableStubStatus()
 	}
 
-	// 获取 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)
+		cosy.ErrHandler(c, err)
+		return
 	}
 
-	// 解析 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])
-		}
+	// 重新加载 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
 	}
 
-	// 解析 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])
-	}
+	// 检查操作后的状态
+	newStubStatus := nginx.GetStubStatus()
 
-	return result, nil
+	c.JSON(http.StatusOK, newStubStatus)
 }

+ 2 - 9
app/src/api/ngx.ts

@@ -70,15 +70,8 @@ const ngx = {
     return http.get('/nginx/status')
   },
 
-  detailed_status(): Promise<{ running: boolean, info: NginxPerformanceInfo }> {
-    return http.get('/nginx/detailed_status')
-  },
-
-  // 创建SSE连接获取实时Nginx性能数据
-  create_detailed_status_stream(): EventSource {
-    const baseUrl = import.meta.env.VITE_API_URL || ''
-    const url = `${baseUrl}/api/nginx/detailed_status/stream`
-    return new EventSource(url)
+  detail_status(): Promise<{ running: boolean, stub_status_enabled: boolean, info: NginxPerformanceInfo }> {
+    return http.get('/nginx/detail_status')
   },
 
   reload() {

+ 57 - 22
app/src/composables/useNginxPerformance.ts

@@ -2,42 +2,75 @@ import type { NginxPerformanceInfo } from '@/api/ngx'
 import ngx from '@/api/ngx'
 import { computed, ref } from 'vue'
 
-export function useNginxPerformance() {
-  const loading = ref(true)
-  const nginxInfo = ref<NginxPerformanceInfo>()
-  const error = ref<string>('')
-  const lastUpdateTime = ref(new Date())
+// Time formatting helper function
+function formatTimeAgo(date: Date): string {
+  const now = new Date()
+  const diffMs = now.getTime() - date.getTime()
+  const diffSec = Math.round(diffMs / 1000)
 
-  // Update refresh time
-  function updateLastUpdateTime() {
-    lastUpdateTime.value = new Date()
+  if (diffSec < 60) {
+    return `${diffSec} ${$gettext('秒前')}`
+  }
+
+  const diffMin = Math.floor(diffSec / 60)
+  if (diffMin < 60) {
+    return `${diffMin} ${$gettext('分钟前')}`
   }
 
+  return date.toLocaleTimeString()
+}
+
+export function useNginxPerformance() {
+  const loading = ref(false)
+  const error = ref('')
+  const nginxInfo = ref<NginxPerformanceInfo | null>(null)
+  const lastUpdateTime = ref<Date | null>(null)
+
+  // stub_status availability
+  const stubStatusEnabled = ref(false)
+  const stubStatusLoading = ref(false)
+  const stubStatusError = ref('')
+
   // Format the last update time
   const formattedUpdateTime = computed(() => {
-    return lastUpdateTime.value.toLocaleTimeString()
+    if (!lastUpdateTime.value)
+      return $gettext('Unknown')
+    return formatTimeAgo(lastUpdateTime.value)
   })
 
-  // Get Nginx status data
-  async function fetchInitialData() {
-    loading.value = true
-    error.value = ''
+  // Update the last update time
+  function updateLastUpdateTime() {
+    lastUpdateTime.value = new Date()
+  }
 
+  // Check stub_status availability and get initial data
+  async function fetchInitialData() {
     try {
-      const result = await ngx.detailed_status()
-      nginxInfo.value = result.info
-      updateLastUpdateTime()
-    }
-    catch (e) {
-      if (e instanceof Error) {
-        error.value = e.message
+      loading.value = true
+      stubStatusLoading.value = true
+      error.value = ''
+
+      // Get performance data
+      const response = await ngx.detail_status()
+
+      if (response.running) {
+        stubStatusEnabled.value = response.stub_status_enabled
+        nginxInfo.value = response.info
+        updateLastUpdateTime()
       }
       else {
-        error.value = $gettext('Get data failed')
+        error.value = $gettext('Nginx is not running')
+        nginxInfo.value = null
       }
     }
+    catch (err) {
+      console.error('Failed to get Nginx performance data:', err)
+      error.value = $gettext('Failed to get performance data')
+      nginxInfo.value = null
+    }
     finally {
       loading.value = false
+      stubStatusLoading.value = false
     }
   }
 
@@ -45,9 +78,11 @@ export function useNginxPerformance() {
     loading,
     nginxInfo,
     error,
-    lastUpdateTime,
     formattedUpdateTime,
     updateLastUpdateTime,
     fetchInitialData,
+    stubStatusEnabled,
+    stubStatusLoading,
+    stubStatusError,
   }
 }

+ 2 - 1
app/src/lib/http/interceptors.ts

@@ -109,8 +109,9 @@ export function setupResponseInterceptor() {
   instance.interceptors.response.use(
     response => {
       nprogress.done()
+
       // Check if full response is requested in config
-      if (response.config?.returnFullResponse) {
+      if (response?.config?.returnFullResponse) {
         return Promise.resolve(response)
       }
       return Promise.resolve(response.data)

+ 81 - 14
app/src/views/dashboard/NginxDashBoard.vue

@@ -5,6 +5,7 @@ import { NginxStatus } from '@/constants'
 import { useUserStore } from '@/pinia'
 import { useGlobalStore } from '@/pinia/moudule/global'
 import { ClockCircleOutlined, ReloadOutlined } from '@ant-design/icons-vue'
+import axios from 'axios'
 import { storeToRefs } from 'pinia'
 import ConnectionMetricsCard from './components/ConnectionMetricsCard.vue'
 import PerformanceStatisticsCard from './components/PerformanceStatisticsCard.vue'
@@ -25,18 +26,50 @@ const {
   formattedUpdateTime,
   updateLastUpdateTime,
   fetchInitialData,
+  stubStatusEnabled,
+  stubStatusLoading,
+  stubStatusError,
 } = useNginxPerformance()
 
 // SSE connection
 const { connect, disconnect } = useSSE()
 
+// Toggle stub_status module status
+async function toggleStubStatus() {
+  try {
+    stubStatusLoading.value = true
+    stubStatusError.value = ''
+    const response = await axios.post('/api/nginx/stub_status', {
+      enable: !stubStatusEnabled.value,
+    })
+
+    if (response.data.stub_status_enabled !== undefined) {
+      stubStatusEnabled.value = response.data.stub_status_enabled
+    }
+
+    if (response.data.error) {
+      stubStatusError.value = response.data.error
+    }
+    else {
+      fetchInitialData().then(connectSSE)
+    }
+  }
+  catch (err) {
+    console.error('Toggle stub_status failed:', err)
+    stubStatusError.value = $gettext('Toggle failed')
+  }
+  finally {
+    stubStatusLoading.value = false
+  }
+}
+
 // Connect SSE
 function connectSSE() {
   disconnect()
   loading.value = true
 
   connect({
-    url: 'api/nginx/detailed_status/stream',
+    url: 'api/nginx/detail_status/stream',
     token: token.value,
     onMessage: data => {
       loading.value = false
@@ -48,6 +81,7 @@ function connectSSE() {
       else {
         error.value = data.message || $gettext('Nginx is not running')
       }
+      stubStatusEnabled.value = data.stub_status_enabled
     },
     onError: () => {
       error.value = $gettext('Connection error, trying to reconnect...')
@@ -55,7 +89,7 @@ function connectSSE() {
       // If the connection fails, try to get data using the traditional method
       setTimeout(() => {
         fetchInitialData()
-      }, 5000)
+      }, 2000)
     },
   })
 }
@@ -110,6 +144,38 @@ onMounted(() => {
       :description="error"
     />
 
+    <!-- stub_status 开关 -->
+    <ACard class="mb-4" :bordered="false">
+      <div class="flex items-center justify-between">
+        <div>
+          <div class="font-medium mb-1">
+            {{ $gettext('Enable stub_status module') }}
+          </div>
+          <div class="text-gray-500 text-sm">
+            {{ $gettext('This module provides Nginx request statistics, connection count, etc. data. After enabling it, you can view performance statistics') }}
+          </div>
+          <div v-if="stubStatusError" class="text-red-500 text-sm mt-1">
+            {{ stubStatusError }}
+          </div>
+        </div>
+        <ASwitch
+          :checked="stubStatusEnabled"
+          :loading="stubStatusLoading"
+          @change="toggleStubStatus"
+        />
+      </div>
+    </ACard>
+
+    <!-- stub_status module is not enabled -->
+    <AAlert
+      v-if="status === NginxStatus.Running && !stubStatusEnabled && !error"
+      class="mb-4"
+      type="info"
+      show-icon
+      :message="$gettext('Need to enable the stub_status module')"
+      :description="$gettext('Please enable the stub_status module to get request statistics, connection count, etc.')"
+    />
+
     <!-- Loading state -->
     <ASpin :spinning="loading" :tip="$gettext('Loading data...')">
       <div v-if="!nginxInfo && !error" class="text-center py-8">
@@ -118,25 +184,26 @@ onMounted(() => {
 
       <div v-if="nginxInfo" class="performance-dashboard">
         <!-- Top performance metrics card -->
+        <ACard class="mb-4" :title="$gettext('Performance Metrics')" :bordered="false">
+          <PerformanceStatisticsCard :nginx-info="nginxInfo" />
+        </ACard>
+
         <ARow :gutter="[16, 16]" class="mb-4">
-          <ACol :span="24">
-            <ACard :title="$gettext('Performance Metrics')" :bordered="false">
-              <PerformanceStatisticsCard :nginx-info="nginxInfo" />
-            </ACard>
+          <!-- Metrics card -->
+          <ACol :sm="24" :lg="12">
+            <ConnectionMetricsCard :nginx-info="nginxInfo" />
           </ACol>
-        </ARow>
-
-        <!-- Metrics card -->
-        <ConnectionMetricsCard :nginx-info="nginxInfo" class="mb-4" />
 
-        <!-- Resource monitoring -->
-        <ARow :gutter="[16, 16]" class="mb-4">
           <!-- CPU and memory usage -->
-          <ACol :xs="24" :md="12">
+          <ACol :sm="24" :lg="12">
             <ResourceUsageCard :nginx-info="nginxInfo" />
           </ACol>
+        </ARow>
+
+        <!-- Resource monitoring -->
+        <ARow :gutter="[16, 16]" class="mb-4">
           <!-- Process distribution -->
-          <ACol :xs="24" :md="12">
+          <ACol :span="24">
             <ProcessDistributionCard :nginx-info="nginxInfo" />
           </ACol>
         </ARow>

+ 3 - 78
app/src/views/dashboard/components/ConnectionMetricsCard.vue

@@ -1,6 +1,5 @@
 <script setup lang="ts">
 import type { NginxPerformanceInfo } from '@/api/ngx'
-import { InfoCircleOutlined } from '@ant-design/icons-vue'
 import { computed, defineProps } from 'vue'
 
 const props = defineProps<{
@@ -17,31 +16,12 @@ const activeConnectionsPercent = computed(() => {
 const workerProcessesPercent = computed(() => {
   return Number(((props.nginxInfo.workers / props.nginxInfo.worker_processes) * 100).toFixed(2))
 })
-
-// Requests per connection
-const requestsPerConnection = computed(() => {
-  if (props.nginxInfo.handled === 0) {
-    return '0'
-  }
-  return (props.nginxInfo.requests / props.nginxInfo.handled).toFixed(2)
-})
-
-// Format numbers
-function formatNumber(num: number): string {
-  if (num >= 1000000) {
-    return `${(num / 1000000).toFixed(2)}M`
-  }
-  else if (num >= 1000) {
-    return `${(num / 1000).toFixed(2)}K`
-  }
-  return num.toString()
-}
 </script>
 
 <template>
-  <ARow :gutter="[16, 16]">
+  <ARow :gutter="[16, 16]" class="h-full">
     <!-- Current active connections -->
-    <ACol :xs="24" :sm="12" :md="12" :lg="6">
+    <ACol :xs="24" :sm="12">
       <ACard class="h-full" :bordered="false" :body-style="{ padding: '20px', height: '100%' }">
         <div class="flex flex-col h-full">
           <div class="mb-2 text-gray-500 font-medium truncate">
@@ -62,7 +42,7 @@ function formatNumber(num: number): string {
     </ACol>
 
     <!-- Worker processes -->
-    <ACol :xs="24" :sm="12" :md="12" :lg="6">
+    <ACol :xs="24" :sm="12">
       <ACard class="h-full" :bordered="false" :body-style="{ padding: '20px', height: '100%' }">
         <div class="flex flex-col h-full">
           <div class="mb-2 text-gray-500 font-medium truncate">
@@ -77,61 +57,6 @@ function formatNumber(num: number): string {
             size="small"
             status="active"
           />
-          <div class="mt-2 text-xs text-gray-500 overflow-hidden text-ellipsis">
-            {{ $gettext('Total Nginx processes') }}: {{ nginxInfo.workers + nginxInfo.master + nginxInfo.cache + nginxInfo.other }}
-            <Tooltip :title="$gettext('Includes master process, worker processes, cache processes, and other Nginx processes')">
-              <InfoCircleOutlined class="ml-1" />
-            </Tooltip>
-          </div>
-        </div>
-      </ACard>
-    </ACol>
-
-    <!-- Requests per connection -->
-    <ACol :xs="24" :sm="12" :md="12" :lg="6">
-      <ACard class="h-full" :bordered="false" :body-style="{ padding: '20px', height: '100%' }">
-        <div class="flex flex-col h-full justify-between">
-          <div>
-            <div class="mb-2 text-gray-500 font-medium truncate">
-              {{ $gettext('Requests per connection') }}
-            </div>
-            <div class="flex items-baseline mb-2">
-              <span class="text-2xl font-bold">{{ requestsPerConnection }}</span>
-              <Tooltip :title="$gettext('The average number of requests per connection, the higher the value, the higher the connection reuse efficiency')">
-                <InfoCircleOutlined class="ml-2 text-gray-500" />
-              </Tooltip>
-            </div>
-          </div>
-          <div>
-            <div class="text-xs text-gray-500 mb-1 truncate">
-              {{ $gettext('Total requests') }}: {{ formatNumber(nginxInfo.requests) }}
-            </div>
-            <div class="text-xs text-gray-500 truncate">
-              {{ $gettext('Total connections') }}: {{ formatNumber(nginxInfo.handled) }}
-            </div>
-          </div>
-        </div>
-      </ACard>
-    </ACol>
-
-    <!-- Resource utilization -->
-    <ACol :xs="24" :sm="12" :md="12" :lg="6">
-      <ACard class="h-full" :bordered="false" :body-style="{ padding: '20px', height: '100%' }">
-        <div class="flex flex-col h-full justify-between">
-          <div class="mb-2 text-gray-500 font-medium truncate">
-            {{ $gettext('Resource Utilization') }}
-          </div>
-          <div class="flex items-center justify-center flex-grow">
-            <AProgress
-              type="dashboard"
-              :percent="Math.round((Math.min(nginxInfo.cpu_usage / 100, 1) * 0.5 + Math.min(nginxInfo.active / (nginxInfo.worker_connections * nginxInfo.worker_processes), 1) * 0.5) * 100)"
-              :width="80"
-              status="active"
-            />
-          </div>
-          <div class="mt-2 text-xs text-gray-500 text-center overflow-hidden text-ellipsis">
-            {{ $gettext('Based on CPU usage and connection usage') }}
-          </div>
         </div>
       </ACard>
     </ACol>

+ 9 - 4
app/src/views/dashboard/components/PerformanceStatisticsCard.vue

@@ -40,9 +40,9 @@ const maxRPS = computed(() => {
         </template>
         <template #title>
           {{ $gettext('Max Requests Per Second') }}
-          <Tooltip :title="$gettext('Calculated based on worker_processes * worker_connections. Actual performance depends on hardware, configuration, and workload')">
+          <ATooltip :title="$gettext('Calculated based on worker_processes * worker_connections. Actual performance depends on hardware, configuration, and workload')">
             <InfoCircleOutlined class="ml-1 text-gray-500" />
-          </Tooltip>
+          </ATooltip>
         </template>
       </AStatistic>
       <div class="text-xs text-gray-500 mt-1">
@@ -69,11 +69,16 @@ const maxRPS = computed(() => {
     <!-- Requests per connection -->
     <ACol :xs="24" :sm="12" :md="8" :lg="6">
       <AStatistic
-        :title="$gettext('Requests Per Connection')"
         :value="requestsPerConnection"
         :precision="2"
-        :value-style="{ color: '#f5222d', fontSize: '24px' }"
+        :value-style="{ color: '#3a7f99', fontSize: '24px' }"
       >
+        <template #title>
+          {{ $gettext('Requests Per Connection') }}
+          <ATooltip :title="$gettext('Total Requests / Total Connections')">
+            <InfoCircleOutlined class="ml-1 text-gray-500" />
+          </ATooltip>
+        </template>
         <template #prefix>
           <DashboardOutlined />
         </template>

+ 8 - 1
app/src/views/dashboard/components/ProcessDistributionCard.vue

@@ -1,6 +1,6 @@
 <script setup lang="ts">
 import type { NginxPerformanceInfo } from '@/api/ngx'
-import { computed, defineProps } from 'vue'
+import { InfoCircleOutlined } from '@ant-design/icons-vue'
 
 const props = defineProps<{
   nginxInfo: NginxPerformanceInfo
@@ -48,6 +48,13 @@ const totalProcesses = computed(() => {
         {{ $gettext('Actual worker to configured ratio') }}:
         <span class="font-medium">{{ nginxInfo.workers }} / {{ nginxInfo.worker_processes }}</span>
       </div>
+
+      <div class="mt-2 text-xs text-gray-500 overflow-hidden text-ellipsis">
+        {{ $gettext('Total Nginx processes') }}: {{ nginxInfo.workers + nginxInfo.master + nginxInfo.cache + nginxInfo.other }}
+        <ATooltip :title="$gettext('Includes master process, worker processes, cache processes, and other Nginx processes')">
+          <InfoCircleOutlined class="ml-1" />
+        </ATooltip>
+      </div>
     </div>
   </ACard>
 </template>

+ 7 - 33
app/src/views/dashboard/components/ResourceUsageCard.vue

@@ -5,37 +5,30 @@ import {
   InfoCircleOutlined,
   ThunderboltOutlined,
 } from '@ant-design/icons-vue'
-import { computed, defineProps } from 'vue'
 
 const props = defineProps<{
   nginxInfo: NginxPerformanceInfo
 }>()
 
-// Resource utilization
-const resourceUtilization = computed(() => {
-  const cpuFactor = Math.min(props.nginxInfo.cpu_usage / 100, 1)
-  const maxConnections = props.nginxInfo.worker_connections * props.nginxInfo.worker_processes
-  const connectionFactor = Math.min(props.nginxInfo.active / maxConnections, 1)
-
-  return Math.round((cpuFactor * 0.5 + connectionFactor * 0.5) * 100)
+const cpuUsage = computed(() => {
+  return Number(Math.min(props.nginxInfo.cpu_usage, 100).toFixed(2))
 })
 </script>
 
 <template>
-  <ACard :title="$gettext('Resource Usage of Nginx')" :bordered="false" class="h-full" :body-style="{ padding: '16px', height: 'calc(100% - 58px)' }">
+  <ACard :bordered="false" class="h-full" :body-style="{ padding: '20px', height: 'calc(100% - 58px)' }">
     <div class="flex flex-col h-full">
       <!-- CPU usage -->
-      <ARow :gutter="[16, 8]" class="mb-2">
+      <ARow :gutter="[16, 8]">
         <ACol :span="24">
           <div class="flex items-center">
             <ThunderboltOutlined class="text-lg mr-2" :style="{ color: nginxInfo.cpu_usage > 80 ? '#cf1322' : '#3f8600' }" />
             <div class="text-base font-medium">
-              {{ $gettext('CPU Usage') }}: <span :style="{ color: nginxInfo.cpu_usage > 80 ? '#cf1322' : '#3f8600' }">{{ nginxInfo.cpu_usage.toFixed(2) }}%</span>
+              {{ $gettext('CPU Usage') }}: <span :style="{ color: nginxInfo.cpu_usage > 80 ? '#cf1322' : '#3f8600' }">{{ cpuUsage.toFixed(2) }}%</span>
             </div>
           </div>
           <AProgress
-            :percent="Math.min(nginxInfo.cpu_usage, 100)"
-            :format="percent => `${percent?.toFixed(2)}%`"
+            :percent="cpuUsage"
             :status="nginxInfo.cpu_usage > 80 ? 'exception' : 'active'"
             size="small"
             class="mt-1"
@@ -48,7 +41,7 @@ const resourceUtilization = computed(() => {
       </ARow>
 
       <!-- Memory usage -->
-      <ARow :gutter="[16, 8]" class="mb-2">
+      <ARow :gutter="[16, 8]" class="mt-2">
         <ACol :span="24">
           <div class="flex items-center">
             <div class="text-blue-500 text-lg mr-2 flex items-center">
@@ -63,25 +56,6 @@ const resourceUtilization = computed(() => {
           </div>
         </ACol>
       </ARow>
-
-      <div class="mt-1 flex justify-between text-xs text-gray-500">
-        {{ $gettext('Per worker memory') }}: {{ (nginxInfo.memory_usage / (nginxInfo.workers || 1)).toFixed(2) }} MB
-      </div>
-
-      <!-- System load -->
-      <div class="mt-4 text-xs text-gray-500 border-t border-gray-100 pt-2">
-        <div class="flex justify-between mb-1">
-          <span>{{ $gettext('System load') }}</span>
-          <span class="font-medium">{{ resourceUtilization }}%</span>
-        </div>
-        <AProgress
-          :percent="resourceUtilization"
-          size="small"
-          :status="resourceUtilization > 80 ? 'exception' : 'active'"
-          :stroke-color="resourceUtilization > 80 ? '#ff4d4f' : resourceUtilization > 50 ? '#faad14' : '#52c41a'"
-          :show-info="false"
-        />
-      </div>
     </div>
   </ACard>
 </template>

+ 48 - 0
internal/nginx/config_info.go

@@ -0,0 +1,48 @@
+package nginx
+
+import (
+	"os/exec"
+	"regexp"
+	"runtime"
+	"strconv"
+
+	"github.com/pkg/errors"
+)
+
+type NginxConfigInfo struct {
+	WorkerProcesses   int `json:"worker_processes"`
+	WorkerConnections int `json:"worker_connections"`
+}
+
+// GetNginxWorkerConfigInfo Get Nginx config info of worker_processes and worker_connections
+func GetNginxWorkerConfigInfo() (*NginxConfigInfo, error) {
+	result := &NginxConfigInfo{
+		WorkerProcesses:   1,
+		WorkerConnections: 1024,
+	}
+
+	// Get worker_processes config
+	cmd := exec.Command("nginx", "-T")
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		return result, errors.Wrap(err, "failed to get nginx config")
+	}
+
+	// Parse worker_processes
+	wpRe := regexp.MustCompile(`worker_processes\s+(\d+|auto);`)
+	if matches := wpRe.FindStringSubmatch(string(output)); len(matches) > 1 {
+		if matches[1] == "auto" {
+			result.WorkerProcesses = runtime.NumCPU()
+		} else {
+			result.WorkerProcesses, _ = strconv.Atoi(matches[1])
+		}
+	}
+
+	// Parse worker_connections
+	wcRe := regexp.MustCompile(`worker_connections\s+(\d+);`)
+	if matches := wcRe.FindStringSubmatch(string(output)); len(matches) > 1 {
+		result.WorkerConnections, _ = strconv.Atoi(matches[1])
+	}
+
+	return result, nil
+}

+ 9 - 0
internal/nginx/nginx.go

@@ -1,6 +1,7 @@
 package nginx
 
 import (
+	"os"
 	"os/exec"
 	"strings"
 	"sync"
@@ -115,3 +116,11 @@ func execCommand(name string, cmd ...string) (out string) {
 	}
 	return
 }
+
+func IsNginxRunning() bool {
+	pidPath := GetPIDPath()
+	if fileInfo, err := os.Stat(pidPath); err != nil || fileInfo.Size() == 0 {
+		return false
+	}
+	return true
+}

+ 55 - 0
internal/nginx/performance.go

@@ -0,0 +1,55 @@
+package nginx
+
+import "github.com/uozi-tech/cosy/logger"
+
+type NginxPerformanceInfo struct {
+	StubStatusData
+	NginxProcessInfo
+	NginxConfigInfo
+}
+
+type NginxPerformanceResponse struct {
+	StubStatusEnabled bool                 `json:"stub_status_enabled"`
+	Running           bool                 `json:"running"`
+	Info              NginxPerformanceInfo `json:"info"`
+}
+
+func GetPerformanceData() NginxPerformanceResponse {
+	// Check if Nginx is running
+	running := IsNginxRunning()
+	if !running {
+		return NginxPerformanceResponse{
+			StubStatusEnabled: false,
+			Running:           false,
+			Info:              NginxPerformanceInfo{},
+		}
+	}
+
+	// Get Nginx status information
+	stubStatusEnabled, statusInfo, err := GetStubStatusData()
+	if err != nil {
+		logger.Warn("Failed to get Nginx status:", err)
+	}
+
+	// Get Nginx process information
+	processInfo, err := GetNginxProcessInfo()
+	if err != nil {
+		logger.Warn("Failed to get Nginx process info:", err)
+	}
+
+	// Get Nginx config information
+	configInfo, err := GetNginxWorkerConfigInfo()
+	if err != nil {
+		logger.Warn("Failed to get Nginx config info:", err)
+	}
+
+	return NginxPerformanceResponse{
+		StubStatusEnabled: stubStatusEnabled,
+		Running:           running,
+		Info: NginxPerformanceInfo{
+			StubStatusData:   *statusInfo,
+			NginxProcessInfo: *processInfo,
+			NginxConfigInfo:  *configInfo,
+		},
+	}
+}

+ 178 - 0
internal/nginx/process_info.go

@@ -0,0 +1,178 @@
+package nginx
+
+import (
+	"fmt"
+	"math"
+	"runtime"
+	"strings"
+	"time"
+
+	"github.com/shirou/gopsutil/v4/process"
+)
+
+type NginxProcessInfo struct {
+	Workers     int     `json:"workers"`
+	Master      int     `json:"master"`
+	Cache       int     `json:"cache"`
+	Other       int     `json:"other"`
+	CPUUsage    float64 `json:"cpu_usage"`
+	MemoryUsage float64 `json:"memory_usage"`
+}
+
+// GetNginxProcessInfo Get Nginx process information
+func GetNginxProcessInfo() (*NginxProcessInfo, error) {
+	result := &NginxProcessInfo{
+		Workers:     0,
+		Master:      0,
+		Cache:       0,
+		Other:       0,
+		CPUUsage:    0.0,
+		MemoryUsage: 0.0,
+	}
+
+	// Find all Nginx processes
+	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{}
+
+	// Get the number of system CPU cores
+	numCPU := runtime.NumCPU()
+
+	// Get the PID of the Nginx master process
+	var masterPID int32 = -1
+	for _, p := range processes {
+		name, err := p.Name()
+		if err != nil {
+			continue
+		}
+
+		cmdline, err := p.Cmdline()
+		if err != nil {
+			continue
+		}
+
+		// Check if it is the Nginx master process
+		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)
+
+			// Get the memory usage
+			mem, err := p.MemoryInfo()
+			if err == nil && mem != nil {
+				// Convert to MB
+				memoryUsage := float64(mem.RSS) / 1024 / 1024
+				totalMemory += memoryUsage
+			}
+
+			break
+		}
+	}
+
+	// Iterate through all processes, distinguishing between worker processes and other Nginx processes
+	for _, p := range processes {
+		if p.Pid == masterPID {
+			continue // Already calculated the master process
+		}
+
+		name, err := p.Name()
+		if err != nil {
+			continue
+		}
+
+		// Only process Nginx related processes
+		if !strings.Contains(strings.ToLower(name), "nginx") {
+			continue
+		}
+
+		// Add to the Nginx process list
+		nginxProcesses = append(nginxProcesses, p)
+
+		// Get the parent process PID
+		ppid, err := p.Ppid()
+		if err != nil {
+			continue
+		}
+
+		cmdline, err := p.Cmdline()
+		if err != nil {
+			continue
+		}
+
+		// Get the memory usage
+		mem, err := p.MemoryInfo()
+		if err == nil && mem != nil {
+			// Convert to MB
+			memoryUsage := float64(mem.RSS) / 1024 / 1024
+			totalMemory += memoryUsage
+		}
+
+		// Distinguish between worker processes, cache processes, and other processes
+		if ppid == masterPID || strings.Contains(cmdline, "worker process") {
+			workerCount++
+		} else if strings.Contains(cmdline, "cache") {
+			cacheCount++
+		} else {
+			otherCount++
+		}
+	}
+
+	// Calculate the CPU usage
+	// First, measure the initial CPU time
+	times1 := make(map[int32]float64)
+	for _, p := range nginxProcesses {
+		times, err := p.Times()
+		if err == nil {
+			// CPU time = user time + system time
+			times1[p.Pid] = times.User + times.System
+		}
+	}
+
+	// Wait for a short period of time
+	time.Sleep(100 * time.Millisecond)
+
+	// Measure the CPU time again
+	totalCPUPercent := 0.0
+	for _, p := range nginxProcesses {
+		times, err := p.Times()
+		if err != nil {
+			continue
+		}
+
+		// Calculate the CPU time difference
+		currentTotal := times.User + times.System
+		if previousTotal, ok := times1[p.Pid]; ok {
+			// Calculate the CPU usage percentage during this period (considering multiple cores)
+			cpuDelta := currentTotal - previousTotal
+			// Calculate the CPU usage per second (considering the sampling time)
+			cpuPercent := (cpuDelta / 0.1) * 100.0 / float64(numCPU)
+			totalCPUPercent += cpuPercent
+		}
+	}
+
+	// Round to the nearest integer, which is more consistent with the top display
+	totalCPUPercent = math.Round(totalCPUPercent)
+
+	// Round the memory usage to two decimal places
+	totalMemory = math.Round(totalMemory*100) / 100
+
+	result.Workers = workerCount
+	result.Master = masterCount
+	result.Cache = cacheCount
+	result.Other = otherCount
+	result.CPUUsage = totalCPUPercent
+	result.MemoryUsage = totalMemory
+
+	return result, nil
+}

+ 199 - 0
internal/nginx/stub_status.go

@@ -0,0 +1,199 @@
+package nginx
+
+import (
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+	"regexp"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+// StubStatusInfo Store the stub_status module status
+type StubStatusInfo struct {
+	Enabled bool   `json:"stub_status_enabled"` // stub_status module is enabled
+	URL     string `json:"stub_status_url"`     // stub_status access address
+}
+
+type StubStatusData 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"`
+}
+
+const (
+	StubStatusPort       = 51828
+	StubStatusPath       = "/stub_status"
+	StubStatusHost       = "localhost"
+	StubStatusProtocol   = "http"
+	StubStatusAllow      = "127.0.0.1"
+	StubStatusDeny       = "all"
+	StubStatusConfigName = "stub_status_nginx-ui.conf"
+)
+
+// GetStubStatusData Get the stub_status module data
+func GetStubStatusData() (bool, *StubStatusData, error) {
+	result := &StubStatusData{
+		Active:   0,
+		Accepts:  0,
+		Handled:  0,
+		Requests: 0,
+		Reading:  0,
+		Writing:  0,
+		Waiting:  0,
+	}
+
+	// Get the stub_status status information
+	enabled, statusURL := IsStubStatusEnabled()
+	if !enabled {
+		return false, result, fmt.Errorf("stub_status is not enabled")
+	}
+
+	// Create an HTTP client
+	client := &http.Client{
+		Timeout: 5 * time.Second,
+	}
+
+	// Send a request to get the stub_status data
+	resp, err := client.Get(statusURL)
+	if err != nil {
+		return enabled, result, fmt.Errorf("failed to get stub status: %v", err)
+	}
+	defer resp.Body.Close()
+
+	// Read the response content
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return enabled, result, fmt.Errorf("failed to read response body: %v", err)
+	}
+
+	// Parse the response content
+	statusContent := string(body)
+
+	// Match the active connection number
+	activeRe := regexp.MustCompile(`Active connections:\s+(\d+)`)
+	if matches := activeRe.FindStringSubmatch(statusContent); len(matches) > 1 {
+		result.Active, _ = strconv.Atoi(matches[1])
+	}
+
+	// Match the request statistics information
+	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])
+	}
+
+	// Match the read and write waiting numbers
+	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 enabled, result, nil
+}
+
+// GetStubStatus Get the stub_status module status
+func GetStubStatus() *StubStatusInfo {
+	enabled, statusURL := IsStubStatusEnabled()
+	return &StubStatusInfo{
+		Enabled: enabled,
+		URL:     statusURL,
+	}
+}
+
+// IsStubStatusEnabled Check if the stub_status module is enabled and return the access address
+// Only check the stub_status_nginx-ui.conf configuration file
+func IsStubStatusEnabled() (bool, string) {
+	stubStatusConfPath := GetConfPath("conf.d", StubStatusConfigName)
+	if _, err := os.Stat(stubStatusConfPath); os.IsNotExist(err) {
+		return false, ""
+	}
+
+	ngxConfig, err := ParseNgxConfig(stubStatusConfPath)
+	if err != nil {
+		return false, ""
+	}
+
+	// Find the stub_status configuration
+	for _, server := range ngxConfig.Servers {
+		protocol := StubStatusProtocol
+		host := StubStatusHost
+		port := strconv.Itoa(StubStatusPort)
+
+		for _, location := range server.Locations {
+			// Check if the location content contains stub_status
+			if strings.Contains(location.Content, "stub_status") {
+				stubStatusURL := fmt.Sprintf("%s://%s:%s%s", protocol, host, port, StubStatusPath)
+				return true, stubStatusURL
+			}
+		}
+	}
+
+	return false, ""
+}
+
+// EnableStubStatus Enable stub_status module
+func EnableStubStatus() error {
+	enabled, _ := IsStubStatusEnabled()
+	if enabled {
+		return nil
+	}
+
+	return CreateStubStatusConfig()
+}
+
+// DisableStubStatus Disable stub_status module
+func DisableStubStatus() error {
+	stubStatusConfPath := GetConfPath("conf.d", StubStatusConfigName)
+	if _, err := os.Stat(stubStatusConfPath); os.IsNotExist(err) {
+		return nil
+	}
+
+	return os.Remove(stubStatusConfPath)
+}
+
+// CreateStubStatusConfig Create a new stub_status configuration file
+func CreateStubStatusConfig() error {
+	httpConfPath := GetConfPath("conf.d", StubStatusConfigName)
+
+	stubStatusConfig := `
+# DO NOT EDIT THIS FILE, IT IS AUTO GENERATED BY NGINX-UI
+# Nginx stub_status configuration for Nginx-UI
+# Modified at ` + time.Now().Format("2006-01-02 15:04:05") + `
+
+server {
+    listen 51828;  # Use non-standard port to avoid conflicts
+    server_name localhost;
+
+    # Status monitoring interface
+    location /stub_status {
+        stub_status;
+        allow 127.0.0.1; # Only allow local access
+        deny all;
+    }
+}
+`
+	ngxConfig, err := ParseNgxConfigByContent(stubStatusConfig)
+	if err != nil {
+		return errors.Wrap(err, "failed to parse new nginx config")
+	}
+
+	ngxConfig.FileName = httpConfPath
+	configText, err := ngxConfig.BuildConfig()
+	if err != nil {
+		return errors.Wrap(err, "failed to build nginx config")
+	}
+
+	return os.WriteFile(httpConfPath, []byte(configText), 0644)
+}