mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2025-05-11 02:15:48 +02:00
Merge pull request #952 from akinoccc/dev
feat: nginx performance dashboard
This commit is contained in:
commit
8fbb0d9fa7
23 changed files with 1791 additions and 47 deletions
|
@ -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,
|
||||||
|
|
|
@ -13,6 +13,14 @@ func InitRouter(r *gin.RouterGroup) {
|
||||||
r.POST("nginx/restart", Restart)
|
r.POST("nginx/restart", Restart)
|
||||||
r.POST("nginx/test", Test)
|
r.POST("nginx/test", Test)
|
||||||
r.GET("nginx/status", Status)
|
r.GET("nginx/status", Status)
|
||||||
|
// 获取 Nginx 详细状态信息,包括连接数、进程信息等(Issue #850)
|
||||||
|
r.GET("nginx/detail_status", GetDetailStatus)
|
||||||
|
// 使用SSE推送Nginx详细状态信息
|
||||||
|
r.GET("nginx/detail_status/stream", StreamDetailStatus)
|
||||||
|
// 获取 stub_status 模块状态
|
||||||
|
r.GET("nginx/stub_status", CheckStubStatus)
|
||||||
|
// 启用或禁用 stub_status 模块
|
||||||
|
r.POST("nginx/stub_status", ToggleStubStatus)
|
||||||
r.POST("nginx_log", nginx_log.GetNginxLogPage)
|
r.POST("nginx_log", nginx_log.GetNginxLogPage)
|
||||||
r.GET("nginx/directives", GetDirectives)
|
r.GET("nginx/directives", GetDirectives)
|
||||||
}
|
}
|
||||||
|
|
132
api/nginx/status.go
Normal file
132
api/nginx/status.go
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
// GetDetailedStatus API 实现
|
||||||
|
// 该功能用于解决 Issue #850,提供类似宝塔面板的 Nginx 负载监控功能
|
||||||
|
// 返回详细的 Nginx 状态信息,包括请求统计、连接数、工作进程等数据
|
||||||
|
package nginx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/0xJacky/Nginx-UI/internal/nginx"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/uozi-tech/cosy"
|
||||||
|
"github.com/uozi-tech/cosy/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NginxPerformanceInfo 存储 Nginx 性能相关信息
|
||||||
|
type NginxPerformanceInfo struct {
|
||||||
|
// 基本状态信息
|
||||||
|
nginx.StubStatusData
|
||||||
|
|
||||||
|
// 进程相关信息
|
||||||
|
nginx.NginxProcessInfo
|
||||||
|
|
||||||
|
// 配置信息
|
||||||
|
nginx.NginxConfigInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDetailStatus 获取 Nginx 详细状态信息
|
||||||
|
func GetDetailStatus(c *gin.Context) {
|
||||||
|
response := nginx.GetPerformanceData()
|
||||||
|
c.JSON(http.StatusOK, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StreamDetailStatus 使用 SSE 流式推送 Nginx 详细状态信息
|
||||||
|
func StreamDetailStatus(c *gin.Context) {
|
||||||
|
// 设置 SSE 的响应头
|
||||||
|
c.Header("Content-Type", "text/event-stream")
|
||||||
|
c.Header("Cache-Control", "no-cache")
|
||||||
|
c.Header("Connection", "keep-alive")
|
||||||
|
c.Header("Access-Control-Allow-Origin", "*")
|
||||||
|
|
||||||
|
// 创建上下文,当客户端断开连接时取消
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
// 为防止 goroutine 泄漏,创建一个计时器通道
|
||||||
|
ticker := time.NewTicker(5 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
// 立即发送一次初始数据
|
||||||
|
sendPerformanceData(c)
|
||||||
|
|
||||||
|
// 使用 goroutine 定期发送数据
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
// 发送性能数据
|
||||||
|
if err := sendPerformanceData(c); err != nil {
|
||||||
|
logger.Warn("Error sending SSE data:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
// 客户端断开连接或请求被取消
|
||||||
|
logger.Debug("Client closed connection")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendPerformanceData 发送一次性能数据
|
||||||
|
func sendPerformanceData(c *gin.Context) error {
|
||||||
|
response := nginx.GetPerformanceData()
|
||||||
|
|
||||||
|
// 发送 SSE 事件
|
||||||
|
c.SSEvent("message", response)
|
||||||
|
|
||||||
|
// 刷新缓冲区,确保数据立即发送
|
||||||
|
c.Writer.Flush()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckStubStatus 获取 Nginx stub_status 模块状态
|
||||||
|
func CheckStubStatus(c *gin.Context) {
|
||||||
|
stubStatus := nginx.GetStubStatus()
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, stubStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToggleStubStatus 启用或禁用 stub_status 模块
|
||||||
|
func ToggleStubStatus(c *gin.Context) {
|
||||||
|
var json struct {
|
||||||
|
Enable bool `json:"enable"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cosy.BindAndValid(c, &json) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stubStatus := nginx.GetStubStatus()
|
||||||
|
|
||||||
|
// 如果当前状态与期望状态相同,则无需操作
|
||||||
|
if stubStatus.Enabled == json.Enable {
|
||||||
|
c.JSON(http.StatusOK, stubStatus)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if json.Enable {
|
||||||
|
err = nginx.EnableStubStatus()
|
||||||
|
} else {
|
||||||
|
err = nginx.DisableStubStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
cosy.ErrHandler(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新加载 Nginx 配置
|
||||||
|
reloadOutput := nginx.Reload()
|
||||||
|
if len(reloadOutput) > 0 && (strings.Contains(strings.ToLower(reloadOutput), "error") ||
|
||||||
|
strings.Contains(strings.ToLower(reloadOutput), "failed")) {
|
||||||
|
cosy.ErrHandler(c, errors.New("Reload Nginx failed"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查操作后的状态
|
||||||
|
newStubStatus := nginx.GetStubStatus()
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, newStubStatus)
|
||||||
|
}
|
|
@ -35,6 +35,24 @@ export interface NgxLocation {
|
||||||
|
|
||||||
export type DirectiveMap = Record<string, { links: string[] }>
|
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 = {
|
const ngx = {
|
||||||
build_config(ngxConfig: NgxConfig) {
|
build_config(ngxConfig: NgxConfig) {
|
||||||
return http.post('/ngx/build_config', ngxConfig)
|
return http.post('/ngx/build_config', ngxConfig)
|
||||||
|
@ -52,6 +70,10 @@ const ngx = {
|
||||||
return http.get('/nginx/status')
|
return http.get('/nginx/status')
|
||||||
},
|
},
|
||||||
|
|
||||||
|
detail_status(): Promise<{ running: boolean, stub_status_enabled: boolean, info: NginxPerformanceInfo }> {
|
||||||
|
return http.get('/nginx/detail_status')
|
||||||
|
},
|
||||||
|
|
||||||
reload() {
|
reload() {
|
||||||
return http.post('/nginx/reload')
|
return http.post('/nginx/reload')
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
import type { EnvGroup } from '@/api/env_group'
|
import type { EnvGroup } from '@/api/env_group'
|
||||||
import type { Environment } from '@/api/environment'
|
import type { Environment } from '@/api/environment'
|
||||||
import nodeApi from '@/api/node'
|
import nodeApi from '@/api/node'
|
||||||
|
import { useSSE } from '@/composables/useSSE'
|
||||||
import { useUserStore } from '@/pinia'
|
import { useUserStore } from '@/pinia'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import { SSE } from 'sse.js'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
envGroups: EnvGroup[]
|
envGroups: EnvGroup[]
|
||||||
|
@ -15,57 +15,39 @@ const { token } = storeToRefs(useUserStore())
|
||||||
|
|
||||||
const environments = ref<Environment[]>([])
|
const environments = ref<Environment[]>([])
|
||||||
const environmentsMap = ref<Record<number, Environment>>({})
|
const environmentsMap = ref<Record<number, Environment>>({})
|
||||||
const sse = shallowRef<SSE>()
|
|
||||||
const loading = ref({
|
const loading = ref({
|
||||||
reload: false,
|
reload: false,
|
||||||
restart: false,
|
restart: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 使用SSE composable
|
||||||
|
const { connect, disconnect } = useSSE()
|
||||||
|
|
||||||
// Get node data when tab is not 'All'
|
// Get node data when tab is not 'All'
|
||||||
watch(modelValue, newVal => {
|
watch(modelValue, newVal => {
|
||||||
if (newVal && newVal !== 0) {
|
if (newVal && newVal !== 0) {
|
||||||
connectSSE()
|
connectSSE()
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
disconnectSSE()
|
disconnect()
|
||||||
}
|
}
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
disconnectSSE()
|
|
||||||
})
|
|
||||||
|
|
||||||
function connectSSE() {
|
function connectSSE() {
|
||||||
disconnectSSE()
|
connect({
|
||||||
|
url: 'api/environments/enabled',
|
||||||
const s = new SSE('api/environments/enabled', {
|
token: token.value,
|
||||||
headers: {
|
onMessage: data => {
|
||||||
Authorization: token.value,
|
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
|
// Get the current Node Group data
|
||||||
|
|
88
app/src/composables/useNginxPerformance.ts
Normal file
88
app/src/composables/useNginxPerformance.ts
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
import type { NginxPerformanceInfo } from '@/api/ngx'
|
||||||
|
import ngx from '@/api/ngx'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
// Time formatting helper function
|
||||||
|
function formatTimeAgo(date: Date): string {
|
||||||
|
const now = new Date()
|
||||||
|
const diffMs = now.getTime() - date.getTime()
|
||||||
|
const diffSec = Math.round(diffMs / 1000)
|
||||||
|
|
||||||
|
if (diffSec < 60) {
|
||||||
|
return `${diffSec} ${$gettext('秒前')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const diffMin = Math.floor(diffSec / 60)
|
||||||
|
if (diffMin < 60) {
|
||||||
|
return `${diffMin} ${$gettext('分钟前')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.toLocaleTimeString()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNginxPerformance() {
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
const nginxInfo = ref<NginxPerformanceInfo | null>(null)
|
||||||
|
const lastUpdateTime = ref<Date | null>(null)
|
||||||
|
|
||||||
|
// stub_status availability
|
||||||
|
const stubStatusEnabled = ref(false)
|
||||||
|
const stubStatusLoading = ref(false)
|
||||||
|
const stubStatusError = ref('')
|
||||||
|
|
||||||
|
// Format the last update time
|
||||||
|
const formattedUpdateTime = computed(() => {
|
||||||
|
if (!lastUpdateTime.value)
|
||||||
|
return $gettext('Unknown')
|
||||||
|
return formatTimeAgo(lastUpdateTime.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update the last update time
|
||||||
|
function updateLastUpdateTime() {
|
||||||
|
lastUpdateTime.value = new Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check stub_status availability and get initial data
|
||||||
|
async function fetchInitialData() {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
stubStatusLoading.value = true
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
// Get performance data
|
||||||
|
const response = await ngx.detail_status()
|
||||||
|
|
||||||
|
if (response.running) {
|
||||||
|
stubStatusEnabled.value = response.stub_status_enabled
|
||||||
|
nginxInfo.value = response.info
|
||||||
|
updateLastUpdateTime()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
error.value = $gettext('Nginx is not running')
|
||||||
|
nginxInfo.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error('Failed to get Nginx performance data:', err)
|
||||||
|
error.value = $gettext('Failed to get performance data')
|
||||||
|
nginxInfo.value = null
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
loading.value = false
|
||||||
|
stubStatusLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
nginxInfo,
|
||||||
|
error,
|
||||||
|
formattedUpdateTime,
|
||||||
|
updateLastUpdateTime,
|
||||||
|
fetchInitialData,
|
||||||
|
stubStatusEnabled,
|
||||||
|
stubStatusLoading,
|
||||||
|
stubStatusError,
|
||||||
|
}
|
||||||
|
}
|
194
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>) {
|
||||||
|
// Format numbers to a more readable form
|
||||||
|
function formatNumber(num: number): string {
|
||||||
|
if (num >= 1000000) {
|
||||||
|
return `${(num / 1000000).toFixed(2)}M`
|
||||||
|
}
|
||||||
|
else if (num >= 1000) {
|
||||||
|
return `${(num / 1000).toFixed(2)}K`
|
||||||
|
}
|
||||||
|
return num.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Active connections percentage
|
||||||
|
const activeConnectionsPercent = computed(() => {
|
||||||
|
if (!nginxInfo.value) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
const maxConnections = nginxInfo.value.worker_connections * nginxInfo.value.worker_processes
|
||||||
|
return Number(((nginxInfo.value.active / maxConnections) * 100).toFixed(2))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Worker processes usage percentage
|
||||||
|
const workerProcessesPercent = computed(() => {
|
||||||
|
if (!nginxInfo.value) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return Number(((nginxInfo.value.workers / nginxInfo.value.worker_processes) * 100).toFixed(2))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Requests per connection
|
||||||
|
const requestsPerConnection = computed(() => {
|
||||||
|
if (!nginxInfo.value || nginxInfo.value.handled === 0) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return (nginxInfo.value.requests / nginxInfo.value.handled).toFixed(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Maximum requests per second
|
||||||
|
const maxRPS = computed(() => {
|
||||||
|
if (!nginxInfo.value) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return nginxInfo.value.worker_processes * nginxInfo.value.worker_connections
|
||||||
|
})
|
||||||
|
|
||||||
|
// Process composition data
|
||||||
|
const processTypeData = computed(() => {
|
||||||
|
if (!nginxInfo.value) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{ type: $gettext('Worker Processes'), value: nginxInfo.value.workers, color: '#1890ff' },
|
||||||
|
{ type: $gettext('Master Process'), value: nginxInfo.value.master, color: '#52c41a' },
|
||||||
|
{ type: $gettext('Cache Processes'), value: nginxInfo.value.cache, color: '#faad14' },
|
||||||
|
{ type: $gettext('Other Processes'), value: nginxInfo.value.other, color: '#f5222d' },
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Resource utilization
|
||||||
|
const resourceUtilization = computed(() => {
|
||||||
|
if (!nginxInfo.value) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const cpuFactor = Math.min(nginxInfo.value.cpu_usage / 100, 1)
|
||||||
|
const maxConnections = nginxInfo.value.worker_connections * nginxInfo.value.worker_processes
|
||||||
|
const connectionFactor = Math.min(nginxInfo.value.active / maxConnections, 1)
|
||||||
|
|
||||||
|
return Math.round((cpuFactor * 0.5 + connectionFactor * 0.5) * 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Table data
|
||||||
|
const statusData = computed(() => {
|
||||||
|
if (!nginxInfo.value) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: '1',
|
||||||
|
name: $gettext('Active connections'),
|
||||||
|
value: formatNumber(nginxInfo.value.active),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '2',
|
||||||
|
name: $gettext('Total handshakes'),
|
||||||
|
value: formatNumber(nginxInfo.value.accepts),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '3',
|
||||||
|
name: $gettext('Total connections'),
|
||||||
|
value: formatNumber(nginxInfo.value.handled),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '4',
|
||||||
|
name: $gettext('Total requests'),
|
||||||
|
value: formatNumber(nginxInfo.value.requests),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '5',
|
||||||
|
name: $gettext('Read requests'),
|
||||||
|
value: nginxInfo.value.reading,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '6',
|
||||||
|
name: $gettext('Responses'),
|
||||||
|
value: nginxInfo.value.writing,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '7',
|
||||||
|
name: $gettext('Waiting processes'),
|
||||||
|
value: nginxInfo.value.waiting,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Worker processes data
|
||||||
|
const workerData = computed(() => {
|
||||||
|
if (!nginxInfo.value) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: '1',
|
||||||
|
name: $gettext('Number of worker processes'),
|
||||||
|
value: nginxInfo.value.workers,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '2',
|
||||||
|
name: $gettext('Master process'),
|
||||||
|
value: nginxInfo.value.master,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '3',
|
||||||
|
name: $gettext('Cache manager processes'),
|
||||||
|
value: nginxInfo.value.cache,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '4',
|
||||||
|
name: $gettext('Other Nginx processes'),
|
||||||
|
value: nginxInfo.value.other,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '5',
|
||||||
|
name: $gettext('Nginx CPU usage rate'),
|
||||||
|
value: `${nginxInfo.value.cpu_usage.toFixed(2)}%`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '6',
|
||||||
|
name: $gettext('Nginx Memory usage'),
|
||||||
|
value: `${nginxInfo.value.memory_usage.toFixed(2)} MB`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Configuration data
|
||||||
|
const configData = computed(() => {
|
||||||
|
if (!nginxInfo.value) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: '1',
|
||||||
|
name: $gettext('Number of worker processes'),
|
||||||
|
value: nginxInfo.value.worker_processes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '2',
|
||||||
|
name: $gettext('Maximum number of connections per worker process'),
|
||||||
|
value: nginxInfo.value.worker_connections,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
formatNumber,
|
||||||
|
activeConnectionsPercent,
|
||||||
|
workerProcessesPercent,
|
||||||
|
requestsPerConnection,
|
||||||
|
maxRPS,
|
||||||
|
processTypeData,
|
||||||
|
resourceUtilization,
|
||||||
|
statusData,
|
||||||
|
workerData,
|
||||||
|
configData,
|
||||||
|
}
|
||||||
|
}
|
91
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
|
||||||
|
* Provide the ability to create, manage, and automatically clean up SSE connections
|
||||||
|
*/
|
||||||
|
export function useSSE() {
|
||||||
|
const sseInstance = shallowRef<SSE>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to SSE service
|
||||||
|
*/
|
||||||
|
function connect(options: SSEOptions) {
|
||||||
|
disconnect()
|
||||||
|
|
||||||
|
const {
|
||||||
|
url,
|
||||||
|
token,
|
||||||
|
onMessage,
|
||||||
|
onError,
|
||||||
|
parseData = true,
|
||||||
|
reconnectInterval = 5000,
|
||||||
|
} = options
|
||||||
|
|
||||||
|
const sse = new SSE(url, {
|
||||||
|
headers: {
|
||||||
|
Authorization: token,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle messages
|
||||||
|
sse.onmessage = (e: SSEvent) => {
|
||||||
|
if (!e.data) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedData = parseData ? JSON.parse(e.data) : e.data
|
||||||
|
onMessage?.(parsedData)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('Error parsing SSE message:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle errors and reconnect
|
||||||
|
sse.onerror = () => {
|
||||||
|
onError?.()
|
||||||
|
|
||||||
|
// Reconnect logic
|
||||||
|
setTimeout(() => {
|
||||||
|
connect(options)
|
||||||
|
}, reconnectInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
sseInstance.value = sse
|
||||||
|
return sse
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect SSE connection
|
||||||
|
*/
|
||||||
|
function disconnect() {
|
||||||
|
if (sseInstance.value) {
|
||||||
|
sseInstance.value.close()
|
||||||
|
sseInstance.value = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Automatically disconnect when the component is unmounted
|
||||||
|
onUnmounted(() => {
|
||||||
|
disconnect()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
connect,
|
||||||
|
disconnect,
|
||||||
|
sseInstance,
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
@ -121,7 +122,7 @@ export function setupResponseInterceptor() {
|
||||||
const otpModal = use2FAModal()
|
const otpModal = use2FAModal()
|
||||||
|
|
||||||
// Handle authentication errors
|
// Handle authentication errors
|
||||||
if (error.response) {
|
if (error?.response) {
|
||||||
switch (error.response.status) {
|
switch (error.response.status) {
|
||||||
case 401:
|
case 401:
|
||||||
secureSessionId.value = ''
|
secureSessionId.value = ''
|
||||||
|
@ -135,7 +136,7 @@ export function setupResponseInterceptor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle JSON error that comes back as Blob for blob request type
|
// 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 {
|
try {
|
||||||
const text = await error.response.data.text()
|
const text = await error.response.data.text()
|
||||||
error.response.data = JSON.parse(text)
|
error.response.data = JSON.parse(text)
|
||||||
|
|
|
@ -4,11 +4,29 @@ import { HomeOutlined } from '@ant-design/icons-vue'
|
||||||
export const dashboardRoutes: RouteRecordRaw[] = [
|
export const dashboardRoutes: RouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
path: 'dashboard',
|
path: 'dashboard',
|
||||||
component: () => import('@/views/dashboard/DashBoard.vue'),
|
redirect: '/dashboard/server',
|
||||||
name: 'Dashboard',
|
name: 'Dashboard',
|
||||||
meta: {
|
meta: {
|
||||||
name: () => $gettext('Dashboard'),
|
name: () => $gettext('Dashboard'),
|
||||||
icon: HomeOutlined,
|
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}
|
220
app/src/views/dashboard/NginxDashBoard.vue
Normal file
220
app/src/views/dashboard/NginxDashBoard.vue
Normal file
|
@ -0,0 +1,220 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useNginxPerformance } from '@/composables/useNginxPerformance'
|
||||||
|
import { useSSE } from '@/composables/useSSE'
|
||||||
|
import { NginxStatus } from '@/constants'
|
||||||
|
import { useUserStore } from '@/pinia'
|
||||||
|
import { useGlobalStore } from '@/pinia/moudule/global'
|
||||||
|
import { ClockCircleOutlined, ReloadOutlined } from '@ant-design/icons-vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import ConnectionMetricsCard from './components/ConnectionMetricsCard.vue'
|
||||||
|
import PerformanceStatisticsCard from './components/PerformanceStatisticsCard.vue'
|
||||||
|
import PerformanceTablesCard from './components/PerformanceTablesCard.vue'
|
||||||
|
import ProcessDistributionCard from './components/ProcessDistributionCard.vue'
|
||||||
|
import ResourceUsageCard from './components/ResourceUsageCard.vue'
|
||||||
|
|
||||||
|
// Global state
|
||||||
|
const global = useGlobalStore()
|
||||||
|
const { nginxStatus: status } = storeToRefs(global)
|
||||||
|
const { token } = storeToRefs(useUserStore())
|
||||||
|
|
||||||
|
// Use performance data composable
|
||||||
|
const {
|
||||||
|
loading,
|
||||||
|
nginxInfo,
|
||||||
|
error,
|
||||||
|
formattedUpdateTime,
|
||||||
|
updateLastUpdateTime,
|
||||||
|
fetchInitialData,
|
||||||
|
stubStatusEnabled,
|
||||||
|
stubStatusLoading,
|
||||||
|
stubStatusError,
|
||||||
|
} = useNginxPerformance()
|
||||||
|
|
||||||
|
// SSE connection
|
||||||
|
const { connect, disconnect } = useSSE()
|
||||||
|
|
||||||
|
// Toggle stub_status module status
|
||||||
|
async function toggleStubStatus() {
|
||||||
|
try {
|
||||||
|
stubStatusLoading.value = true
|
||||||
|
stubStatusError.value = ''
|
||||||
|
const response = await axios.post('/api/nginx/stub_status', {
|
||||||
|
enable: !stubStatusEnabled.value,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.data.stub_status_enabled !== undefined) {
|
||||||
|
stubStatusEnabled.value = response.data.stub_status_enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.data.error) {
|
||||||
|
stubStatusError.value = response.data.error
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
fetchInitialData().then(connectSSE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error('Toggle stub_status failed:', err)
|
||||||
|
stubStatusError.value = $gettext('Toggle failed')
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
stubStatusLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect SSE
|
||||||
|
function connectSSE() {
|
||||||
|
disconnect()
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
connect({
|
||||||
|
url: 'api/nginx/detail_status/stream',
|
||||||
|
token: token.value,
|
||||||
|
onMessage: data => {
|
||||||
|
loading.value = false
|
||||||
|
|
||||||
|
if (data.running) {
|
||||||
|
nginxInfo.value = data.info
|
||||||
|
updateLastUpdateTime()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
error.value = data.message || $gettext('Nginx is not running')
|
||||||
|
}
|
||||||
|
stubStatusEnabled.value = data.stub_status_enabled
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
error.value = $gettext('Connection error, trying to reconnect...')
|
||||||
|
|
||||||
|
// If the connection fails, try to get data using the traditional method
|
||||||
|
setTimeout(() => {
|
||||||
|
fetchInitialData()
|
||||||
|
}, 2000)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manually refresh data
|
||||||
|
function refreshData() {
|
||||||
|
fetchInitialData().then(connectSSE)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize connection when the component is mounted
|
||||||
|
onMounted(() => {
|
||||||
|
fetchInitialData().then(connectSSE)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- Top operation bar -->
|
||||||
|
<div class="mb-4 mx-6 md:mx-0 flex flex-wrap justify-between items-center">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<ABadge :status="status === NginxStatus.Running ? 'success' : 'error'" />
|
||||||
|
<span class="font-medium">{{ status === NginxStatus.Running ? $gettext('Nginx is running') : $gettext('Nginx is not running') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<ClockCircleOutlined class="mr-1 text-gray-500" />
|
||||||
|
<span class="mr-4 text-gray-500 text-sm text-nowrap">{{ $gettext('Last update') }}: {{ formattedUpdateTime }}</span>
|
||||||
|
<AButton type="text" size="small" :loading="loading" @click="refreshData">
|
||||||
|
<template #icon>
|
||||||
|
<ReloadOutlined />
|
||||||
|
</template>
|
||||||
|
</AButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Nginx status prompt -->
|
||||||
|
<AAlert
|
||||||
|
v-if="status !== NginxStatus.Running"
|
||||||
|
class="mb-4"
|
||||||
|
type="warning"
|
||||||
|
show-icon
|
||||||
|
:message="$gettext('Nginx is not running')"
|
||||||
|
:description="$gettext('Cannot get performance data in this state')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Error prompt -->
|
||||||
|
<AAlert
|
||||||
|
v-if="error"
|
||||||
|
class="mb-4"
|
||||||
|
type="error"
|
||||||
|
show-icon
|
||||||
|
:message="$gettext('Get data failed')"
|
||||||
|
:description="error"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- stub_status 开关 -->
|
||||||
|
<ACard class="mb-4" :bordered="false">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="font-medium mb-1">
|
||||||
|
{{ $gettext('Enable stub_status module') }}
|
||||||
|
</div>
|
||||||
|
<div class="text-gray-500 text-sm">
|
||||||
|
{{ $gettext('This module provides Nginx request statistics, connection count, etc. data. After enabling it, you can view performance statistics') }}
|
||||||
|
</div>
|
||||||
|
<div v-if="stubStatusError" class="text-red-500 text-sm mt-1">
|
||||||
|
{{ stubStatusError }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ASwitch
|
||||||
|
:checked="stubStatusEnabled"
|
||||||
|
:loading="stubStatusLoading"
|
||||||
|
@change="toggleStubStatus"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ACard>
|
||||||
|
|
||||||
|
<!-- stub_status module is not enabled -->
|
||||||
|
<AAlert
|
||||||
|
v-if="status === NginxStatus.Running && !stubStatusEnabled && !error"
|
||||||
|
class="mb-4"
|
||||||
|
type="info"
|
||||||
|
show-icon
|
||||||
|
:message="$gettext('Need to enable the stub_status module')"
|
||||||
|
:description="$gettext('Please enable the stub_status module to get request statistics, connection count, etc.')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Loading state -->
|
||||||
|
<ASpin :spinning="loading" :tip="$gettext('Loading data...')">
|
||||||
|
<div v-if="!nginxInfo && !error" class="text-center py-8">
|
||||||
|
<AEmpty :description="$gettext('No data')" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="nginxInfo" class="performance-dashboard">
|
||||||
|
<!-- Top performance metrics card -->
|
||||||
|
<ACard class="mb-4" :title="$gettext('Performance Metrics')" :bordered="false">
|
||||||
|
<PerformanceStatisticsCard :nginx-info="nginxInfo" />
|
||||||
|
</ACard>
|
||||||
|
|
||||||
|
<ARow :gutter="[16, 16]" class="mb-4">
|
||||||
|
<!-- Metrics card -->
|
||||||
|
<ACol :sm="24" :lg="12">
|
||||||
|
<ConnectionMetricsCard :nginx-info="nginxInfo" />
|
||||||
|
</ACol>
|
||||||
|
|
||||||
|
<!-- CPU and memory usage -->
|
||||||
|
<ACol :sm="24" :lg="12">
|
||||||
|
<ResourceUsageCard :nginx-info="nginxInfo" />
|
||||||
|
</ACol>
|
||||||
|
</ARow>
|
||||||
|
|
||||||
|
<!-- Resource monitoring -->
|
||||||
|
<ARow :gutter="[16, 16]" class="mb-4">
|
||||||
|
<!-- Process distribution -->
|
||||||
|
<ACol :span="24">
|
||||||
|
<ProcessDistributionCard :nginx-info="nginxInfo" />
|
||||||
|
</ACol>
|
||||||
|
</ARow>
|
||||||
|
|
||||||
|
<!-- Performance metrics table -->
|
||||||
|
<ARow :gutter="[16, 16]" class="mb-4">
|
||||||
|
<ACol :span="24">
|
||||||
|
<PerformanceTablesCard :nginx-info="nginxInfo" />
|
||||||
|
</ACol>
|
||||||
|
</ARow>
|
||||||
|
</div>
|
||||||
|
</ASpin>
|
||||||
|
</div>
|
||||||
|
</template>
|
64
app/src/views/dashboard/components/ConnectionMetricsCard.vue
Normal file
64
app/src/views/dashboard/components/ConnectionMetricsCard.vue
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { NginxPerformanceInfo } from '@/api/ngx'
|
||||||
|
import { computed, defineProps } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
nginxInfo: NginxPerformanceInfo
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// Active connections percentage
|
||||||
|
const activeConnectionsPercent = computed(() => {
|
||||||
|
const maxConnections = props.nginxInfo.worker_connections * props.nginxInfo.worker_processes
|
||||||
|
return Number(((props.nginxInfo.active / maxConnections) * 100).toFixed(2))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Worker processes usage percentage
|
||||||
|
const workerProcessesPercent = computed(() => {
|
||||||
|
return Number(((props.nginxInfo.workers / props.nginxInfo.worker_processes) * 100).toFixed(2))
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ARow :gutter="[16, 16]" class="h-full">
|
||||||
|
<!-- Current active connections -->
|
||||||
|
<ACol :xs="24" :sm="12">
|
||||||
|
<ACard class="h-full" :bordered="false" :body-style="{ padding: '20px', height: '100%' }">
|
||||||
|
<div class="flex flex-col h-full">
|
||||||
|
<div class="mb-2 text-gray-500 font-medium truncate">
|
||||||
|
{{ $gettext('Current active connections') }}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-baseline mb-2">
|
||||||
|
<span class="text-2xl font-bold mr-2">{{ nginxInfo.active }}</span>
|
||||||
|
<span class="text-gray-500 text-sm">/ {{ nginxInfo.worker_connections * nginxInfo.worker_processes }}</span>
|
||||||
|
</div>
|
||||||
|
<AProgress
|
||||||
|
:percent="activeConnectionsPercent"
|
||||||
|
:format="percent => `${percent?.toFixed(2)}%`"
|
||||||
|
:status="activeConnectionsPercent > 80 ? 'exception' : 'normal'"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ACard>
|
||||||
|
</ACol>
|
||||||
|
|
||||||
|
<!-- Worker processes -->
|
||||||
|
<ACol :xs="24" :sm="12">
|
||||||
|
<ACard class="h-full" :bordered="false" :body-style="{ padding: '20px', height: '100%' }">
|
||||||
|
<div class="flex flex-col h-full">
|
||||||
|
<div class="mb-2 text-gray-500 font-medium truncate">
|
||||||
|
{{ $gettext('Worker Processes') }}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-baseline mb-2">
|
||||||
|
<span class="text-2xl font-bold mr-2">{{ nginxInfo.workers }}</span>
|
||||||
|
<span class="text-gray-500 text-sm">/ {{ nginxInfo.worker_processes }}</span>
|
||||||
|
</div>
|
||||||
|
<AProgress
|
||||||
|
:percent="workerProcessesPercent"
|
||||||
|
size="small"
|
||||||
|
status="active"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ACard>
|
||||||
|
</ACol>
|
||||||
|
</ARow>
|
||||||
|
</template>
|
107
app/src/views/dashboard/components/PerformanceStatisticsCard.vue
Normal file
107
app/src/views/dashboard/components/PerformanceStatisticsCard.vue
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { NginxPerformanceInfo } from '@/api/ngx'
|
||||||
|
import {
|
||||||
|
ApiOutlined,
|
||||||
|
CloudServerOutlined,
|
||||||
|
DashboardOutlined,
|
||||||
|
InfoCircleOutlined,
|
||||||
|
ThunderboltOutlined,
|
||||||
|
} from '@ant-design/icons-vue'
|
||||||
|
import { computed, defineProps } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
nginxInfo: NginxPerformanceInfo
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// Calculate connection efficiency - requests per connection
|
||||||
|
const requestsPerConnection = computed(() => {
|
||||||
|
if (props.nginxInfo.handled === 0) {
|
||||||
|
return '0'
|
||||||
|
}
|
||||||
|
return (props.nginxInfo.requests / props.nginxInfo.handled).toFixed(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Estimate maximum requests per second
|
||||||
|
const maxRPS = computed(() => {
|
||||||
|
return props.nginxInfo.worker_processes * props.nginxInfo.worker_connections
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ARow :gutter="[16, 24]">
|
||||||
|
<!-- Maximum RPS -->
|
||||||
|
<ACol :xs="24" :sm="12" :md="8" :lg="6">
|
||||||
|
<AStatistic
|
||||||
|
:value="maxRPS"
|
||||||
|
:value-style="{ color: '#1890ff', fontSize: '24px' }"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<ThunderboltOutlined />
|
||||||
|
</template>
|
||||||
|
<template #title>
|
||||||
|
{{ $gettext('Max Requests Per Second') }}
|
||||||
|
<ATooltip :title="$gettext('Calculated based on worker_processes * worker_connections. Actual performance depends on hardware, configuration, and workload')">
|
||||||
|
<InfoCircleOutlined class="ml-1 text-gray-500" />
|
||||||
|
</ATooltip>
|
||||||
|
</template>
|
||||||
|
</AStatistic>
|
||||||
|
<div class="text-xs text-gray-500 mt-1">
|
||||||
|
worker_processes ({{ nginxInfo.worker_processes }}) × worker_connections ({{ nginxInfo.worker_connections }})
|
||||||
|
</div>
|
||||||
|
</ACol>
|
||||||
|
|
||||||
|
<!-- Maximum concurrent connections -->
|
||||||
|
<ACol :xs="24" :sm="12" :md="8" :lg="6">
|
||||||
|
<AStatistic
|
||||||
|
:title="$gettext('Max Concurrent Connections')"
|
||||||
|
:value="nginxInfo.worker_processes * nginxInfo.worker_connections"
|
||||||
|
:value-style="{ color: '#52c41a', fontSize: '24px' }"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<ApiOutlined />
|
||||||
|
</template>
|
||||||
|
</AStatistic>
|
||||||
|
<div class="text-xs text-gray-500 mt-1">
|
||||||
|
{{ $gettext('Current usage') }}: {{ ((nginxInfo.active / (nginxInfo.worker_processes * nginxInfo.worker_connections)) * 100).toFixed(2) }}%
|
||||||
|
</div>
|
||||||
|
</ACol>
|
||||||
|
|
||||||
|
<!-- Requests per connection -->
|
||||||
|
<ACol :xs="24" :sm="12" :md="8" :lg="6">
|
||||||
|
<AStatistic
|
||||||
|
:value="requestsPerConnection"
|
||||||
|
:precision="2"
|
||||||
|
:value-style="{ color: '#3a7f99', fontSize: '24px' }"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
{{ $gettext('Requests Per Connection') }}
|
||||||
|
<ATooltip :title="$gettext('Total Requests / Total Connections')">
|
||||||
|
<InfoCircleOutlined class="ml-1 text-gray-500" />
|
||||||
|
</ATooltip>
|
||||||
|
</template>
|
||||||
|
<template #prefix>
|
||||||
|
<DashboardOutlined />
|
||||||
|
</template>
|
||||||
|
</AStatistic>
|
||||||
|
<div class="text-xs text-gray-500 mt-1">
|
||||||
|
{{ $gettext('Higher value means better connection reuse') }}
|
||||||
|
</div>
|
||||||
|
</ACol>
|
||||||
|
|
||||||
|
<!-- Total Nginx processes -->
|
||||||
|
<ACol :xs="24" :sm="12" :md="8" :lg="6">
|
||||||
|
<AStatistic
|
||||||
|
:title="$gettext('Total Nginx Processes')"
|
||||||
|
:value="nginxInfo.workers + nginxInfo.master + nginxInfo.cache + nginxInfo.other"
|
||||||
|
:value-style="{ color: '#722ed1', fontSize: '24px' }"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<CloudServerOutlined />
|
||||||
|
</template>
|
||||||
|
</AStatistic>
|
||||||
|
<div class="text-xs text-gray-500 mt-1">
|
||||||
|
{{ $gettext('Workers') }}: {{ nginxInfo.workers }}, {{ $gettext('Master') }}: {{ nginxInfo.master }}, {{ $gettext('Others') }}: {{ nginxInfo.cache + nginxInfo.other }}
|
||||||
|
</div>
|
||||||
|
</ACol>
|
||||||
|
</ARow>
|
||||||
|
</template>
|
211
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')
|
||||||
|
|
||||||
|
// Table column definition
|
||||||
|
const columns: TableColumnType[] = [
|
||||||
|
{
|
||||||
|
title: $gettext('Indicator'),
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
width: '30%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $gettext('Value'),
|
||||||
|
dataIndex: 'value',
|
||||||
|
key: 'value',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// Format numbers
|
||||||
|
function formatNumber(num: number): string {
|
||||||
|
if (num >= 1000000) {
|
||||||
|
return `${(num / 1000000).toFixed(2)}M`
|
||||||
|
}
|
||||||
|
else if (num >= 1000) {
|
||||||
|
return `${(num / 1000).toFixed(2)}K`
|
||||||
|
}
|
||||||
|
return num.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status data
|
||||||
|
const statusData = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: '1',
|
||||||
|
name: $gettext('Active connections'),
|
||||||
|
value: formatNumber(props.nginxInfo.active),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '2',
|
||||||
|
name: $gettext('Total handshakes'),
|
||||||
|
value: formatNumber(props.nginxInfo.accepts),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '3',
|
||||||
|
name: $gettext('Total connections'),
|
||||||
|
value: formatNumber(props.nginxInfo.handled),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '4',
|
||||||
|
name: $gettext('Total requests'),
|
||||||
|
value: formatNumber(props.nginxInfo.requests),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '5',
|
||||||
|
name: $gettext('Read requests'),
|
||||||
|
value: props.nginxInfo.reading,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '6',
|
||||||
|
name: $gettext('Responses'),
|
||||||
|
value: props.nginxInfo.writing,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '7',
|
||||||
|
name: $gettext('Waiting processes'),
|
||||||
|
value: props.nginxInfo.waiting,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Worker processes data
|
||||||
|
const workerData = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: '1',
|
||||||
|
name: $gettext('Number of worker processes'),
|
||||||
|
value: props.nginxInfo.workers,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '2',
|
||||||
|
name: $gettext('Master process'),
|
||||||
|
value: props.nginxInfo.master,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '3',
|
||||||
|
name: $gettext('Cache manager processes'),
|
||||||
|
value: props.nginxInfo.cache,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '4',
|
||||||
|
name: $gettext('Other Nginx processes'),
|
||||||
|
value: props.nginxInfo.other,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '5',
|
||||||
|
name: $gettext('Nginx CPU usage rate'),
|
||||||
|
value: `${props.nginxInfo.cpu_usage.toFixed(2)}%`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '6',
|
||||||
|
name: $gettext('Nginx Memory usage'),
|
||||||
|
value: `${props.nginxInfo.memory_usage.toFixed(2)} MB`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Configuration data
|
||||||
|
const configData = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: '1',
|
||||||
|
name: $gettext('Number of worker processes'),
|
||||||
|
value: props.nginxInfo.worker_processes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '2',
|
||||||
|
name: $gettext('Maximum number of connections per worker process'),
|
||||||
|
value: props.nginxInfo.worker_connections,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Maximum requests per second
|
||||||
|
const maxRPS = computed(() => {
|
||||||
|
return props.nginxInfo.worker_processes * props.nginxInfo.worker_connections
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ACard :bordered="false">
|
||||||
|
<ATabs v-model:active-key="activeTabKey">
|
||||||
|
<!-- Request statistics -->
|
||||||
|
<ATabPane key="status" :tab="$gettext('Request statistics')">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<ATable
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="statusData"
|
||||||
|
:pagination="false"
|
||||||
|
size="middle"
|
||||||
|
:scroll="{ x: '100%' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ATabPane>
|
||||||
|
|
||||||
|
<!-- Process information -->
|
||||||
|
<ATabPane key="workers" :tab="$gettext('Process information')">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<ATable
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="workerData"
|
||||||
|
:pagination="false"
|
||||||
|
size="middle"
|
||||||
|
:scroll="{ x: '100%' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ATabPane>
|
||||||
|
|
||||||
|
<!-- Configuration information -->
|
||||||
|
<ATabPane key="config" :tab="$gettext('Configuration information')">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<ATable
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="configData"
|
||||||
|
:pagination="false"
|
||||||
|
size="middle"
|
||||||
|
:scroll="{ x: '100%' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<AAlert type="info" show-icon>
|
||||||
|
<template #message>
|
||||||
|
{{ $gettext('Nginx theoretical maximum performance') }}
|
||||||
|
</template>
|
||||||
|
<template #description>
|
||||||
|
<p>
|
||||||
|
{{ $gettext('Theoretical maximum concurrent connections:') }}
|
||||||
|
<strong>{{ nginxInfo.worker_processes * nginxInfo.worker_connections }}</strong>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{{ $gettext('Theoretical maximum RPS (Requests Per Second):') }}
|
||||||
|
<strong>{{ maxRPS }}</strong>
|
||||||
|
<ATooltip :title="$gettext('Calculated based on worker_processes * worker_connections. Actual performance depends on hardware, configuration, and workload')">
|
||||||
|
<InfoCircleOutlined class="ml-1 text-gray-500" />
|
||||||
|
</ATooltip>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{{ $gettext('Maximum worker process number:') }}
|
||||||
|
<strong>{{ nginxInfo.worker_processes }}</strong>
|
||||||
|
<span class="text-gray-500 text-xs ml-2">
|
||||||
|
{{ nginxInfo.worker_processes === nginxInfo.workers ? $gettext('auto = CPU cores') : $gettext('manually set') }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
{{ $gettext('Tips: You can increase the concurrency processing capacity by increasing worker_processes or worker_connections') }}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</AAlert>
|
||||||
|
</div>
|
||||||
|
</ATabPane>
|
||||||
|
</ATabs>
|
||||||
|
</ACard>
|
||||||
|
</template>
|
|
@ -0,0 +1,60 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { NginxPerformanceInfo } from '@/api/ngx'
|
||||||
|
import { InfoCircleOutlined } from '@ant-design/icons-vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
nginxInfo: NginxPerformanceInfo
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// Process composition data
|
||||||
|
const processTypeData = computed(() => {
|
||||||
|
return [
|
||||||
|
{ type: $gettext('Worker Processes'), value: props.nginxInfo.workers, color: '#1890ff' },
|
||||||
|
{ type: $gettext('Master Process'), value: props.nginxInfo.master, color: '#52c41a' },
|
||||||
|
{ type: $gettext('Cache Processes'), value: props.nginxInfo.cache, color: '#faad14' },
|
||||||
|
{ type: $gettext('Other Processes'), value: props.nginxInfo.other, color: '#f5222d' },
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Total processes
|
||||||
|
const totalProcesses = computed(() => {
|
||||||
|
return props.nginxInfo.workers + props.nginxInfo.master + props.nginxInfo.cache + props.nginxInfo.other
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ACard :title="$gettext('Process Distribution')" :bordered="false" class="h-full" :body-style="{ height: 'calc(100% - 58px)' }">
|
||||||
|
<div class="process-distribution h-full flex flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
<div v-for="(item, index) in processTypeData" :key="index" class="mb-3">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-3 h-3 rounded-full mr-2" :style="{ backgroundColor: item.color }" />
|
||||||
|
<div class="flex-grow truncate">
|
||||||
|
{{ item.type }}
|
||||||
|
</div>
|
||||||
|
<div class="font-medium w-8 text-right">
|
||||||
|
{{ item.value }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<AProgress
|
||||||
|
:percent="totalProcesses === 0 ? 0 : (item.value / totalProcesses) * 100"
|
||||||
|
:stroke-color="item.color"
|
||||||
|
size="small"
|
||||||
|
:show-info="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-auto text-xs text-gray-500 truncate">
|
||||||
|
{{ $gettext('Actual worker to configured ratio') }}:
|
||||||
|
<span class="font-medium">{{ nginxInfo.workers }} / {{ nginxInfo.worker_processes }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 text-xs text-gray-500 overflow-hidden text-ellipsis">
|
||||||
|
{{ $gettext('Total Nginx processes') }}: {{ nginxInfo.workers + nginxInfo.master + nginxInfo.cache + nginxInfo.other }}
|
||||||
|
<ATooltip :title="$gettext('Includes master process, worker processes, cache processes, and other Nginx processes')">
|
||||||
|
<InfoCircleOutlined class="ml-1" />
|
||||||
|
</ATooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ACard>
|
||||||
|
</template>
|
61
app/src/views/dashboard/components/ResourceUsageCard.vue
Normal file
61
app/src/views/dashboard/components/ResourceUsageCard.vue
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { NginxPerformanceInfo } from '@/api/ngx'
|
||||||
|
import {
|
||||||
|
FundProjectionScreenOutlined,
|
||||||
|
InfoCircleOutlined,
|
||||||
|
ThunderboltOutlined,
|
||||||
|
} from '@ant-design/icons-vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
nginxInfo: NginxPerformanceInfo
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const cpuUsage = computed(() => {
|
||||||
|
return Number(Math.min(props.nginxInfo.cpu_usage, 100).toFixed(2))
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ACard :bordered="false" class="h-full" :body-style="{ padding: '20px', height: 'calc(100% - 58px)' }">
|
||||||
|
<div class="flex flex-col h-full">
|
||||||
|
<!-- CPU usage -->
|
||||||
|
<ARow :gutter="[16, 8]">
|
||||||
|
<ACol :span="24">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<ThunderboltOutlined class="text-lg mr-2" :style="{ color: nginxInfo.cpu_usage > 80 ? '#cf1322' : '#3f8600' }" />
|
||||||
|
<div class="text-base font-medium">
|
||||||
|
{{ $gettext('CPU Usage') }}: <span :style="{ color: nginxInfo.cpu_usage > 80 ? '#cf1322' : '#3f8600' }">{{ cpuUsage.toFixed(2) }}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<AProgress
|
||||||
|
:percent="cpuUsage"
|
||||||
|
:status="nginxInfo.cpu_usage > 80 ? 'exception' : 'active'"
|
||||||
|
size="small"
|
||||||
|
class="mt-1"
|
||||||
|
:show-info="false"
|
||||||
|
/>
|
||||||
|
<div v-if="nginxInfo.cpu_usage > 50" class="text-xs text-orange-500 mt-1">
|
||||||
|
{{ $gettext('CPU usage is relatively high, consider optimizing Nginx configuration') }}
|
||||||
|
</div>
|
||||||
|
</ACol>
|
||||||
|
</ARow>
|
||||||
|
|
||||||
|
<!-- Memory usage -->
|
||||||
|
<ARow :gutter="[16, 8]" class="mt-2">
|
||||||
|
<ACol :span="24">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="text-blue-500 text-lg mr-2 flex items-center">
|
||||||
|
<FundProjectionScreenOutlined />
|
||||||
|
</div>
|
||||||
|
<div class="text-base font-medium">
|
||||||
|
{{ $gettext('Memory Usage(RSS)') }}: <span class="text-blue-500">{{ nginxInfo.memory_usage.toFixed(2) }} MB</span>
|
||||||
|
</div>
|
||||||
|
<ATooltip :title="$gettext('Resident Set Size: Actual memory resident in physical memory, including all shared library memory, which will be repeated calculated for multiple processes')">
|
||||||
|
<InfoCircleOutlined class="ml-1 text-gray-500" />
|
||||||
|
</ATooltip>
|
||||||
|
</div>
|
||||||
|
</ACol>
|
||||||
|
</ARow>
|
||||||
|
</div>
|
||||||
|
</ACard>
|
||||||
|
</template>
|
48
internal/nginx/config_info.go
Normal file
48
internal/nginx/config_info.go
Normal 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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
55
internal/nginx/performance.go
Normal file
55
internal/nginx/performance.go
Normal 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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
178
internal/nginx/process_info.go
Normal file
178
internal/nginx/process_info.go
Normal 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
|
||||||
|
}
|
199
internal/nginx/stub_status.go
Normal file
199
internal/nginx/stub_status.go
Normal 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)
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue