Просмотр исходного кода

Merge pull request #952 from akinoccc/dev

feat: nginx performance dashboard
Jacky 3 недель назад
Родитель
Сommit
8fbb0d9fa7

+ 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,

+ 8 - 0
api/nginx/router.go

@@ -13,6 +13,14 @@ func InitRouter(r *gin.RouterGroup) {
 	r.POST("nginx/restart", Restart)
 	r.POST("nginx/test", Test)
 	r.GET("nginx/status", Status)
+	// 获取 Nginx 详细状态信息,包括连接数、进程信息等(Issue #850)
+	r.GET("nginx/detail_status", GetDetailStatus)
+	// 使用SSE推送Nginx详细状态信息
+	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)
 }

+ 132 - 0
api/nginx/status.go

@@ -0,0 +1,132 @@
+// GetDetailedStatus API 实现
+// 该功能用于解决 Issue #850,提供类似宝塔面板的 Nginx 负载监控功能
+// 返回详细的 Nginx 状态信息,包括请求统计、连接数、工作进程等数据
+package nginx
+
+import (
+	"errors"
+	"net/http"
+	"strings"
+	"time"
+
+	"github.com/0xJacky/Nginx-UI/internal/nginx"
+	"github.com/gin-gonic/gin"
+	"github.com/uozi-tech/cosy"
+	"github.com/uozi-tech/cosy/logger"
+)
+
+// NginxPerformanceInfo 存储 Nginx 性能相关信息
+type NginxPerformanceInfo struct {
+	// 基本状态信息
+	nginx.StubStatusData
+
+	// 进程相关信息
+	nginx.NginxProcessInfo
+
+	// 配置信息
+	nginx.NginxConfigInfo
+}
+
+// GetDetailStatus 获取 Nginx 详细状态信息
+func GetDetailStatus(c *gin.Context) {
+	response := nginx.GetPerformanceData()
+	c.JSON(http.StatusOK, response)
+}
+
+// StreamDetailStatus 使用 SSE 流式推送 Nginx 详细状态信息
+func StreamDetailStatus(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 {
+	response := nginx.GetPerformanceData()
+
+	// 发送 SSE 事件
+	c.SSEvent("message", response)
+
+	// 刷新缓冲区,确保数据立即发送
+	c.Writer.Flush()
+	return nil
+}
+
+// CheckStubStatus 获取 Nginx stub_status 模块状态
+func CheckStubStatus(c *gin.Context) {
+	stubStatus := nginx.GetStubStatus()
+
+	c.JSON(http.StatusOK, stubStatus)
+}
+
+// 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()
+	}
+
+	if err != nil {
+		cosy.ErrHandler(c, err)
+		return
+	}
+
+	// 重新加载 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
+	}
+
+	// 检查操作后的状态
+	newStubStatus := nginx.GetStubStatus()
+
+	c.JSON(http.StatusOK, newStubStatus)
+}

+ 22 - 0
app/src/api/ngx.ts

