feat: add stub_status_port configuration for Nginx settings

This commit is contained in:
Jacky 2025-04-11 08:32:02 +00:00
parent 2d56914af1
commit 509443a6e7
No known key found for this signature in database
GPG key ID: 215C21B10DF38B4D
9 changed files with 88 additions and 40 deletions

View file

@ -13,13 +13,13 @@ 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 // Get detailed Nginx status information, including connection count, process information, etc. (Issue #850)
r.GET("nginx/detail_status", GetDetailStatus) r.GET("nginx/detail_status", GetDetailStatus)
// 使用SSE推送Nginx详细状态信息 // Use SSE to push detailed Nginx status information
r.GET("nginx/detail_status/stream", StreamDetailStatus) r.GET("nginx/detail_status/stream", StreamDetailStatus)
// 获取 stub_status 模块状态 // Get stub_status module status
r.GET("nginx/stub_status", CheckStubStatus) r.GET("nginx/stub_status", CheckStubStatus)
// 启用或禁用 stub_status 模块 // Enable or disable stub_status module
r.POST("nginx/stub_status", ToggleStubStatus) r.POST("nginx/stub_status", ToggleStubStatus)
r.POST("nginx_log", nginx_log.GetNginxLogPage) r.POST("nginx_log", nginx_log.GetNginxLogPage)
r.GET("nginx/directives", GetDirectives) r.GET("nginx/directives", GetDirectives)

View file

