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

This commit is contained in:
Akino 2025-04-10 10:11:26 +00:00
parent 32d7c74835
commit 2d0961f1a3
No known key found for this signature in database
GPG key ID: FB2F74D193A40907
16 changed files with 720 additions and 543 deletions

View file

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

View file

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

View file

@ -4,107 +4,37 @@
package nginx package nginx
import ( import (
"fmt" "errors"
"io"
"math"
"net/http" "net/http"
"os"
"os/exec"
"regexp"
"runtime"
"strconv"
"strings" "strings"
"time" "time"
"github.com/0xJacky/Nginx-UI/internal/nginx" "github.com/0xJacky/Nginx-UI/internal/nginx"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/shirou/gopsutil/v4/process" "github.com/uozi-tech/cosy"
"github.com/uozi-tech/cosy/logger" "github.com/uozi-tech/cosy/logger"
) )
// NginxPerformanceInfo 存储 Nginx 性能相关信息 // NginxPerformanceInfo 存储 Nginx 性能相关信息
type NginxPerformanceInfo struct { type NginxPerformanceInfo struct {
// 基本状态信息 // 基本状态信息
Active int `json:"active"` // 活动连接数 nginx.StubStatusData
Accepts int `json:"accepts"` // 总握手次数
Handled int `json:"handled"` // 总连接次数
Requests int `json:"requests"` // 总请求数
Reading int `json:"reading"` // 读取客户端请求数
Writing int `json:"writing"` // 响应数
Waiting int `json:"waiting"` // 驻留进程(等待请求)
// 进程相关信息 // 进程相关信息
Workers int `json:"workers"` // 工作进程数 nginx.NginxProcessInfo
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
// 配置信息 // 配置信息
WorkerProcesses int `json:"worker_processes"` // worker_processes 配置 nginx.NginxConfigInfo
WorkerConnections int `json:"worker_connections"` // worker_connections 配置
} }
// GetDetailedStatus 获取 Nginx 详细状态信息 // GetDetailStatus 获取 Nginx 详细状态信息
func GetDetailedStatus(c *gin.Context) { func GetDetailStatus(c *gin.Context) {
// 检查 Nginx 是否运行 response := nginx.GetPerformanceData()
pidPath := nginx.GetPIDPath() c.JSON(http.StatusOK, response)
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,
})
} }
// StreamDetailedStatus 使用 SSE 流式推送 Nginx 详细状态信息 // StreamDetailStatus 使用 SSE 流式推送 Nginx 详细状态信息
func StreamDetailedStatus(c *gin.Context) { func StreamDetailStatus(c *gin.Context) {
// 设置 SSE 的响应头 // 设置 SSE 的响应头
c.Header("Content-Type", "text/event-stream") c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache") c.Header("Cache-Control", "no-cache")
@ -140,312 +70,63 @@ func StreamDetailedStatus(c *gin.Context) {
// sendPerformanceData 发送一次性能数据 // sendPerformanceData 发送一次性能数据
func sendPerformanceData(c *gin.Context) error { func sendPerformanceData(c *gin.Context) error {
// 检查 Nginx 是否运行 response := nginx.GetPerformanceData()
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"],
}
// 发送 SSE 事件 // 发送 SSE 事件
c.SSEvent("message", gin.H{ c.SSEvent("message", response)
"running": running,
"info": info,
})
// 刷新缓冲区,确保数据立即发送 // 刷新缓冲区,确保数据立即发送
c.Writer.Flush() c.Writer.Flush()
return nil return nil
} }
// 获取 stub_status 模块数据 // CheckStubStatus 获取 Nginx stub_status 模块状态
func getStubStatusInfo() (map[string]int, error) { func CheckStubStatus(c *gin.Context) {
result := map[string]int{ stubStatus := nginx.GetStubStatus()
"active": 0, "accepts": 0, "handled": 0, "requests": 0,
"reading": 0, "writing": 0, "waiting": 0,
}
// 默认尝试访问 stub_status 页面 c.JSON(http.StatusOK, stubStatus)
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
} }
// 获取 Nginx 进程信息 // ToggleStubStatus 启用或禁用 stub_status 模块
func getNginxProcessInfo() (map[string]interface{}, error) { func ToggleStubStatus(c *gin.Context) {
result := map[string]interface{}{ var json struct {
"workers": 0, Enable bool `json:"enable"`
"master": 0, }
"cache": 0,
"other": 0, if !cosy.BindAndValid(c, &json) {
"cpu_usage": 0.0, return
"memory_usage": 0.0, }
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 { if err != nil {
return result, fmt.Errorf("failed to get processes: %v", err) cosy.ErrHandler(c, err)
return
} }
totalMemory := 0.0 // 重新加载 Nginx 配置
workerCount := 0 reloadOutput := nginx.Reload()
masterCount := 0 if len(reloadOutput) > 0 && (strings.Contains(strings.ToLower(reloadOutput), "error") ||
cacheCount := 0 strings.Contains(strings.ToLower(reloadOutput), "failed")) {
otherCount := 0 cosy.ErrHandler(c, errors.New("Reload Nginx failed"))
nginxProcesses := []*process.Process{} return
// 获取系统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 { newStubStatus := nginx.GetStubStatus()
if p.Pid == masterPID {
continue // 已经计算过主进程
}
name, err := p.Name() c.JSON(http.StatusOK, newStubStatus)
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
} }

View file

@ -70,15 +70,8 @@ const ngx = {
return http.get('/nginx/status') return http.get('/nginx/status')
}, },
detailed_status(): Promise<{ running: boolean, info: NginxPerformanceInfo }> { detail_status(): Promise<{ running: boolean, stub_status_enabled: boolean, info: NginxPerformanceInfo }> {
return http.get('/nginx/detailed_status') return http.get('/nginx/detail_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)
}, },
reload() { reload() {

View file

@ -2,42 +2,75 @@ import type { NginxPerformanceInfo } from '@/api/ngx'
import ngx from '@/api/ngx' import ngx from '@/api/ngx'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
export function useNginxPerformance() { // Time formatting helper function
const loading = ref(true) function formatTimeAgo(date: Date): string {
const nginxInfo = ref<NginxPerformanceInfo>() const now = new Date()
const error = ref<string>('') const diffMs = now.getTime() - date.getTime()
const lastUpdateTime = ref(new Date()) 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<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() { function updateLastUpdateTime() {
lastUpdateTime.value = new Date() lastUpdateTime.value = new Date()
} }
// Format the last update time // Check stub_status availability and get initial data
const formattedUpdateTime = computed(() => {
return lastUpdateTime.value.toLocaleTimeString()
})
// Get Nginx status data
async function fetchInitialData() { async function fetchInitialData() {
loading.value = true
error.value = ''
try { try {
const result = await ngx.detailed_status() loading.value = true
nginxInfo.value = result.info stubStatusLoading.value = true
updateLastUpdateTime() error.value = ''
}
catch (e) { // Get performance data
if (e instanceof Error) { const response = await ngx.detail_status()
error.value = e.message
if (response.running) {
stubStatusEnabled.value = response.stub_status_enabled
nginxInfo.value = response.info
updateLastUpdateTime()
} }
else { 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 { finally {
loading.value = false loading.value = false
stubStatusLoading.value = false
} }
} }
@ -45,9 +78,11 @@ export function useNginxPerformance() {
loading, loading,
nginxInfo, nginxInfo,
error, error,
lastUpdateTime,
formattedUpdateTime, formattedUpdateTime,
updateLastUpdateTime, updateLastUpdateTime,
fetchInitialData, fetchInitialData,
stubStatusEnabled,
stubStatusLoading,
stubStatusError,
} }
} }

View file

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

View file

@ -5,6 +5,7 @@ import { NginxStatus } from '@/constants'
import { useUserStore } from '@/pinia' import { useUserStore } from '@/pinia'
import { useGlobalStore } from '@/pinia/moudule/global' import { useGlobalStore } from '@/pinia/moudule/global'
import { ClockCircleOutlined, ReloadOutlined } from '@ant-design/icons-vue' import { ClockCircleOutlined, ReloadOutlined } from '@ant-design/icons-vue'
import axios from 'axios'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import ConnectionMetricsCard from './components/ConnectionMetricsCard.vue' import ConnectionMetricsCard from './components/ConnectionMetricsCard.vue'
import PerformanceStatisticsCard from './components/PerformanceStatisticsCard.vue' import PerformanceStatisticsCard from './components/PerformanceStatisticsCard.vue'
@ -25,18 +26,50 @@ const {
formattedUpdateTime, formattedUpdateTime,
updateLastUpdateTime, updateLastUpdateTime,
fetchInitialData, fetchInitialData,
stubStatusEnabled,
stubStatusLoading,
stubStatusError,
} = useNginxPerformance() } = useNginxPerformance()
// SSE connection // SSE connection
const { connect, disconnect } = useSSE() 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 // Connect SSE
function connectSSE() { function connectSSE() {
disconnect() disconnect()
loading.value = true loading.value = true
connect({ connect({
url: 'api/nginx/detailed_status/stream', url: 'api/nginx/detail_status/stream',
token: token.value, token: token.value,
onMessage: data => { onMessage: data => {
loading.value = false loading.value = false
@ -48,6 +81,7 @@ function connectSSE() {
else { else {
error.value = data.message || $gettext('Nginx is not running') error.value = data.message || $gettext('Nginx is not running')
} }
stubStatusEnabled.value = data.stub_status_enabled
}, },
onError: () => { onError: () => {
error.value = $gettext('Connection error, trying to reconnect...') 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 // If the connection fails, try to get data using the traditional method
setTimeout(() => { setTimeout(() => {
fetchInitialData() fetchInitialData()
}, 5000) }, 2000)
}, },
}) })
} }
@ -110,6 +144,38 @@ onMounted(() => {
:description="error" :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 --> <!-- Loading state -->
<ASpin :spinning="loading" :tip="$gettext('Loading data...')"> <ASpin :spinning="loading" :tip="$gettext('Loading data...')">
<div v-if="!nginxInfo && !error" class="text-center py-8"> <div v-if="!nginxInfo && !error" class="text-center py-8">
@ -118,25 +184,26 @@ onMounted(() => {
<div v-if="nginxInfo" class="performance-dashboard"> <div v-if="nginxInfo" class="performance-dashboard">
<!-- Top performance metrics card --> <!-- 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"> <ARow :gutter="[16, 16]" class="mb-4">
<ACol :span="24"> <!-- Metrics card -->
<ACard :title="$gettext('Performance Metrics')" :bordered="false"> <ACol :sm="24" :lg="12">
<PerformanceStatisticsCard :nginx-info="nginxInfo" /> <ConnectionMetricsCard :nginx-info="nginxInfo" />
</ACard> </ACol>
<!-- CPU and memory usage -->
<ACol :sm="24" :lg="12">
<ResourceUsageCard :nginx-info="nginxInfo" />
</ACol> </ACol>
</ARow> </ARow>
<!-- Metrics card -->
<ConnectionMetricsCard :nginx-info="nginxInfo" class="mb-4" />
<!-- Resource monitoring --> <!-- Resource monitoring -->
<ARow :gutter="[16, 16]" class="mb-4"> <ARow :gutter="[16, 16]" class="mb-4">
<!-- CPU and memory usage -->
<ACol :xs="24" :md="12">
<ResourceUsageCard :nginx-info="nginxInfo" />
</ACol>
<!-- Process distribution --> <!-- Process distribution -->
<ACol :xs="24" :md="12"> <ACol :span="24">
<ProcessDistributionCard :nginx-info="nginxInfo" /> <ProcessDistributionCard :nginx-info="nginxInfo" />
</ACol> </ACol>
</ARow> </ARow>

View file

@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { NginxPerformanceInfo } from '@/api/ngx' import type { NginxPerformanceInfo } from '@/api/ngx'
import { InfoCircleOutlined } from '@ant-design/icons-vue'
import { computed, defineProps } from 'vue' import { computed, defineProps } from 'vue'
const props = defineProps<{ const props = defineProps<{
@ -17,31 +16,12 @@ const activeConnectionsPercent = computed(() => {
const workerProcessesPercent = computed(() => { const workerProcessesPercent = computed(() => {
return Number(((props.nginxInfo.workers / props.nginxInfo.worker_processes) * 100).toFixed(2)) 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> </script>
<template> <template>
<ARow :gutter="[16, 16]"> <ARow :gutter="[16, 16]" class="h-full">
<!-- Current active connections --> <!-- 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%' }"> <ACard class="h-full" :bordered="false" :body-style="{ padding: '20px', height: '100%' }">
<div class="flex flex-col h-full"> <div class="flex flex-col h-full">
<div class="mb-2 text-gray-500 font-medium truncate"> <div class="mb-2 text-gray-500 font-medium truncate">
@ -62,7 +42,7 @@ function formatNumber(num: number): string {
</ACol> </ACol>
<!-- Worker processes --> <!-- 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%' }"> <ACard class="h-full" :bordered="false" :body-style="{ padding: '20px', height: '100%' }">
<div class="flex flex-col h-full"> <div class="flex flex-col h-full">
<div class="mb-2 text-gray-500 font-medium truncate"> <div class="mb-2 text-gray-500 font-medium truncate">
@ -77,61 +57,6 @@ function formatNumber(num: number): string {
size="small" size="small"
status="active" 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> </div>
</ACard> </ACard>
</ACol> </ACol>

View file

@ -40,9 +40,9 @@ const maxRPS = computed(() => {
</template> </template>
<template #title> <template #title>
{{ $gettext('Max Requests Per Second') }} {{ $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" /> <InfoCircleOutlined class="ml-1 text-gray-500" />
</Tooltip> </ATooltip>
</template> </template>
</AStatistic> </AStatistic>
<div class="text-xs text-gray-500 mt-1"> <div class="text-xs text-gray-500 mt-1">
@ -69,11 +69,16 @@ const maxRPS = computed(() => {
<!-- Requests per connection --> <!-- Requests per connection -->
<ACol :xs="24" :sm="12" :md="8" :lg="6"> <ACol :xs="24" :sm="12" :md="8" :lg="6">
<AStatistic <AStatistic
:title="$gettext('Requests Per Connection')"
:value="requestsPerConnection" :value="requestsPerConnection"
:precision="2" :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> <template #prefix>
<DashboardOutlined /> <DashboardOutlined />
</template> </template>

View file

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { NginxPerformanceInfo } from '@/api/ngx' import type { NginxPerformanceInfo } from '@/api/ngx'
import { computed, defineProps } from 'vue' import { InfoCircleOutlined } from '@ant-design/icons-vue'
const props = defineProps<{ const props = defineProps<{
nginxInfo: NginxPerformanceInfo nginxInfo: NginxPerformanceInfo
@ -48,6 +48,13 @@ const totalProcesses = computed(() => {
{{ $gettext('Actual worker to configured ratio') }}: {{ $gettext('Actual worker to configured ratio') }}:
<span class="font-medium">{{ nginxInfo.workers }} / {{ nginxInfo.worker_processes }}</span> <span class="font-medium">{{ nginxInfo.workers }} / {{ nginxInfo.worker_processes }}</span>
</div> </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> </div>
</ACard> </ACard>
</template> </template>

View file

@ -5,37 +5,30 @@ import {
InfoCircleOutlined, InfoCircleOutlined,
ThunderboltOutlined, ThunderboltOutlined,
} from '@ant-design/icons-vue' } from '@ant-design/icons-vue'
import { computed, defineProps } from 'vue'
const props = defineProps<{ const props = defineProps<{
nginxInfo: NginxPerformanceInfo nginxInfo: NginxPerformanceInfo
}>() }>()
// Resource utilization const cpuUsage = computed(() => {
const resourceUtilization = computed(() => { return Number(Math.min(props.nginxInfo.cpu_usage, 100).toFixed(2))
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)
}) })
</script> </script>
<template> <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"> <div class="flex flex-col h-full">
<!-- CPU usage --> <!-- CPU usage -->
<ARow :gutter="[16, 8]" class="mb-2"> <ARow :gutter="[16, 8]">
<ACol :span="24"> <ACol :span="24">
<div class="flex items-center"> <div class="flex items-center">
<ThunderboltOutlined class="text-lg mr-2" :style="{ color: nginxInfo.cpu_usage > 80 ? '#cf1322' : '#3f8600' }" /> <ThunderboltOutlined class="text-lg mr-2" :style="{ color: nginxInfo.cpu_usage > 80 ? '#cf1322' : '#3f8600' }" />
<div class="text-base font-medium"> <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>
</div> </div>
<AProgress <AProgress
:percent="Math.min(nginxInfo.cpu_usage, 100)" :percent="cpuUsage"
:format="percent => `${percent?.toFixed(2)}%`"
:status="nginxInfo.cpu_usage > 80 ? 'exception' : 'active'" :status="nginxInfo.cpu_usage > 80 ? 'exception' : 'active'"
size="small" size="small"
class="mt-1" class="mt-1"
@ -48,7 +41,7 @@ const resourceUtilization = computed(() => {
</ARow> </ARow>
<!-- Memory usage --> <!-- Memory usage -->
<ARow :gutter="[16, 8]" class="mb-2"> <ARow :gutter="[16, 8]" class="mt-2">
<ACol :span="24"> <ACol :span="24">
<div class="flex items-center"> <div class="flex items-center">
<div class="text-blue-500 text-lg mr-2 flex items-center"> <div class="text-blue-500 text-lg mr-2 flex items-center">
@ -63,25 +56,6 @@ const resourceUtilization = computed(() => {
</div> </div>
</ACol> </ACol>
</ARow> </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> </div>
</ACard> </ACard>
</template> </template>

View file

@ -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
}

View file

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

View file

@ -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,
},
}
}

View file

@ -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
}

View file

@ -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)
}