@@ -35,6 +35,24 @@ export interface NgxLocation {
 
 export type DirectiveMap = Record<string, { links: string[] }>
 
+export interface NginxPerformanceInfo {
+  active: number // 活动连接数
+  accepts: number // 总握手次数
+  handled: number // 总连接次数
+  requests: number // 总请求数
+  reading: number // 读取客户端请求数
+  writing: number // 响应数
+  waiting: number // 驻留进程(等待请求)
+  workers: number // 工作进程数
+  master: number // 主进程数
+  cache: number // 缓存管理进程数
+  other: number // 其他Nginx相关进程数
+  cpu_usage: number // CPU 使用率
+  memory_usage: number // 内存使用率(MB)
+  worker_processes: number // worker_processes 配置
+  worker_connections: number // worker_connections 配置
+}
+
 const ngx = {
   build_config(ngxConfig: NgxConfig) {
     return http.post('/ngx/build_config', ngxConfig)
@@ -52,6 +70,10 @@ const ngx = {
     return http.get('/nginx/status')
   },
 
+  detail_status(): Promise<{ running: boolean, stub_status_enabled: boolean, info: NginxPerformanceInfo }> {
+    return http.get('/nginx/detail_status')
+  },
+
   reload() {
     return http.post('/nginx/reload')
   },

+ 17 - 35
app/src/components/EnvGroupTabs/EnvGroupTabs.vue

@@ -2,9 +2,9 @@
 import type { EnvGroup } from '@/api/env_group'
 import type { Environment } from '@/api/environment'
 import nodeApi from '@/api/node'
+import { useSSE } from '@/composables/useSSE'
 import { useUserStore } from '@/pinia'
 import { message } from 'ant-design-vue'
-import { SSE } from 'sse.js'
 
 const props = defineProps<{
   envGroups: EnvGroup[]
@@ -15,57 +15,39 @@ const { token } = storeToRefs(useUserStore())
 
 const environments = ref<Environment[]>([])
 const environmentsMap = ref<Record<number, Environment>>({})
-const sse = shallowRef<SSE>()
 const loading = ref({
   reload: false,
   restart: false,
 })
 
+// 使用SSE composable
+const { connect, disconnect } = useSSE()
+
 // Get node data when tab is not 'All'
 watch(modelValue, newVal => {
   if (newVal && newVal !== 0) {
     connectSSE()
   }
   else {
-    disconnectSSE()
+    disconnect()
   }
 }, { immediate: true })
 
-onUnmounted(() => {
-  disconnectSSE()
-})
-
 function connectSSE() {
-  disconnectSSE()
-
-  const s = new SSE('api/environments/enabled', {
-    headers: {
-      Authorization: token.value,
+  connect({
+    url: 'api/environments/enabled',
+    token: token.value,
+    onMessage: data => {
+      environments.value = data
+      environmentsMap.value = environments.value.reduce((acc, node) => {
+        acc[node.id] = node
+        return acc
+      }, {} as Record<number, Environment>)
+    },
+    onError: () => {
+      // 错误处理已由useSSE内部实现自动重连
     },
   })
-
-  s.onmessage = e => {
-    environments.value = JSON.parse(e.data)
-    environmentsMap.value = environments.value.reduce((acc, node) => {
-      acc[node.id] = node
-      return acc
-    }, {} as Record<number, Environment>)
-  }
-
-  s.onerror = () => {
-    setTimeout(() => {
-      connectSSE()
-    }, 5000)
-  }
-
-  sse.value = s
-}
-
-function disconnectSSE() {
-  if (sse.value) {
-    sse.value.close()
-    sse.value = undefined
-  }
 }
 
 // Get the current Node Group data

+ 88 - 0
app/src/composables/useNginxPerformance.ts

@@ -0,0 +1,88 @@
+import type { NginxPerformanceInfo } from '@/api/ngx'
+import ngx from '@/api/ngx'
+import { computed, ref } from 'vue'
+
+// 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)
+
+  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(() => {
+    if (!lastUpdateTime.value)
+      return $gettext('Unknown')
+    return formatTimeAgo(lastUpdateTime.value)
+  })
+
+  // Update the last update time
+  function updateLastUpdateTime() {
+    lastUpdateTime.value = new Date()
+  }
+
+  // Check stub_status availability and get initial data
+  async function fetchInitialData() {
+    try {
+      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('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
+    }
+  }
+
+  return {
+    loading,
+    nginxInfo,
+    error,
+    formattedUpdateTime,
+    updateLastUpdateTime,
+    fetchInitialData,
+    stubStatusEnabled,
+    stubStatusLoading,
+    stubStatusError,
+  }
+}

+ 194 - 0
app/src/composables/usePerformanceMetrics.ts

@@ -0,0 +1,194 @@
+import type { NginxPerformanceInfo } from '@/api/ngx'
+import type { Ref } from 'vue'
+import { computed } from 'vue'
+
+export function usePerformanceMetrics(nginxInfo: Ref<NginxPerformanceInfo | undefined>) {
+  // Format numbers to a more readable form
+  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()
+  }
+
+  // Active connections percentage
+  const activeConnectionsPercent = computed(() => {
+    if (!nginxInfo.value) {
+      return 0
+    }
+    const maxConnections = nginxInfo.value.worker_connections * nginxInfo.value.worker_processes
+    return Number(((nginxInfo.value.active / maxConnections) * 100).toFixed(2))
+  })
+
+  // Worker processes usage percentage
+  const workerProcessesPercent = computed(() => {
+    if (!nginxInfo.value) {
+      return 0
+    }
+    return Number(((nginxInfo.value.workers / nginxInfo.value.worker_processes) * 100).toFixed(2))
+  })
+
+  // Requests per connection
+  const requestsPerConnection = computed(() => {
+    if (!nginxInfo.value || nginxInfo.value.handled === 0) {
+      return 0
+    }
+    return (nginxInfo.value.requests / nginxInfo.value.handled).toFixed(2)
+  })
+
+  // Maximum requests per second
+  const maxRPS = computed(() => {
+    if (!nginxInfo.value) {
+      return 0
+    }
+    return nginxInfo.value.worker_processes * nginxInfo.value.worker_connections
+  })
+
+  // Process composition data
+  const processTypeData = computed(() => {
+    if (!nginxInfo.value) {
+      return []
+    }
+
+    return [
+      { type: $gettext('Worker Processes'), value: nginxInfo.value.workers, color: '#1890ff' },
+      { type: $gettext('Master Process'), value: nginxInfo.value.master, color: '#52c41a' },
+      { type: $gettext('Cache Processes'), value: nginxInfo.value.cache, color: '#faad14' },
+      { type: $gettext('Other Processes'), value: nginxInfo.value.other, color: '#f5222d' },
+    ]
+  })
+
+  // Resource utilization
+  const resourceUtilization = computed(() => {
+    if (!nginxInfo.value) {
+      return 0
+    }
+
+    const cpuFactor = Math.min(nginxInfo.value.cpu_usage / 100, 1)
+    const maxConnections = nginxInfo.value.worker_connections * nginxInfo.value.worker_processes
+    const connectionFactor = Math.min(nginxInfo.value.active / maxConnections, 1)
+
+    return Math.round((cpuFactor * 0.5 + connectionFactor * 0.5) * 100)
+  })
+
+  // Table data
+  const statusData = computed(() => {
+    if (!nginxInfo.value) {
+      return []
+    }
+
+    return [
+      {
+        key: '1',
+        name: $gettext('Active connections'),
+        value: formatNumber(nginxInfo.value.active),
+      },
+      {
+        key: '2',
+        name: $gettext('Total handshakes'),
+        value: formatNumber(nginxInfo.value.accepts),
+      },
+      {
+        key: '3',
+        name: $gettext('Total connections'),
+        value: formatNumber(nginxInfo.value.handled),
+      },
+      {
+        key: '4',
+        name: $gettext('Total requests'),
+        value: formatNumber(nginxInfo.value.requests),
+      },
+      {
+        key: '5',
+        name: $gettext('Read requests'),
+        value: nginxInfo.value.reading,
+      },
+      {
+        key: '6',
+        name: $gettext('Responses'),
+        value: nginxInfo.value.writing,
+      },
+      {
+        key: '7',
+        name: $gettext('Waiting processes'),
+        value: nginxInfo.value.waiting,
+      },
+    ]
+  })
+
+  // Worker processes data
+  const workerData = computed(() => {
+    if (!nginxInfo.value) {
+      return []
+    }
+
+    return [
+      {
+        key: '1',
+        name: $gettext('Number of worker processes'),
+        value: nginxInfo.value.workers,
+      },
+      {
+        key: '2',
+        name: $gettext('Master process'),
+        value: nginxInfo.value.master,
+      },
+      {
+        key: '3',
+        name: $gettext('Cache manager processes'),
+        value: nginxInfo.value.cache,
+      },
+      {
+        key: '4',
+        name: $gettext('Other Nginx processes'),
+        value: nginxInfo.value.other,
+      },
+      {
+        key: '5',
+        name: $gettext('Nginx CPU usage rate'),
+        value: `${nginxInfo.value.cpu_usage.toFixed(2)}%`,
+      },
+      {
+        key: '6',
+        name: $gettext('Nginx Memory usage'),
+        value: `${nginxInfo.value.memory_usage.toFixed(2)} MB`,
+      },
+    ]
+  })
+
+  // Configuration data
+  const configData = computed(() => {
+    if (!nginxInfo.value) {
+      return []
+    }
+
+    return [
+      {
+        key: '1',
+        name: $gettext('Number of worker processes'),
+        value: nginxInfo.value.worker_processes,
+      },
+      {
+        key: '2',
+        name: $gettext('Maximum number of connections per worker process'),
+        value: nginxInfo.value.worker_connections,
+      },
+    ]
+  })
+
+  return {
+    formatNumber,
+    activeConnectionsPercent,
+    workerProcessesPercent,
+    requestsPerConnection,
+    maxRPS,
+    processTypeData,
+    resourceUtilization,
+    statusData,
+    workerData,
+    configData,
+  }
+}

+ 91 - 0
app/src/composables/useSSE.ts

@@ -0,0 +1,91 @@
+import type { SSEvent } from 'sse.js'
+import { SSE } from 'sse.js'
+import { onUnmounted, shallowRef } from 'vue'
+
+export interface SSEOptions {
+  url: string
+  token: string
+  onMessage?: (data: any) => void
+  onError?: () => void
+  parseData?: boolean
+  reconnectInterval?: number
+}
+
+/**
+ * SSE Composable
+ * Provide the ability to create, manage, and automatically clean up SSE connections
+ */
+export function useSSE() {
+  const sseInstance = shallowRef<SSE>()
+
+  /**
+   * Connect to SSE service
+   */
+  function connect(options: SSEOptions) {
+    disconnect()
+
+    const {
+      url,
+      token,
+      onMessage,
+      onError,
+      parseData = true,
+      reconnectInterval = 5000,
+    } = options
+
+    const sse = new SSE(url, {
+      headers: {
+        Authorization: token,
+      },
+    })
+
+    // Handle messages
+    sse.onmessage = (e: SSEvent) => {
+      if (!e.data) {
+        return
+      }
+
+      try {
+        const parsedData = parseData ? JSON.parse(e.data) : e.data
+        onMessage?.(parsedData)
+      }
+      catch (error) {
+        console.error('Error parsing SSE message:', error)
+      }
+    }
+
+    // Handle errors and reconnect
+    sse.onerror = () => {
+      onError?.()
+
+      // Reconnect logic
+      setTimeout(() => {
+        connect(options)
+      }, reconnectInterval)
+    }
+
+    sseInstance.value = sse
+    return sse
+  }
+
+  /**
+   * Disconnect SSE connection
+   */
+  function disconnect() {
+    if (sseInstance.value) {
+      sseInstance.value.close()
+      sseInstance.value = undefined
+    }
+  }
+
+  // Automatically disconnect when the component is unmounted
+  onUnmounted(() => {
+    disconnect()
+  })
+
+  return {
+    connect,
+    disconnect,
+    sseInstance,
+  }
+}

+ 4 - 3
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)
@@ -121,7 +122,7 @@ export function setupResponseInterceptor() {
       const otpModal = use2FAModal()
 
       // Handle authentication errors
-      if (error.response) {
+      if (error?.response) {
         switch (error.response.status) {
           case 401:
             secureSessionId.value = ''
@@ -135,7 +136,7 @@ export function setupResponseInterceptor() {
       }
 
       // Handle JSON error that comes back as Blob for blob request type
-      if (error.response?.data instanceof Blob && error.response.data.type === 'application/json') {
+      if (error?.response?.data instanceof Blob && error?.response?.data?.type === 'application/json') {
         try {
           const text = await error.response.data.text()
           error.response.data = JSON.parse(text)

+ 19 - 1
app/src/routes/modules/dashboard.ts

@@ -4,11 +4,29 @@ import { HomeOutlined } from '@ant-design/icons-vue'
 export const dashboardRoutes: RouteRecordRaw[] = [
   {
     path: 'dashboard',
-    component: () => import('@/views/dashboard/DashBoard.vue'),
+    redirect: '/dashboard/server',
     name: 'Dashboard',
     meta: {
       name: () => $gettext('Dashboard'),
       icon: HomeOutlined,
     },
+    children: [
+      {
+        path: 'server',
+        component: () => import('@/views/dashboard/ServerDashBoard.vue'),
+        name: 'Server',
+        meta: {
+          name: () => $gettext('Server'),
+        },
+      },
+      {
+        path: 'nginx',
+        component: () => import('@/views/dashboard/NginxDashBoard.vue'),
+        name: 'NginxPerformance',
+        meta: {
+          name: () => $gettext('Nginx'),
+        },
+      },
+    ],
   },
 ]

+ 1 - 1
app/src/version.json

@@ -1 +1 @@
-{"version":"2.0.0-rc.5","build_id":12,"total_build":406}
+{"version":"2.0.0-rc.5","build_id":13,"total_build":407}

+ 220 - 0
app/src/views/dashboard/NginxDashBoard.vue

@@ -0,0 +1,220 @@
+<script setup lang="ts">
+import { useNginxPerformance } from '@/composables/useNginxPerformance'
+import { useSSE } from '@/composables/useSSE'
+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'
+import PerformanceTablesCard from './components/PerformanceTablesCard.vue'
+import ProcessDistributionCard from './components/ProcessDistributionCard.vue'
+import ResourceUsageCard from './components/ResourceUsageCard.vue'
+
+// Global state
+const global = useGlobalStore()
+const { nginxStatus: status } = storeToRefs(global)
+const { token } = storeToRefs(useUserStore())
+
+// Use performance data composable
+const {
+  loading,
+  nginxInfo,
+  error,
+  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/detail_status/stream',
+    token: token.value,
+    onMessage: data => {
+      loading.value = false
+
+      if (data.running) {
+        nginxInfo.value = data.info
+        updateLastUpdateTime()
+      }
+      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...')
+
+      // If the connection fails, try to get data using the traditional method
+      setTimeout(() => {
+        fetchInitialData()
+      }, 2000)
+    },
+  })
+}
+
+// Manually refresh data
+function refreshData() {
+  fetchInitialData().then(connectSSE)
+}
+
+// Initialize connection when the component is mounted
+onMounted(() => {
+  fetchInitialData().then(connectSSE)
+})
+</script>
+
+<template>
+  <div>
+    <!-- Top operation bar -->
+    <div class="mb-4 mx-6 md:mx-0 flex flex-wrap justify-between items-center">
+      <div class="flex items-center">
+        <ABadge :status="status === NginxStatus.Running ? 'success' : 'error'" />
+        <span class="font-medium">{{ status === NginxStatus.Running ? $gettext('Nginx is running') : $gettext('Nginx is not running') }}</span>
+      </div>
+      <div class="flex items-center">
+        <ClockCircleOutlined class="mr-1 text-gray-500" />
+        <span class="mr-4 text-gray-500 text-sm text-nowrap">{{ $gettext('Last update') }}: {{ formattedUpdateTime }}</span>
+        <AButton type="text" size="small" :loading="loading" @click="refreshData">
+          <template #icon>
+            <ReloadOutlined />
+          </template>
+        </AButton>
+      </div>
+    </div>
+
+    <!-- Nginx status prompt -->
+    <AAlert
+      v-if="status !== NginxStatus.Running"
+      class="mb-4"
+      type="warning"
+      show-icon
+      :message="$gettext('Nginx is not running')"
+      :description="$gettext('Cannot get performance data in this state')"
+    />
+
+    <!-- Error prompt -->
+    <AAlert
+      v-if="error"
+      class="mb-4"
+      type="error"
+      show-icon
+      :message="$gettext('Get data failed')"
+      :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">
+        <AEmpty :description="$gettext('No data')" />
+      </div>
+
+      <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">
+          <!-- Metrics card -->
+          <ACol :sm="24" :lg="12">
+            <ConnectionMetricsCard :nginx-info="nginxInfo" />
+          </ACol>
+
+          <!-- CPU and memory usage -->
+          <ACol :sm="24" :lg="12">
+            <ResourceUsageCard :nginx-info="nginxInfo" />
+          </ACol>
+        </ARow>
+
+        <!-- Resource monitoring -->
+        <ARow :gutter="[16, 16]" class="mb-4">
+          <!-- Process distribution -->
+          <ACol :span="24">
+            <ProcessDistributionCard :nginx-info="nginxInfo" />
+          </ACol>
+        </ARow>
+
+        <!-- Performance metrics table -->
+        <ARow :gutter="[16, 16]" class="mb-4">
+          <ACol :span="24">
+            <PerformanceTablesCard :nginx-info="nginxInfo" />
+          </ACol>
+        </ARow>
+      </div>
+    </ASpin>
+  </div>
+</template>

+ 0 - 0
app/src/views/dashboard/DashBoard.vue → app/src/views/dashboard/ServerDashBoard.vue


+ 64 - 0
app/src/views/dashboard/components/ConnectionMetricsCard.vue

@@ -0,0 +1,64 @@
+<script setup lang="ts">
+import type { NginxPerformanceInfo } from '@/api/ngx'
+import { computed, defineProps } from 'vue'
+
+const props = defineProps<{
+  nginxInfo: NginxPerformanceInfo
+}>()
+
+// Active connections percentage
+const activeConnectionsPercent = computed(() => {
+  const maxConnections = props.nginxInfo.worker_connections * props.nginxInfo.worker_processes
+  return Number(((props.nginxInfo.active / maxConnections) * 100).toFixed(2))
+})
+
+// Worker processes usage percentage
+const workerProcessesPercent = computed(() => {
+  return Number(((props.nginxInfo.workers / props.nginxInfo.worker_processes) * 100).toFixed(2))
+})
+</script>
+
+<template>
+  <ARow :gutter="[16, 16]" class="h-full">
+    <!-- Current active connections -->
+    <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">
+            {{ $gettext('Current active connections') }}
+          </div>
+          <div class="flex items-baseline mb-2">
+            <span class="text-2xl font-bold mr-2">{{ nginxInfo.active }}</span>
+            <span class="text-gray-500 text-sm">/ {{ nginxInfo.worker_connections * nginxInfo.worker_processes }}</span>
+          </div>
+          <AProgress
+            :percent="activeConnectionsPercent"
+            :format="percent => `${percent?.toFixed(2)}%`"
+            :status="activeConnectionsPercent > 80 ? 'exception' : 'normal'"
+            size="small"
+          />
+        </div>
+      </ACard>
+    </ACol>
+
+    <!-- Worker processes -->
+    <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">
+            {{ $gettext('Worker Processes') }}
+          </div>
+          <div class="flex items-baseline mb-2">
+            <span class="text-2xl font-bold mr-2">{{ nginxInfo.workers }}</span>
+            <span class="text-gray-500 text-sm">/ {{ nginxInfo.worker_processes }}</span>
+          </div>
+          <AProgress
+            :percent="workerProcessesPercent"
+            size="small"
+            status="active"
+          />
+        </div>
+      </ACard>
+    </ACol>
+  </ARow>
+</template>

+ 107 - 0
app/src/views/dashboard/components/PerformanceStatisticsCard.vue

@@ -0,0 +1,107 @@
+<script setup lang="ts">
+import type { NginxPerformanceInfo } from '@/api/ngx'
+import {
+  ApiOutlined,
+  CloudServerOutlined,
+  DashboardOutlined,
+  InfoCircleOutlined,
+  ThunderboltOutlined,
+} from '@ant-design/icons-vue'
+import { computed, defineProps } from 'vue'
+
+const props = defineProps<{
+  nginxInfo: NginxPerformanceInfo
+}>()
+
+// Calculate connection efficiency - requests per connection
+const requestsPerConnection = computed(() => {
+  if (props.nginxInfo.handled === 0) {
+    return '0'
+  }
+  return (props.nginxInfo.requests / props.nginxInfo.handled).toFixed(2)
+})
+
+// Estimate maximum requests per second
+const maxRPS = computed(() => {
+  return props.nginxInfo.worker_processes * props.nginxInfo.worker_connections
+})
+</script>
+
+<template>
+  <ARow :gutter="[16, 24]">
+    <!-- Maximum RPS -->
+    <ACol :xs="24" :sm="12" :md="8" :lg="6">
+      <AStatistic
+        :value="maxRPS"
+        :value-style="{ color: '#1890ff', fontSize: '24px' }"
+      >
+        <template #prefix>
+          <ThunderboltOutlined />
+        </template>
+        <template #title>
+          {{ $gettext('Max Requests Per Second') }}
+          <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" />
+          </ATooltip>
+        </template>
+      </AStatistic>
+      <div class="text-xs text-gray-500 mt-1">
+        worker_processes ({{ nginxInfo.worker_processes }}) × worker_connections ({{ nginxInfo.worker_connections }})
+      </div>
+    </ACol>
+
+    <!-- Maximum concurrent connections -->
+    <ACol :xs="24" :sm="12" :md="8" :lg="6">
+      <AStatistic
+        :title="$gettext('Max Concurrent Connections')"
+        :value="nginxInfo.worker_processes * nginxInfo.worker_connections"
+        :value-style="{ color: '#52c41a', fontSize: '24px' }"
+      >
+        <template #prefix>
+          <ApiOutlined />
+        </template>
+      </AStatistic>
+      <div class="text-xs text-gray-500 mt-1">
+        {{ $gettext('Current usage') }}: {{ ((nginxInfo.active / (nginxInfo.worker_processes * nginxInfo.worker_connections)) * 100).toFixed(2) }}%
+      </div>
+    </ACol>
+
+    <!-- Requests per connection -->
+    <ACol :xs="24" :sm="12" :md="8" :lg="6">
+      <AStatistic
+        :value="requestsPerConnection"
+        :precision="2"
+        :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>
+      </AStatistic>
+      <div class="text-xs text-gray-500 mt-1">
+        {{ $gettext('Higher value means better connection reuse') }}
+      </div>
+    </ACol>
+
+    <!-- Total Nginx processes -->
+    <ACol :xs="24" :sm="12" :md="8" :lg="6">
+      <AStatistic
+        :title="$gettext('Total Nginx Processes')"
+        :value="nginxInfo.workers + nginxInfo.master + nginxInfo.cache + nginxInfo.other"
+        :value-style="{ color: '#722ed1', fontSize: '24px' }"
+      >
+        <template #prefix>
+          <CloudServerOutlined />
+        </template>
+      </AStatistic>
+      <div class="text-xs text-gray-500 mt-1">
+        {{ $gettext('Workers') }}: {{ nginxInfo.workers }}, {{ $gettext('Master') }}: {{ nginxInfo.master }}, {{ $gettext('Others') }}: {{ nginxInfo.cache + nginxInfo.other }}
+      </div>
+    </ACol>
+  </ARow>
+</template>

+ 211 - 0
app/src/views/dashboard/components/PerformanceTablesCard.vue

@@ -0,0 +1,211 @@
+<script setup lang="ts">
+import type { NginxPerformanceInfo } from '@/api/ngx'
+import type { TableColumnType } from 'ant-design-vue'
+import { InfoCircleOutlined } from '@ant-design/icons-vue'
+import { computed, defineProps, ref } from 'vue'
+
+const props = defineProps<{
+  nginxInfo: NginxPerformanceInfo
+}>()
+
+const activeTabKey = ref('status')
+
+// Table column definition
+const columns: TableColumnType[] = [
+  {
+    title: $gettext('Indicator'),
+    dataIndex: 'name',
+    key: 'name',
+    width: '30%',
+  },
+  {
+    title: $gettext('Value'),
+    dataIndex: 'value',
+    key: 'value',
+  },
+]
+
+// 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()
+}
+
+// Status data
+const statusData = computed(() => {
+  return [
+    {
+      key: '1',
+      name: $gettext('Active connections'),
+      value: formatNumber(props.nginxInfo.active),
+    },
+    {
+      key: '2',
+      name: $gettext('Total handshakes'),
+      value: formatNumber(props.nginxInfo.accepts),
+    },
+    {
+      key: '3',
+      name: $gettext('Total connections'),
+      value: formatNumber(props.nginxInfo.handled),
+    },
+    {
+      key: '4',
+      name: $gettext('Total requests'),
+      value: formatNumber(props.nginxInfo.requests),
+    },
+    {
+      key: '5',
+      name: $gettext('Read requests'),
+      value: props.nginxInfo.reading,
+    },
+    {
+      key: '6',
+      name: $gettext('Responses'),
+      value: props.nginxInfo.writing,
+    },
+    {
+      key: '7',
+      name: $gettext('Waiting processes'),
+      value: props.nginxInfo.waiting,
+    },
+  ]
+})
+
+// Worker processes data
+const workerData = computed(() => {
+  return [
+    {
+      key: '1',
+      name: $gettext('Number of worker processes'),
+      value: props.nginxInfo.workers,
+    },
+    {
+      key: '2',
+      name: $gettext('Master process'),
+      value: props.nginxInfo.master,
+    },
+    {
+      key: '3',
+      name: $gettext('Cache manager processes'),
+      value: props.nginxInfo.cache,
+    },
+    {
+      key: '4',
+      name: $gettext('Other Nginx processes'),
+      value: props.nginxInfo.other,
+    },
+    {
+      key: '5',
+      name: $gettext('Nginx CPU usage rate'),
+      value: `${props.nginxInfo.cpu_usage.toFixed(2)}%`,
+    },
+    {
+      key: '6',
+      name: $gettext('Nginx Memory usage'),
+      value: `${props.nginxInfo.memory_usage.toFixed(2)} MB`,
+    },
+  ]
+})
+
+// Configuration data
+const configData = computed(() => {
+  return [
+    {
+      key: '1',
+      name: $gettext('Number of worker processes'),
+      value: props.nginxInfo.worker_processes,
+    },
+    {
+      key: '2',
+      name: $gettext('Maximum number of connections per worker process'),
+      value: props.nginxInfo.worker_connections,
+    },
+  ]
+})
+
+// Maximum requests per second
+const maxRPS = computed(() => {
+  return props.nginxInfo.worker_processes * props.nginxInfo.worker_connections
+})
+</script>
+
+<template>
+  <ACard :bordered="false">
+    <ATabs v-model:active-key="activeTabKey">
+      <!-- Request statistics -->
+      <ATabPane key="status" :tab="$gettext('Request statistics')">
+        <div class="overflow-x-auto">
+          <ATable
+            :columns="columns"
+            :data-source="statusData"
+            :pagination="false"
+            size="middle"
+            :scroll="{ x: '100%' }"
+          />
+        </div>
+      </ATabPane>
+
+      <!-- Process information -->
+      <ATabPane key="workers" :tab="$gettext('Process information')">
+        <div class="overflow-x-auto">
+          <ATable
+            :columns="columns"
+            :data-source="workerData"
+            :pagination="false"
+            size="middle"
+            :scroll="{ x: '100%' }"
+          />
+        </div>
+      </ATabPane>
+
+      <!-- Configuration information -->
+      <ATabPane key="config" :tab="$gettext('Configuration information')">
+        <div class="overflow-x-auto">
+          <ATable
+            :columns="columns"
+            :data-source="configData"
+            :pagination="false"
+            size="middle"
+            :scroll="{ x: '100%' }"
+          />
+        </div>
+        <div class="mt-4">
+          <AAlert type="info" show-icon>
+            <template #message>
+              {{ $gettext('Nginx theoretical maximum performance') }}
+            </template>
+            <template #description>
+              <p>
+                {{ $gettext('Theoretical maximum concurrent connections:') }}
+                <strong>{{ nginxInfo.worker_processes * nginxInfo.worker_connections }}</strong>
+              </p>
+              <p>
+                {{ $gettext('Theoretical maximum RPS (Requests Per Second):') }}
+                <strong>{{ maxRPS }}</strong>
+                <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" />
+                </ATooltip>
+              </p>
+              <p>
+                {{ $gettext('Maximum worker process number:') }}
+                <strong>{{ nginxInfo.worker_processes }}</strong>
+                <span class="text-gray-500 text-xs ml-2">
+                  {{ nginxInfo.worker_processes === nginxInfo.workers ? $gettext('auto = CPU cores') : $gettext('manually set') }}
+                </span>
+              </p>
+              <p class="mb-0">
+                {{ $gettext('Tips: You can increase the concurrency processing capacity by increasing worker_processes or worker_connections') }}
+              </p>
+            </template>
+          </AAlert>
+        </div>
+      </ATabPane>
+    </ATabs>
+  </ACard>
+</template>

+ 60 - 0
app/src/views/dashboard/components/ProcessDistributionCard.vue

@@ -0,0 +1,60 @@
+<script setup lang="ts">
+import type { NginxPerformanceInfo } from '@/api/ngx'
+import { InfoCircleOutlined } from '@ant-design/icons-vue'
+
+const props = defineProps<{
+  nginxInfo: NginxPerformanceInfo
+}>()
+
+// Process composition data
+const processTypeData = computed(() => {
+  return [
+    { type: $gettext('Worker Processes'), value: props.nginxInfo.workers, color: '#1890ff' },
+    { type: $gettext('Master Process'), value: props.nginxInfo.master, color: '#52c41a' },
+    { type: $gettext('Cache Processes'), value: props.nginxInfo.cache, color: '#faad14' },
+    { type: $gettext('Other Processes'), value: props.nginxInfo.other, color: '#f5222d' },
+  ]
+})
+
+// Total processes
+const totalProcesses = computed(() => {
+  return props.nginxInfo.workers + props.nginxInfo.master + props.nginxInfo.cache + props.nginxInfo.other
+})
+</script>
+
+<template>
+  <ACard :title="$gettext('Process Distribution')" :bordered="false" class="h-full" :body-style="{ height: 'calc(100% - 58px)' }">
+    <div class="process-distribution h-full flex flex-col justify-between">
+      <div>
+        <div v-for="(item, index) in processTypeData" :key="index" class="mb-3">
+          <div class="flex items-center">
+            <div class="w-3 h-3 rounded-full mr-2" :style="{ backgroundColor: item.color }" />
+            <div class="flex-grow truncate">
+              {{ item.type }}
+            </div>
+            <div class="font-medium w-8 text-right">
+              {{ item.value }}
+            </div>
+          </div>
+          <AProgress
+            :percent="totalProcesses === 0 ? 0 : (item.value / totalProcesses) * 100"
+            :stroke-color="item.color"
+            size="small"
+            :show-info="false"
+          />
+        </div>
+      </div>
+      <div class="mt-auto text-xs text-gray-500 truncate">
+        {{ $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>

+ 61 - 0
app/src/views/dashboard/components/ResourceUsageCard.vue

@@ -0,0 +1,61 @@
+<script setup lang="ts">
+import type { NginxPerformanceInfo } from '@/api/ngx'
+import {
+  FundProjectionScreenOutlined,
+  InfoCircleOutlined,
+  ThunderboltOutlined,
+} from '@ant-design/icons-vue'
+
+const props = defineProps<{
+  nginxInfo: NginxPerformanceInfo
+}>()
+
+const cpuUsage = computed(() => {
+  return Number(Math.min(props.nginxInfo.cpu_usage, 100).toFixed(2))
+})
+</script>
+
+<template>
+  <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]">
+        <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' }">{{ cpuUsage.toFixed(2) }}%</span>
+            </div>
+          </div>
+          <AProgress
+            :percent="cpuUsage"
+            :status="nginxInfo.cpu_usage > 80 ? 'exception' : 'active'"
+            size="small"
+            class="mt-1"
+            :show-info="false"
+          />
+          <div v-if="nginxInfo.cpu_usage > 50" class="text-xs text-orange-500 mt-1">
+            {{ $gettext('CPU usage is relatively high, consider optimizing Nginx configuration') }}
+          </div>
+        </ACol>
+      </ARow>
+
+      <!-- Memory usage -->
+      <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">
+              <FundProjectionScreenOutlined />
+            </div>
+            <div class="text-base font-medium">
+              {{ $gettext('Memory Usage(RSS)') }}: <span class="text-blue-500">{{ nginxInfo.memory_usage.toFixed(2) }} MB</span>
+            </div>
+            <ATooltip :title="$gettext('Resident Set Size: Actual memory resident in physical memory, including all shared library memory, which will be repeated calculated for multiple processes')">
+              <InfoCircleOutlined class="ml-1 text-gray-500" />
+            </ATooltip>
+          </div>
+        </ACol>
+      </ARow>
+    </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)
+}