@ -1,6 +1,6 @@
// GetDetailedStatus API 实现 // Implementation of GetDetailedStatus API
// 该功能用于解决 Issue #850提供类似宝塔面板的 Nginx 负载监控功能 // This feature is designed to address Issue #850, providing Nginx load monitoring functionality similar to BT Panel
// 返回详细的 Nginx 状态信息,包括请求统计、连接数、工作进程等数据 // Returns detailed Nginx status information, including request statistics, connections, worker processes, and other data
package nginx package nginx
import ( import (
@ -15,79 +15,79 @@ import (
"github.com/uozi-tech/cosy/logger" "github.com/uozi-tech/cosy/logger"
) )
// NginxPerformanceInfo 存储 Nginx 性能相关信息 // NginxPerformanceInfo stores Nginx performance-related information
type NginxPerformanceInfo struct { type NginxPerformanceInfo struct {
// 基本状态信息 // Basic status information
nginx.StubStatusData nginx.StubStatusData
// 进程相关信息 // Process-related information
nginx.NginxProcessInfo nginx.NginxProcessInfo
// 配置信息 // Configuration information
nginx.NginxConfigInfo nginx.NginxConfigInfo
} }
// GetDetailStatus 获取 Nginx 详细状态信息 // GetDetailStatus retrieves detailed Nginx status information
func GetDetailStatus(c *gin.Context) { func GetDetailStatus(c *gin.Context) {
response := nginx.GetPerformanceData() response := nginx.GetPerformanceData()
c.JSON(http.StatusOK, response) c.JSON(http.StatusOK, response)
} }
// StreamDetailStatus 使用 SSE 流式推送 Nginx 详细状态信息 // StreamDetailStatus streams Nginx detailed status information using SSE
func StreamDetailStatus(c *gin.Context) { func StreamDetailStatus(c *gin.Context) {
// 设置 SSE 的响应头 // Set SSE response headers
c.Header("Content-Type", "text/event-stream") c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache") c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive") c.Header("Connection", "keep-alive")
c.Header("Access-Control-Allow-Origin", "*") c.Header("Access-Control-Allow-Origin", "*")
// 创建上下文,当客户端断开连接时取消 // Create context that cancels when client disconnects
ctx := c.Request.Context() ctx := c.Request.Context()
// 为防止 goroutine 泄漏,创建一个计时器通道 // Create a ticker channel to prevent goroutine leaks
ticker := time.NewTicker(5 * time.Second) ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop() defer ticker.Stop()
// 立即发送一次初始数据 // Send initial data immediately
sendPerformanceData(c) sendPerformanceData(c)
// 使用 goroutine 定期发送数据 // Use goroutine to send data periodically
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
// 发送性能数据 // Send performance data
if err := sendPerformanceData(c); err != nil { if err := sendPerformanceData(c); err != nil {
logger.Warn("Error sending SSE data:", err) logger.Warn("Error sending SSE data:", err)
return return
} }
case <-ctx.Done(): case <-ctx.Done():
// 客户端断开连接或请求被取消 // Client closed connection or request canceled
logger.Debug("Client closed connection") logger.Debug("Client closed connection")
return return
} }
} }
} }
// sendPerformanceData 发送一次性能数据 // sendPerformanceData sends performance data once
func sendPerformanceData(c *gin.Context) error { func sendPerformanceData(c *gin.Context) error {
response := nginx.GetPerformanceData() response := nginx.GetPerformanceData()
// 发送 SSE 事件 // Send SSE event
c.SSEvent("message", response) c.SSEvent("message", response)
// 刷新缓冲区,确保数据立即发送 // Flush buffer to ensure data is sent immediately
c.Writer.Flush() c.Writer.Flush()
return nil return nil
} }
// CheckStubStatus 获取 Nginx stub_status 模块状态 // CheckStubStatus gets Nginx stub_status module status
func CheckStubStatus(c *gin.Context) { func CheckStubStatus(c *gin.Context) {
stubStatus := nginx.GetStubStatus() stubStatus := nginx.GetStubStatus()
c.JSON(http.StatusOK, stubStatus) c.JSON(http.StatusOK, stubStatus)
} }
// ToggleStubStatus 启用或禁用 stub_status 模块 // ToggleStubStatus enables or disables stub_status module
func ToggleStubStatus(c *gin.Context) { func ToggleStubStatus(c *gin.Context) {
var json struct { var json struct {
Enable bool `json:"enable"` Enable bool `json:"enable"`
@ -99,7 +99,7 @@ func ToggleStubStatus(c *gin.Context) {
stubStatus := nginx.GetStubStatus() stubStatus := nginx.GetStubStatus()
// 如果当前状态与期望状态相同,则无需操作 // If current status matches desired status, no action needed
if stubStatus.Enabled == json.Enable { if stubStatus.Enabled == json.Enable {
c.JSON(http.StatusOK, stubStatus) c.JSON(http.StatusOK, stubStatus)
return return
@ -117,7 +117,7 @@ func ToggleStubStatus(c *gin.Context) {
return return
} }
// 重新加载 Nginx 配置 // Reload Nginx configuration
reloadOutput := nginx.Reload() reloadOutput := nginx.Reload()
if len(reloadOutput) > 0 && (strings.Contains(strings.ToLower(reloadOutput), "error") || if len(reloadOutput) > 0 && (strings.Contains(strings.ToLower(reloadOutput), "error") ||
strings.Contains(strings.ToLower(reloadOutput), "failed")) { strings.Contains(strings.ToLower(reloadOutput), "failed")) {
@ -125,7 +125,7 @@ func ToggleStubStatus(c *gin.Context) {
return return
} }
// 检查操作后的状态 // Check status after operation
newStubStatus := nginx.GetStubStatus() newStubStatus := nginx.GetStubStatus()
c.JSON(http.StatusOK, newStubStatus) c.JSON(http.StatusOK, newStubStatus)

View file

@ -71,6 +71,7 @@ func SaveSettings(c *gin.Context) {
Node settings.Node `json:"node"` Node settings.Node `json:"node"`
Openai settings.OpenAI `json:"openai"` Openai settings.OpenAI `json:"openai"`
Logrotate settings.Logrotate `json:"logrotate"` Logrotate settings.Logrotate `json:"logrotate"`
Nginx settings.Nginx `json:"nginx"`
} }
if !cosy.BindAndValid(c, &json) { if !cosy.BindAndValid(c, &json) {
@ -104,6 +105,7 @@ func SaveSettings(c *gin.Context) {
cSettings.ProtectedFill(settings.NodeSettings, &json.Node) cSettings.ProtectedFill(settings.NodeSettings, &json.Node)
cSettings.ProtectedFill(settings.OpenAISettings, &json.Openai) cSettings.ProtectedFill(settings.OpenAISettings, &json.Openai)
cSettings.ProtectedFill(settings.LogrotateSettings, &json.Logrotate) cSettings.ProtectedFill(settings.LogrotateSettings, &json.Logrotate)
cSettings.ProtectedFill(settings.NginxSettings, &json.Nginx)
err := settings.Save() err := settings.Save()
if err != nil { if err != nil {

View file

@ -63,6 +63,7 @@ export interface NginxSettings {
test_config_cmd: string test_config_cmd: string
reload_cmd: string reload_cmd: string
restart_cmd: string restart_cmd: string
stub_status_port: number
} }
export interface NodeSettings { export interface NodeSettings {

View file

@ -6,6 +6,9 @@ const data: Ref<Settings> = inject('data') as Ref<Settings>
<template> <template>
<AForm layout="vertical"> <AForm layout="vertical">
<AFormItem :label="$gettext('Stub Status Port')">
<AInputNumber v-model:value="data.nginx.stub_status_port" />
</AFormItem>
<AFormItem :label="$gettext('Nginx Access Log Path')"> <AFormItem :label="$gettext('Nginx Access Log Path')">
{{ data.nginx.access_log_path }} {{ data.nginx.access_log_path }}
</AFormItem> </AFormItem>

View file

@ -74,6 +74,7 @@ const data = ref<Settings>({
test_config_cmd: '', test_config_cmd: '',
reload_cmd: '', reload_cmd: '',
restart_cmd: '', restart_cmd: '',
stub_status_port: 51820,
}, },
node: { node: {
name: '', name: '',

View file

@ -43,7 +43,7 @@ func GetPerformanceData() NginxPerformanceResponse {
logger.Warn("Failed to get Nginx config info:", err) logger.Warn("Failed to get Nginx config info:", err)
} }
// 确保ProcessMode字段能够正确传递 // Ensure ProcessMode field is correctly passed
perfInfo := NginxPerformanceInfo{ perfInfo := NginxPerformanceInfo{
StubStatusData: *statusInfo, StubStatusData: *statusInfo,
NginxProcessInfo: *processInfo, NginxProcessInfo: *processInfo,

View file

@ -1,6 +1,7 @@
package nginx package nginx
import ( import (
"bytes"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@ -8,8 +9,10 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"text/template"
"time" "time"
"github.com/0xJacky/Nginx-UI/settings"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/uozi-tech/cosy/logger" "github.com/uozi-tech/cosy/logger"
) )
@ -31,7 +34,6 @@ type StubStatusData struct {
} }
const ( const (
StubStatusPort = 51828
StubStatusPath = "/stub_status" StubStatusPath = "/stub_status"
StubStatusHost = "127.0.0.1" StubStatusHost = "127.0.0.1"
StubStatusProtocol = "http" StubStatusProtocol = "http"
@ -54,7 +56,7 @@ func GetStubStatusData() (bool, *StubStatusData, error) {
// Get the stub_status status information // Get the stub_status status information
enabled, statusURL := IsStubStatusEnabled() enabled, statusURL := IsStubStatusEnabled()
logger.Info("GetStubStatusData", "enabled", enabled, "statusURL", statusURL) logger.Debug("GetStubStatusData", "enabled", enabled, "statusURL", statusURL)
if !enabled { if !enabled {
return false, result, fmt.Errorf("stub_status is not enabled") return false, result, fmt.Errorf("stub_status is not enabled")
} }
@ -131,12 +133,12 @@ func IsStubStatusEnabled() (bool, string) {
for _, server := range ngxConfig.Servers { for _, server := range ngxConfig.Servers {
protocol := StubStatusProtocol protocol := StubStatusProtocol
host := StubStatusHost host := StubStatusHost
port := strconv.Itoa(StubStatusPort) port := settings.NginxSettings.StubStatusPort
for _, location := range server.Locations { for _, location := range server.Locations {
// Check if the location content contains stub_status // Check if the location content contains stub_status
if strings.Contains(location.Content, "stub_status") { if strings.Contains(location.Content, "stub_status") {
stubStatusURL := fmt.Sprintf("%s://%s:%s%s", protocol, host, port, StubStatusPath) stubStatusURL := fmt.Sprintf("%s://%s:%d%s", protocol, host, port, StubStatusPath)
return true, stubStatusURL return true, stubStatusURL
} }
} }
@ -169,23 +171,54 @@ func DisableStubStatus() error {
func CreateStubStatusConfig() error { func CreateStubStatusConfig() error {
httpConfPath := GetConfPath("conf.d", StubStatusConfigName) httpConfPath := GetConfPath("conf.d", StubStatusConfigName)
stubStatusConfig := ` const stubStatusTemplate = `
# DO NOT EDIT THIS FILE, IT IS AUTO GENERATED BY NGINX-UI # DO NOT EDIT THIS FILE, IT IS AUTO GENERATED BY NGINX-UI
# Nginx stub_status configuration for Nginx-UI # Nginx stub_status configuration for Nginx-UI
# Modified at ` + time.Now().Format("2006-01-02 15:04:05") + ` # Modified at {{.ModifiedTime}}
server { server {
listen 51828; # Use non-standard port to avoid conflicts listen {{.Port}}; # Use non-standard port to avoid conflicts
server_name localhost; server_name {{.ServerName}};
# Status monitoring interface # Status monitoring interface
location /stub_status { location {{.StatusPath}} {
stub_status; stub_status;
allow 127.0.0.1; # Only allow local access allow {{.AllowIP}}; # Only allow local access
deny all; deny {{.DenyAccess}};
} }
} }
` `
type StubStatusTemplateData struct {
ModifiedTime string
Port uint
ServerName string
StatusPath string
AllowIP string
DenyAccess string
}
data := StubStatusTemplateData{
ModifiedTime: time.Now().Format(time.DateTime),
Port: settings.NginxSettings.StubStatusPort,
ServerName: "localhost",
StatusPath: StubStatusPath,
AllowIP: StubStatusAllow,
DenyAccess: StubStatusDeny,
}
tmpl, err := template.New("stub_status").Parse(stubStatusTemplate)
if err != nil {
return errors.Wrap(err, "failed to parse template")
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return errors.Wrap(err, "failed to execute template")
}
stubStatusConfig := buf.String()
ngxConfig, err := ParseNgxConfigByContent(stubStatusConfig) ngxConfig, err := ParseNgxConfigByContent(stubStatusConfig)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to parse new nginx config") return errors.Wrap(err, "failed to parse new nginx config")

View file

@ -10,6 +10,14 @@ type Nginx struct {
TestConfigCmd string `json:"test_config_cmd" protected:"true"` TestConfigCmd string `json:"test_config_cmd" protected:"true"`
ReloadCmd string `json:"reload_cmd" protected:"true"` ReloadCmd string `json:"reload_cmd" protected:"true"`
RestartCmd string `json:"restart_cmd" protected:"true"` RestartCmd string `json:"restart_cmd" protected:"true"`
StubStatusPort uint `json:"stub_status_port" binding:"omitempty,min=1,max=65535"`
} }
var NginxSettings = &Nginx{} var NginxSettings = &Nginx{}
func (n *Nginx) GetStubStatusPort() uint {
if n.StubStatusPort == 0 {
return 51820
}
return n.StubStatusPort
}