mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2025-05-10 18:05:48 +02:00
feat: add nginx detail status
This commit is contained in:
parent
241fa4adfe
commit
4e8346f04e
17 changed files with 1607 additions and 39 deletions
|
@ -13,6 +13,10 @@ 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/detailed_status", GetDetailedStatus)
|
||||
// 使用SSE推送Nginx详细状态信息
|
||||
r.GET("nginx/detailed_status/stream", StreamDetailedStatus)
|
||||
r.POST("nginx_log", nginx_log.GetNginxLogPage)
|
||||
r.GET("nginx/directives", GetDirectives)
|
||||
}
|
||||
|
|
452
api/nginx/status.go
Normal file
452
api/nginx/status.go
Normal file
|
@ -0,0 +1,452 @@
|
|||
// GetDetailedStatus API 实现
|
||||
// 该功能用于解决 Issue #850,提供类似宝塔面板的 Nginx 负载监控功能
|
||||
// 返回详细的 Nginx 状态信息,包括请求统计、连接数、工作进程等数据
|
||||
package nginx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"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/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"` // 驻留进程(等待请求)
|
||||
|
||||
// 进程相关信息
|
||||
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)
|
||||
|
||||
// 配置信息
|
||||
WorkerProcesses int `json:"worker_processes"` // worker_processes 配置
|
||||
WorkerConnections int `json:"worker_connections"` // worker_connections 配置
|
||||
}
|
||||
|
||||
// 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,
|
||||
})
|
||||
}
|
||||
|
||||
// StreamDetailedStatus 使用 SSE 流式推送 Nginx 详细状态信息
|
||||
func StreamDetailedStatus(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 {
|
||||
// 检查 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"],
|
||||
}
|
||||
|
||||
// 发送 SSE 事件
|
||||
c.SSEvent("message", gin.H{
|
||||
"running": running,
|
||||
"info": info,
|
||||
})
|
||||
|
||||
// 刷新缓冲区,确保数据立即发送
|
||||
c.Writer.Flush()
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取 stub_status 模块数据
|
||||
func getStubStatusInfo() (map[string]int, error) {
|
||||
result := map[string]int{
|
||||
"active": 0, "accepts": 0, "handled": 0, "requests": 0,
|
||||
"reading": 0, "writing": 0, "waiting": 0,
|
||||
}
|
||||
|
||||
// 默认尝试访问 stub_status 页面
|
||||
statusURL := "http://localhost/stub_status"
|
||||
|
||||
// 创建 HTTP 客户端
|
||||
client := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
// 发送请求获取 stub_status 数据
|
||||
resp, err := client.Get(statusURL)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("failed to get stub status: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 读取响应内容
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("failed to read response body: %v", err)
|
||||
}
|
||||
|
||||
// 解析响应内容
|
||||
statusContent := string(body)
|
||||
|
||||
// 匹配活动连接数
|
||||
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 进程信息
|
||||
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,
|
||||
}
|
||||
|
||||
// 查找所有 Nginx 进程
|
||||
processes, err := process.Processes()
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("failed to get processes: %v", err)
|
||||
}
|
||||
|
||||
totalMemory := 0.0
|
||||
workerCount := 0
|
||||
masterCount := 0
|
||||
cacheCount := 0
|
||||
otherCount := 0
|
||||
nginxProcesses := []*process.Process{}
|
||||
|
||||
// 获取系统CPU核心数
|
||||
numCPU := runtime.NumCPU()
|
||||
|
||||
// 获取Nginx主进程的PID
|
||||
var masterPID int32 = -1
|
||||
for _, p := range processes {
|
||||
name, err := p.Name()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
cmdline, err := p.Cmdline()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查是否是Nginx主进程
|
||||
if strings.Contains(strings.ToLower(name), "nginx") &&
|
||||
(strings.Contains(cmdline, "master process") ||
|
||||
!strings.Contains(cmdline, "worker process")) &&
|
||||
p.Pid > 0 {
|
||||
masterPID = p.Pid
|
||||
masterCount++
|
||||
nginxProcesses = append(nginxProcesses, p)
|
||||
|
||||
// 获取内存使用情况 - 使用RSS代替
|
||||
// 注意:理想情况下我们应该使用USS(仅包含进程独占内存),但gopsutil不直接支持
|
||||
mem, err := p.MemoryInfo()
|
||||
if err == nil && mem != nil {
|
||||
// 转换为 MB
|
||||
memoryUsage := float64(mem.RSS) / 1024 / 1024
|
||||
totalMemory += memoryUsage
|
||||
logger.Debug("Master进程内存使用(MB):", memoryUsage)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 遍历所有进程,区分工作进程和其他Nginx进程
|
||||
for _, p := range processes {
|
||||
if p.Pid == masterPID {
|
||||
continue // 已经计算过主进程
|
||||
}
|
||||
|
||||
name, err := p.Name()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 只处理Nginx相关进程
|
||||
if !strings.Contains(strings.ToLower(name), "nginx") {
|
||||
continue
|
||||
}
|
||||
|
||||
// 添加到Nginx进程列表
|
||||
nginxProcesses = append(nginxProcesses, p)
|
||||
|
||||
// 获取父进程PID
|
||||
ppid, err := p.Ppid()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
cmdline, err := p.Cmdline()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 获取内存使用情况 - 使用RSS代替
|
||||
// 注意:理想情况下我们应该使用USS(仅包含进程独占内存),但gopsutil不直接支持
|
||||
mem, err := p.MemoryInfo()
|
||||
if err == nil && mem != nil {
|
||||
// 转换为 MB
|
||||
memoryUsage := float64(mem.RSS) / 1024 / 1024
|
||||
totalMemory += memoryUsage
|
||||
}
|
||||
|
||||
// 区分工作进程、缓存进程和其他进程
|
||||
if ppid == masterPID || strings.Contains(cmdline, "worker process") {
|
||||
workerCount++
|
||||
} else if strings.Contains(cmdline, "cache") {
|
||||
cacheCount++
|
||||
} else {
|
||||
otherCount++
|
||||
}
|
||||
}
|
||||
|
||||
// 重新计算CPU使用率,更接近top命令的计算方式
|
||||
// 首先进行初始CPU时间测量
|
||||
times1 := make(map[int32]float64)
|
||||
for _, p := range nginxProcesses {
|
||||
times, err := p.Times()
|
||||
if err == nil {
|
||||
// CPU时间 = 用户时间 + 系统时间
|
||||
times1[p.Pid] = times.User + times.System
|
||||
}
|
||||
}
|
||||
|
||||
// 等待一小段时间
|
||||
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
|
||||
}
|
|
@ -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,17 @@ 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)
|
||||
},
|
||||
|
||||
reload() {
|
||||
return http.post('/nginx/reload')
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
53
app/src/composables/useNginxPerformance.ts
Normal file
53
app/src/composables/useNginxPerformance.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import type { NginxPerformanceInfo } from '@/api/ngx'
|
||||
import ngx from '@/api/ngx'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
export function useNginxPerformance() {
|
||||
const loading = ref(true)
|
||||
const nginxInfo = ref<NginxPerformanceInfo>()
|
||||
const error = ref<string>('')
|
||||
const lastUpdateTime = ref(new Date())
|
||||
|
||||
// 更新刷新时间
|
||||
function updateLastUpdateTime() {
|
||||
lastUpdateTime.value = new Date()
|
||||
}
|
||||
|
||||
// 格式化上次更新时间
|
||||
const formattedUpdateTime = computed(() => {
|
||||
return lastUpdateTime.value.toLocaleTimeString()
|
||||
})
|
||||
|
||||
// 获取Nginx状态数据
|
||||
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
|
||||
}
|
||||
else {
|
||||
error.value = $gettext('Get data failed')
|
||||
}
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
nginxInfo,
|
||||
error,
|
||||
lastUpdateTime,
|
||||
formattedUpdateTime,
|
||||
updateLastUpdateTime,
|
||||
fetchInitialData,
|
||||
}
|
||||
}
|
194
app/src/composables/usePerformanceMetrics.ts
Normal file
194
app/src/composables/usePerformanceMetrics.ts
Normal file
|
@ -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>) {
|
||||
// 格式化数值为可读性更好的形式
|
||||
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()
|
||||
}
|
||||
|
||||
// 活跃连接百分比
|
||||
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))
|
||||
})
|
||||
|
||||
// 工作进程使用百分比
|
||||
const workerProcessesPercent = computed(() => {
|
||||
if (!nginxInfo.value) {
|
||||
return 0
|
||||
}
|
||||
return Number(((nginxInfo.value.workers / nginxInfo.value.worker_processes) * 100).toFixed(2))
|
||||
})
|
||||
|
||||
// 每连接请求数
|
||||
const requestsPerConnection = computed(() => {
|
||||
if (!nginxInfo.value || nginxInfo.value.handled === 0) {
|
||||
return 0
|
||||
}
|
||||
return (nginxInfo.value.requests / nginxInfo.value.handled).toFixed(2)
|
||||
})
|
||||
|
||||
// 最大每秒请求数
|
||||
const maxRPS = computed(() => {
|
||||
if (!nginxInfo.value) {
|
||||
return 0
|
||||
}
|
||||
return nginxInfo.value.worker_processes * nginxInfo.value.worker_connections
|
||||
})
|
||||
|
||||
// 进程构成数据
|
||||
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' },
|
||||
]
|
||||
})
|
||||
|
||||
// 资源利用率
|
||||
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)
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
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,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
// 工作进程数据
|
||||
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`,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
// 配置数据
|
||||
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
app/src/composables/useSSE.ts
Normal file
91
app/src/composables/useSSE.ts
Normal file
|
@ -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
|
||||
* 提供创建、管理和自动清理 SSE 连接的能力
|
||||
*/
|
||||
export function useSSE() {
|
||||
const sseInstance = shallowRef<SSE>()
|
||||
|
||||
/**
|
||||
* 连接 SSE 服务
|
||||
*/
|
||||
function connect(options: SSEOptions) {
|
||||
disconnect()
|
||||
|
||||
const {
|
||||
url,
|
||||
token,
|
||||
onMessage,
|
||||
onError,
|
||||
parseData = true,
|
||||
reconnectInterval = 5000,
|
||||
} = options
|
||||
|
||||
const sse = new SSE(url, {
|
||||
headers: {
|
||||
Authorization: token,
|
||||
},
|
||||
})
|
||||
|
||||
// 处理消息
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理错误并重连
|
||||
sse.onerror = () => {
|
||||
onError?.()
|
||||
|
||||
// 重连逻辑
|
||||
setTimeout(() => {
|
||||
connect(options)
|
||||
}, reconnectInterval)
|
||||
}
|
||||
|
||||
sseInstance.value = sse
|
||||
return sse
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开 SSE 连接
|
||||
*/
|
||||
function disconnect() {
|
||||
if (sseInstance.value) {
|
||||
sseInstance.value.close()
|
||||
sseInstance.value = undefined
|
||||
}
|
||||
}
|
||||
|
||||
// 组件卸载时自动断开连接
|
||||
onUnmounted(() => {
|
||||
disconnect()
|
||||
})
|
||||
|
||||
return {
|
||||
connect,
|
||||
disconnect,
|
||||
sseInstance,
|
||||
}
|
||||
}
|
|
@ -121,7 +121,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 +135,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)
|
||||
|
|
|
@ -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 @@
|
|||
{"version":"2.0.0-rc.5","build_id":12,"total_build":406}
|
||||
{"version":"2.0.0-rc.5","build_id":13,"total_build":407}
|
153
app/src/views/dashboard/NginxDashBoard.vue
Normal file
153
app/src/views/dashboard/NginxDashBoard.vue
Normal file
|
@ -0,0 +1,153 @@
|
|||
<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 { 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'
|
||||
|
||||
// 全局状态
|
||||
const global = useGlobalStore()
|
||||
const { nginxStatus: status } = storeToRefs(global)
|
||||
const { token } = storeToRefs(useUserStore())
|
||||
|
||||
// 使用性能数据composable
|
||||
const {
|
||||
loading,
|
||||
nginxInfo,
|
||||
error,
|
||||
formattedUpdateTime,
|
||||
updateLastUpdateTime,
|
||||
fetchInitialData,
|
||||
} = useNginxPerformance()
|
||||
|
||||
// SSE 连接
|
||||
const { connect, disconnect } = useSSE()
|
||||
|
||||
// 连接SSE
|
||||
function connectSSE() {
|
||||
disconnect()
|
||||
loading.value = true
|
||||
|
||||
connect({
|
||||
url: 'api/nginx/detailed_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')
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
error.value = $gettext('Connection error, trying to reconnect...')
|
||||
|
||||
// 如果连接失败,尝试使用传统方式获取数据
|
||||
setTimeout(() => {
|
||||
fetchInitialData()
|
||||
}, 5000)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 手动刷新数据
|
||||
function refreshData() {
|
||||
fetchInitialData().then(connectSSE)
|
||||
}
|
||||
|
||||
// 组件挂载时初始化连接
|
||||
onMounted(() => {
|
||||
fetchInitialData().then(connectSSE)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- 顶部操作栏 -->
|
||||
<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 状态提示 -->
|
||||
<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')"
|
||||
/>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<AAlert
|
||||
v-if="error"
|
||||
class="mb-4"
|
||||
type="error"
|
||||
show-icon
|
||||
:message="$gettext('Get data failed')"
|
||||
:description="error"
|
||||
/>
|
||||
|
||||
<!-- 加载中状态 -->
|
||||
<ASpin :spinning="loading" :tip="$gettext('Loading data...')">
|
||||
<div v-if="!nginxInfo && !loading && !error" class="text-center py-8">
|
||||
<AEmpty :description="$gettext('No data')" />
|
||||
</div>
|
||||
|
||||
<div v-if="nginxInfo" class="performance-dashboard">
|
||||
<!-- 顶部性能指标卡片 -->
|
||||
<ARow :gutter="[16, 16]" class="mb-4">
|
||||
<ACol :span="24">
|
||||
<ACard :title="$gettext('Performance Metrics')" :bordered="false">
|
||||
<PerformanceStatisticsCard :nginx-info="nginxInfo" />
|
||||
</ACard>
|
||||
</ACol>
|
||||
</ARow>
|
||||
|
||||
<!-- 指标卡片 -->
|
||||
<ConnectionMetricsCard :nginx-info="nginxInfo" class="mb-4" />
|
||||
|
||||
<!-- 资源监控 -->
|
||||
<ARow :gutter="[16, 16]" class="mb-4">
|
||||
<!-- CPU和内存使用 -->
|
||||
<ACol :xs="24" :md="12">
|
||||
<ResourceUsageCard :nginx-info="nginxInfo" />
|
||||
</ACol>
|
||||
<!-- 进程分布 -->
|
||||
<ACol :xs="24" :md="12">
|
||||
<ProcessDistributionCard :nginx-info="nginxInfo" />
|
||||
</ACol>
|
||||
</ARow>
|
||||
|
||||
<!-- 性能指标表格 -->
|
||||
<ARow :gutter="[16, 16]" class="mb-4">
|
||||
<ACol :span="24">
|
||||
<PerformanceTablesCard :nginx-info="nginxInfo" />
|
||||
</ACol>
|
||||
</ARow>
|
||||
</div>
|
||||
</ASpin>
|
||||
</div>
|
||||
</template>
|
139
app/src/views/dashboard/components/ConnectionMetricsCard.vue
Normal file
139
app/src/views/dashboard/components/ConnectionMetricsCard.vue
Normal file
|
@ -0,0 +1,139 @@
|
|||
<script setup lang="ts">
|
||||
import type { NginxPerformanceInfo } from '@/api/ngx'
|
||||
import { InfoCircleOutlined } from '@ant-design/icons-vue'
|
||||
import { computed, defineProps } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
nginxInfo: NginxPerformanceInfo
|
||||
}>()
|
||||
|
||||
// 活跃连接百分比
|
||||
const activeConnectionsPercent = computed(() => {
|
||||
const maxConnections = props.nginxInfo.worker_connections * props.nginxInfo.worker_processes
|
||||
return Number(((props.nginxInfo.active / maxConnections) * 100).toFixed(2))
|
||||
})
|
||||
|
||||
// 工作进程使用百分比
|
||||
const workerProcessesPercent = computed(() => {
|
||||
return Number(((props.nginxInfo.workers / props.nginxInfo.worker_processes) * 100).toFixed(2))
|
||||
})
|
||||
|
||||
// 每连接请求数
|
||||
const requestsPerConnection = computed(() => {
|
||||
if (props.nginxInfo.handled === 0) {
|
||||
return '0'
|
||||
}
|
||||
return (props.nginxInfo.requests / props.nginxInfo.handled).toFixed(2)
|
||||
})
|
||||
|
||||
// 格式化数值
|
||||
function formatNumber(num: number): string {
|
||||
if (num >= 1000000) {
|
||||
return `${(num / 1000000).toFixed(2)}M`
|
||||
}
|
||||
else if (num >= 1000) {
|
||||
return `${(num / 1000).toFixed(2)}K`
|
||||
}
|
||||
return num.toString()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ARow :gutter="[16, 16]">
|
||||
<!-- 当前活跃连接 -->
|
||||
<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">
|
||||
<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>
|
||||
|
||||
<!-- 工作进程 -->
|
||||
<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">
|
||||
<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 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>
|
||||
|
||||
<!-- 每连接请求数 -->
|
||||
<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>
|
||||
|
||||
<!-- 资源利用率 -->
|
||||
<ACol :xs="24" :sm="12" :md="12" :lg="6">
|
||||
<ACard class="h-full" :bordered="false" :body-style="{ padding: '20px', height: '100%' }">
|
||||
<div class="flex flex-col h-full justify-between">
|
||||
<div class="mb-2 text-gray-500 font-medium truncate">
|
||||
{{ $gettext('Resource Utilization') }}
|
||||
</div>
|
||||
<div class="flex items-center justify-center flex-grow">
|
||||
<AProgress
|
||||
type="dashboard"
|
||||
:percent="Math.round((Math.min(nginxInfo.cpu_usage / 100, 1) * 0.5 + Math.min(nginxInfo.active / (nginxInfo.worker_connections * nginxInfo.worker_processes), 1) * 0.5) * 100)"
|
||||
:width="80"
|
||||
status="active"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-gray-500 text-center overflow-hidden text-ellipsis">
|
||||
{{ $gettext('Based on CPU usage and connection usage') }}
|
||||
</div>
|
||||
</div>
|
||||
</ACard>
|
||||
</ACol>
|
||||
</ARow>
|
||||
</template>
|
102
app/src/views/dashboard/components/PerformanceStatisticsCard.vue
Normal file
102
app/src/views/dashboard/components/PerformanceStatisticsCard.vue
Normal file
|
@ -0,0 +1,102 @@
|
|||
<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
|
||||
}>()
|
||||
|
||||
// 计算连接效率 - 每连接的请求数
|
||||
const requestsPerConnection = computed(() => {
|
||||
if (props.nginxInfo.handled === 0) {
|
||||
return '0'
|
||||
}
|
||||
return (props.nginxInfo.requests / props.nginxInfo.handled).toFixed(2)
|
||||
})
|
||||
|
||||
// 估算最大每秒请求数
|
||||
const maxRPS = computed(() => {
|
||||
return props.nginxInfo.worker_processes * props.nginxInfo.worker_connections
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ARow :gutter="[16, 24]">
|
||||
<!-- 最大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') }}
|
||||
<Tooltip :title="$gettext('Calculated based on worker_processes * worker_connections. Actual performance depends on hardware, configuration, and workload')">
|
||||
<InfoCircleOutlined class="ml-1 text-gray-500" />
|
||||
</Tooltip>
|
||||
</template>
|
||||
</AStatistic>
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
worker_processes ({{ nginxInfo.worker_processes }}) × worker_connections ({{ nginxInfo.worker_connections }})
|
||||
</div>
|
||||
</ACol>
|
||||
|
||||
<!-- 最大并发连接 -->
|
||||
<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>
|
||||
|
||||
<!-- 每连接请求数 -->
|
||||
<ACol :xs="24" :sm="12" :md="8" :lg="6">
|
||||
<AStatistic
|
||||
:title="$gettext('Requests Per Connection')"
|
||||
:value="requestsPerConnection"
|
||||
:precision="2"
|
||||
:value-style="{ color: '#f5222d', fontSize: '24px' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<DashboardOutlined />
|
||||
</template>
|
||||
</AStatistic>
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
{{ $gettext('Higher value means better connection reuse') }}
|
||||
</div>
|
||||
</ACol>
|
||||
|
||||
<!-- Nginx进程总数 -->
|
||||
<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
app/src/views/dashboard/components/PerformanceTablesCard.vue
Normal file
211
app/src/views/dashboard/components/PerformanceTablesCard.vue
Normal file
|
@ -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')
|
||||
|
||||
// 表格列定义
|
||||
const columns: TableColumnType[] = [
|
||||
{
|
||||
title: $gettext('Indicator'),
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: '30%',
|
||||
},
|
||||
{
|
||||
title: $gettext('Value'),
|
||||
dataIndex: 'value',
|
||||
key: 'value',
|
||||
},
|
||||
]
|
||||
|
||||
// 格式化数值
|
||||
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()
|
||||
}
|
||||
|
||||
// 状态数据
|
||||
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,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
// 工作进程数据
|
||||
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`,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
// 配置数据
|
||||
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,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
// 最大每秒请求数
|
||||
const maxRPS = computed(() => {
|
||||
return props.nginxInfo.worker_processes * props.nginxInfo.worker_connections
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ACard :bordered="false">
|
||||
<ATabs v-model:active-key="activeTabKey">
|
||||
<!-- 请求统计 -->
|
||||
<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>
|
||||
|
||||
<!-- 进程信息 -->
|
||||
<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>
|
||||
|
||||
<!-- 配置信息 -->
|
||||
<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>
|
|
@ -0,0 +1,53 @@
|
|||
<script setup lang="ts">
|
||||
import type { NginxPerformanceInfo } from '@/api/ngx'
|
||||
import { computed, defineProps } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
nginxInfo: NginxPerformanceInfo
|
||||
}>()
|
||||
|
||||
// 进程构成数据
|
||||
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' },
|
||||
]
|
||||
})
|
||||
|
||||
// 总进程数
|
||||
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>
|
||||
</ACard>
|
||||
</template>
|
87
app/src/views/dashboard/components/ResourceUsageCard.vue
Normal file
87
app/src/views/dashboard/components/ResourceUsageCard.vue
Normal file
|
@ -0,0 +1,87 @@
|
|||
<script setup lang="ts">
|
||||
import type { NginxPerformanceInfo } from '@/api/ngx'
|
||||
import {
|
||||
FundProjectionScreenOutlined,
|
||||
InfoCircleOutlined,
|
||||
ThunderboltOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { computed, defineProps } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
nginxInfo: NginxPerformanceInfo
|
||||
}>()
|
||||
|
||||
// 资源利用率
|
||||
const resourceUtilization = computed(() => {
|
||||
const cpuFactor = Math.min(props.nginxInfo.cpu_usage / 100, 1)
|
||||
const maxConnections = props.nginxInfo.worker_connections * props.nginxInfo.worker_processes
|
||||
const connectionFactor = Math.min(props.nginxInfo.active / maxConnections, 1)
|
||||
|
||||
return Math.round((cpuFactor * 0.5 + connectionFactor * 0.5) * 100)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ACard :title="$gettext('Resource Usage of Nginx')" :bordered="false" class="h-full" :body-style="{ padding: '16px', height: 'calc(100% - 58px)' }">
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- CPU使用率 -->
|
||||
<ARow :gutter="[16, 8]" class="mb-2">
|
||||
<ACol :span="24">
|
||||
<div class="flex items-center">
|
||||
<ThunderboltOutlined class="text-lg mr-2" :style="{ color: nginxInfo.cpu_usage > 80 ? '#cf1322' : '#3f8600' }" />
|
||||
<div class="text-base font-medium">
|
||||
{{ $gettext('CPU Usage') }}: <span :style="{ color: nginxInfo.cpu_usage > 80 ? '#cf1322' : '#3f8600' }">{{ nginxInfo.cpu_usage.toFixed(2) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<AProgress
|
||||
:percent="Math.min(nginxInfo.cpu_usage, 100)"
|
||||
:format="percent => `${percent?.toFixed(2)}%`"
|
||||
: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>
|
||||
|
||||
<!-- 内存使用 -->
|
||||
<ARow :gutter="[16, 8]" class="mb-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 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>
|
||||
|
||||
<!-- 系统负载 -->
|
||||
<div class="mt-4 text-xs text-gray-500 border-t border-gray-100 pt-2">
|
||||
<div class="flex justify-between mb-1">
|
||||
<span>{{ $gettext('System load') }}</span>
|
||||
<span class="font-medium">{{ resourceUtilization }}%</span>
|
||||
</div>
|
||||
<AProgress
|
||||
:percent="resourceUtilization"
|
||||
size="small"
|
||||
:status="resourceUtilization > 80 ? 'exception' : 'active'"
|
||||
:stroke-color="resourceUtilization > 80 ? '#ff4d4f' : resourceUtilization > 50 ? '#faad14' : '#52c41a'"
|
||||
:show-info="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ACard>
|
||||
</template>
|
Loading…
Add table
Add a link
Reference in a new issue