diff --git a/api/nginx/control.go b/api/nginx/control.go index f6b492a7..c2038f5e 100644 --- a/api/nginx/control.go +++ b/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, diff --git a/api/nginx/router.go b/api/nginx/router.go index 27a95f78..e7be797d 100644 --- a/api/nginx/router.go +++ b/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) } diff --git a/api/nginx/status.go b/api/nginx/status.go index 19d4adcf..5b8d5e1b 100644 --- a/api/nginx/status.go +++ b/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, - } +// CheckStubStatus 获取 Nginx stub_status 模块状态 +func CheckStubStatus(c *gin.Context) { + stubStatus := nginx.GetStubStatus() - // 默认尝试访问 stub_status 页面 - statusURL := "http://localhost/stub_status" - - // 创建 HTTP 客户端 - client := &http.Client{ - Timeout: 5 * time.Second, - } - - // 发送请求获取 stub_status 数据 - resp, err := client.Get(statusURL) - if err != nil { - return result, fmt.Errorf("failed to get stub status: %v", err) - } - defer resp.Body.Close() - - // 读取响应内容 - body, err := io.ReadAll(resp.Body) - if err != nil { - return result, fmt.Errorf("failed to read response body: %v", err) - } - - // 解析响应内容 - statusContent := string(body) - - // 匹配活动连接数 - activeRe := regexp.MustCompile(`Active connections:\s+(\d+)`) - if matches := activeRe.FindStringSubmatch(statusContent); len(matches) > 1 { - result["active"], _ = strconv.Atoi(matches[1]) - } - - // 匹配请求统计信息 - serverRe := regexp.MustCompile(`(\d+)\s+(\d+)\s+(\d+)`) - if matches := serverRe.FindStringSubmatch(statusContent); len(matches) > 3 { - result["accepts"], _ = strconv.Atoi(matches[1]) - result["handled"], _ = strconv.Atoi(matches[2]) - result["requests"], _ = strconv.Atoi(matches[3]) - } - - // 匹配读写等待数 - connRe := regexp.MustCompile(`Reading:\s+(\d+)\s+Writing:\s+(\d+)\s+Waiting:\s+(\d+)`) - if matches := connRe.FindStringSubmatch(statusContent); len(matches) > 3 { - result["reading"], _ = strconv.Atoi(matches[1]) - result["writing"], _ = strconv.Atoi(matches[2]) - result["waiting"], _ = strconv.Atoi(matches[3]) - } - - return result, nil + 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"` + } + + 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() } - // 查找所有 Nginx 进程 - processes, err := process.Processes() if err != nil { - return result, fmt.Errorf("failed to get processes: %v", err) + cosy.ErrHandler(c, err) + return } - 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 配置 + 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 } - // 遍历所有进程,区分工作进程和其他Nginx进程 - for _, p := range processes { - if p.Pid == masterPID { - continue // 已经计算过主进程 - } + // 检查操作后的状态 + newStubStatus := nginx.GetStubStatus() - name, err := p.Name() - if err != nil { - continue - } - - // 只处理Nginx相关进程 - if !strings.Contains(strings.ToLower(name), "nginx") { - continue - } - - // 添加到Nginx进程列表 - nginxProcesses = append(nginxProcesses, p) - - // 获取父进程PID - ppid, err := p.Ppid() - if err != nil { - continue - } - - cmdline, err := p.Cmdline() - if err != nil { - continue - } - - // 获取内存使用情况 - 使用RSS代替 - // 注意:理想情况下我们应该使用USS(仅包含进程独占内存),但gopsutil不直接支持 - mem, err := p.MemoryInfo() - if err == nil && mem != nil { - // 转换为 MB - memoryUsage := float64(mem.RSS) / 1024 / 1024 - totalMemory += memoryUsage - } - - // 区分工作进程、缓存进程和其他进程 - if ppid == masterPID || strings.Contains(cmdline, "worker process") { - workerCount++ - } else if strings.Contains(cmdline, "cache") { - cacheCount++ - } else { - otherCount++ - } - } - - // 重新计算CPU使用率,更接近top命令的计算方式 - // 首先进行初始CPU时间测量 - times1 := make(map[int32]float64) - for _, p := range nginxProcesses { - times, err := p.Times() - if err == nil { - // CPU时间 = 用户时间 + 系统时间 - times1[p.Pid] = times.User + times.System - } - } - - // 等待一小段时间 - time.Sleep(100 * time.Millisecond) - - // 再次测量CPU时间 - totalCPUPercent := 0.0 - for _, p := range nginxProcesses { - times, err := p.Times() - if err != nil { - continue - } - - // 计算CPU时间差 - currentTotal := times.User + times.System - if previousTotal, ok := times1[p.Pid]; ok { - // 计算这段时间内的CPU使用百分比(考虑多核) - cpuDelta := currentTotal - previousTotal - // 计算每秒CPU使用率(考虑采样时间) - cpuPercent := (cpuDelta / 0.1) * 100.0 / float64(numCPU) - totalCPUPercent += cpuPercent - } - } - - // 四舍五入到整数,更符合top显示方式 - totalCPUPercent = math.Round(totalCPUPercent) - - // 四舍五入内存使用量到两位小数 - totalMemory = math.Round(totalMemory*100) / 100 - - result["workers"] = workerCount - result["master"] = masterCount - result["cache"] = cacheCount - result["other"] = otherCount - result["cpu_usage"] = totalCPUPercent - result["memory_usage"] = totalMemory - - return result, nil -} - -// 获取 Nginx 配置信息 -func getNginxConfigInfo() (map[string]int, error) { - result := map[string]int{ - "worker_processes": 1, - "worker_connections": 1024, - } - - // 获取 worker_processes 配置 - cmd := exec.Command("nginx", "-T") - output, err := cmd.CombinedOutput() - if err != nil { - return result, fmt.Errorf("failed to get nginx config: %v", err) - } - - // 解析 worker_processes - wpRe := regexp.MustCompile(`worker_processes\s+(\d+|auto);`) - if matches := wpRe.FindStringSubmatch(string(output)); len(matches) > 1 { - if matches[1] == "auto" { - result["worker_processes"] = runtime.NumCPU() - } else { - result["worker_processes"], _ = strconv.Atoi(matches[1]) - } - } - - // 解析 worker_connections - wcRe := regexp.MustCompile(`worker_connections\s+(\d+);`) - if matches := wcRe.FindStringSubmatch(string(output)); len(matches) > 1 { - result["worker_connections"], _ = strconv.Atoi(matches[1]) - } - - return result, nil + c.JSON(http.StatusOK, newStubStatus) } diff --git a/app/src/api/ngx.ts b/app/src/api/ngx.ts index 183a2390..76fcc869 100644 --- a/app/src/api/ngx.ts +++ b/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() { diff --git a/app/src/composables/useNginxPerformance.ts b/app/src/composables/useNginxPerformance.ts index 53d5a8ec..dea0490d 100644 --- a/app/src/composables/useNginxPerformance.ts +++ b/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() - const error = ref('') - 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 + 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(null) + const lastUpdateTime = ref(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() } - // Format the last update time - const formattedUpdateTime = computed(() => { - return lastUpdateTime.value.toLocaleTimeString() - }) - - // Get Nginx status data + // Check stub_status availability and get initial data async function fetchInitialData() { - loading.value = true - error.value = '' - 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, } } diff --git a/app/src/lib/http/interceptors.ts b/app/src/lib/http/interceptors.ts index 13152df2..1a2dc911 100644 --- a/app/src/lib/http/interceptors.ts +++ b/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) diff --git a/app/src/views/dashboard/NginxDashBoard.vue b/app/src/views/dashboard/NginxDashBoard.vue index 650e61c4..3999d36e 100644 --- a/app/src/views/dashboard/NginxDashBoard.vue +++ b/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" /> + + +
+
+
+ {{ $gettext('Enable stub_status module') }} +
+
+ {{ $gettext('This module provides Nginx request statistics, connection count, etc. data. After enabling it, you can view performance statistics') }} +
+
+ {{ stubStatusError }} +
+
+ +
+
+ + + +
@@ -118,25 +184,26 @@ onMounted(() => {
+ + + + - - - - + + + + + + + + - - - - - - - - + diff --git a/app/src/views/dashboard/components/ConnectionMetricsCard.vue b/app/src/views/dashboard/components/ConnectionMetricsCard.vue index 7b00c958..fd1bb0f5 100644 --- a/app/src/views/dashboard/components/ConnectionMetricsCard.vue +++ b/app/src/views/dashboard/components/ConnectionMetricsCard.vue @@ -1,6 +1,5 @@
@@ -69,11 +69,16 @@ const maxRPS = computed(() => { + diff --git a/app/src/views/dashboard/components/ProcessDistributionCard.vue b/app/src/views/dashboard/components/ProcessDistributionCard.vue index 78a209d1..f30821db 100644 --- a/app/src/views/dashboard/components/ProcessDistributionCard.vue +++ b/app/src/views/dashboard/components/ProcessDistributionCard.vue @@ -1,6 +1,6 @@ diff --git a/internal/nginx/config_info.go b/internal/nginx/config_info.go new file mode 100644 index 00000000..c331582d --- /dev/null +++ b/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 +} diff --git a/internal/nginx/nginx.go b/internal/nginx/nginx.go index e27d0abf..ea7e4ce3 100644 --- a/internal/nginx/nginx.go +++ b/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 +} diff --git a/internal/nginx/performance.go b/internal/nginx/performance.go new file mode 100644 index 00000000..ebec965d --- /dev/null +++ b/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, + }, + } +} diff --git a/internal/nginx/process_info.go b/internal/nginx/process_info.go new file mode 100644 index 00000000..04aff025 --- /dev/null +++ b/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 +} diff --git a/internal/nginx/stub_status.go b/internal/nginx/stub_status.go new file mode 100644 index 00000000..a7787c42 --- /dev/null +++ b/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) +}