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

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

View file

@ -0,0 +1,48 @@
package nginx
import (
"os/exec"
"regexp"
"runtime"
"strconv"
"github.com/pkg/errors"
)
type NginxConfigInfo struct {
WorkerProcesses int `json:"worker_processes"`
WorkerConnections int `json:"worker_connections"`
}
// GetNginxWorkerConfigInfo Get Nginx config info of worker_processes and worker_connections
func GetNginxWorkerConfigInfo() (*NginxConfigInfo, error) {
result := &NginxConfigInfo{
WorkerProcesses: 1,
WorkerConnections: 1024,
}
// Get worker_processes config
cmd := exec.Command("nginx", "-T")
output, err := cmd.CombinedOutput()
if err != nil {
return result, errors.Wrap(err, "failed to get nginx config")
}
// Parse worker_processes
wpRe := regexp.MustCompile(`worker_processes\s+(\d+|auto);`)
if matches := wpRe.FindStringSubmatch(string(output)); len(matches) > 1 {
if matches[1] == "auto" {
result.WorkerProcesses = runtime.NumCPU()
} else {
result.WorkerProcesses, _ = strconv.Atoi(matches[1])
}
}
// Parse worker_connections
wcRe := regexp.MustCompile(`worker_connections\s+(\d+);`)
if matches := wcRe.FindStringSubmatch(string(output)); len(matches) > 1 {
result.WorkerConnections, _ = strconv.Atoi(matches[1])
}
return result, nil
}

View file

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

View file

@ -0,0 +1,55 @@
package nginx
import "github.com/uozi-tech/cosy/logger"
type NginxPerformanceInfo struct {
StubStatusData
NginxProcessInfo
NginxConfigInfo
}
type NginxPerformanceResponse struct {
StubStatusEnabled bool `json:"stub_status_enabled"`
Running bool `json:"running"`
Info NginxPerformanceInfo `json:"info"`
}
func GetPerformanceData() NginxPerformanceResponse {
// Check if Nginx is running
running := IsNginxRunning()
if !running {
return NginxPerformanceResponse{
StubStatusEnabled: false,
Running: false,
Info: NginxPerformanceInfo{},
}
}
// Get Nginx status information
stubStatusEnabled, statusInfo, err := GetStubStatusData()
if err != nil {
logger.Warn("Failed to get Nginx status:", err)
}
// Get Nginx process information
processInfo, err := GetNginxProcessInfo()
if err != nil {
logger.Warn("Failed to get Nginx process info:", err)
}
// Get Nginx config information
configInfo, err := GetNginxWorkerConfigInfo()
if err != nil {
logger.Warn("Failed to get Nginx config info:", err)
}
return NginxPerformanceResponse{
StubStatusEnabled: stubStatusEnabled,
Running: running,
Info: NginxPerformanceInfo{
StubStatusData: *statusInfo,
NginxProcessInfo: *processInfo,
NginxConfigInfo: *configInfo,
},
}
}

View file

@ -0,0 +1,178 @@
package nginx
import (
"fmt"
"math"
"runtime"
"strings"
"time"
"github.com/shirou/gopsutil/v4/process"
)
type NginxProcessInfo struct {
Workers int `json:"workers"`
Master int `json:"master"`
Cache int `json:"cache"`
Other int `json:"other"`
CPUUsage float64 `json:"cpu_usage"`
MemoryUsage float64 `json:"memory_usage"`
}
// GetNginxProcessInfo Get Nginx process information
func GetNginxProcessInfo() (*NginxProcessInfo, error) {
result := &NginxProcessInfo{
Workers: 0,
Master: 0,
Cache: 0,
Other: 0,
CPUUsage: 0.0,
MemoryUsage: 0.0,
}
// Find all Nginx processes
processes, err := process.Processes()
if err != nil {
return result, fmt.Errorf("failed to get processes: %v", err)
}
totalMemory := 0.0
workerCount := 0
masterCount := 0
cacheCount := 0
otherCount := 0
nginxProcesses := []*process.Process{}
// Get the number of system CPU cores
numCPU := runtime.NumCPU()
// Get the PID of the Nginx master process
var masterPID int32 = -1
for _, p := range processes {
name, err := p.Name()
if err != nil {
continue
}
cmdline, err := p.Cmdline()
if err != nil {
continue
}
// Check if it is the Nginx master process
if strings.Contains(strings.ToLower(name), "nginx") &&
(strings.Contains(cmdline, "master process") ||
!strings.Contains(cmdline, "worker process")) &&
p.Pid > 0 {
masterPID = p.Pid
masterCount++
nginxProcesses = append(nginxProcesses, p)
// Get the memory usage
mem, err := p.MemoryInfo()
if err == nil && mem != nil {
// Convert to MB
memoryUsage := float64(mem.RSS) / 1024 / 1024
totalMemory += memoryUsage
}
break
}
}
// Iterate through all processes, distinguishing between worker processes and other Nginx processes
for _, p := range processes {
if p.Pid == masterPID {
continue // Already calculated the master process
}
name, err := p.Name()
if err != nil {
continue
}
// Only process Nginx related processes
if !strings.Contains(strings.ToLower(name), "nginx") {
continue
}
// Add to the Nginx process list
nginxProcesses = append(nginxProcesses, p)
// Get the parent process PID
ppid, err := p.Ppid()
if err != nil {
continue
}
cmdline, err := p.Cmdline()
if err != nil {
continue
}
// Get the memory usage
mem, err := p.MemoryInfo()
if err == nil && mem != nil {
// Convert to MB
memoryUsage := float64(mem.RSS) / 1024 / 1024
totalMemory += memoryUsage
}
// Distinguish between worker processes, cache processes, and other processes
if ppid == masterPID || strings.Contains(cmdline, "worker process") {
workerCount++
} else if strings.Contains(cmdline, "cache") {
cacheCount++
} else {
otherCount++
}
}
// Calculate the CPU usage
// First, measure the initial CPU time
times1 := make(map[int32]float64)
for _, p := range nginxProcesses {
times, err := p.Times()
if err == nil {
// CPU time = user time + system time
times1[p.Pid] = times.User + times.System
}
}
// Wait for a short period of time
time.Sleep(100 * time.Millisecond)
// Measure the CPU time again
totalCPUPercent := 0.0
for _, p := range nginxProcesses {
times, err := p.Times()
if err != nil {
continue
}
// Calculate the CPU time difference
currentTotal := times.User + times.System
if previousTotal, ok := times1[p.Pid]; ok {
// Calculate the CPU usage percentage during this period (considering multiple cores)
cpuDelta := currentTotal - previousTotal
// Calculate the CPU usage per second (considering the sampling time)
cpuPercent := (cpuDelta / 0.1) * 100.0 / float64(numCPU)
totalCPUPercent += cpuPercent
}
}
// Round to the nearest integer, which is more consistent with the top display
totalCPUPercent = math.Round(totalCPUPercent)
// Round the memory usage to two decimal places
totalMemory = math.Round(totalMemory*100) / 100
result.Workers = workerCount
result.Master = masterCount
result.Cache = cacheCount
result.Other = otherCount
result.CPUUsage = totalCPUPercent
result.MemoryUsage = totalMemory
return result, nil
}

View file

@ -0,0 +1,199 @@
package nginx
import (
"fmt"
"io"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
)
// StubStatusInfo Store the stub_status module status
type StubStatusInfo struct {
Enabled bool `json:"stub_status_enabled"` // stub_status module is enabled
URL string `json:"stub_status_url"` // stub_status access address
}
type StubStatusData struct {
Active int `json:"active"`
Accepts int `json:"accepts"`
Handled int `json:"handled"`
Requests int `json:"requests"`
Reading int `json:"reading"`
Writing int `json:"writing"`
Waiting int `json:"waiting"`
}
const (
StubStatusPort = 51828
StubStatusPath = "/stub_status"
StubStatusHost = "localhost"
StubStatusProtocol = "http"
StubStatusAllow = "127.0.0.1"
StubStatusDeny = "all"
StubStatusConfigName = "stub_status_nginx-ui.conf"
)
// GetStubStatusData Get the stub_status module data
func GetStubStatusData() (bool, *StubStatusData, error) {
result := &StubStatusData{
Active: 0,
Accepts: 0,
Handled: 0,
Requests: 0,
Reading: 0,
Writing: 0,
Waiting: 0,
}
// Get the stub_status status information
enabled, statusURL := IsStubStatusEnabled()
if !enabled {
return false, result, fmt.Errorf("stub_status is not enabled")
}
// Create an HTTP client
client := &http.Client{
Timeout: 5 * time.Second,
}
// Send a request to get the stub_status data
resp, err := client.Get(statusURL)
if err != nil {
return enabled, result, fmt.Errorf("failed to get stub status: %v", err)
}
defer resp.Body.Close()
// Read the response content
body, err := io.ReadAll(resp.Body)
if err != nil {
return enabled, result, fmt.Errorf("failed to read response body: %v", err)
}
// Parse the response content
statusContent := string(body)
// Match the active connection number
activeRe := regexp.MustCompile(`Active connections:\s+(\d+)`)
if matches := activeRe.FindStringSubmatch(statusContent); len(matches) > 1 {
result.Active, _ = strconv.Atoi(matches[1])
}
// Match the request statistics information
serverRe := regexp.MustCompile(`(\d+)\s+(\d+)\s+(\d+)`)
if matches := serverRe.FindStringSubmatch(statusContent); len(matches) > 3 {
result.Accepts, _ = strconv.Atoi(matches[1])
result.Handled, _ = strconv.Atoi(matches[2])
result.Requests, _ = strconv.Atoi(matches[3])
}
// Match the read and write waiting numbers
connRe := regexp.MustCompile(`Reading:\s+(\d+)\s+Writing:\s+(\d+)\s+Waiting:\s+(\d+)`)
if matches := connRe.FindStringSubmatch(statusContent); len(matches) > 3 {
result.Reading, _ = strconv.Atoi(matches[1])
result.Writing, _ = strconv.Atoi(matches[2])
result.Waiting, _ = strconv.Atoi(matches[3])
}
return enabled, result, nil
}
// GetStubStatus Get the stub_status module status
func GetStubStatus() *StubStatusInfo {
enabled, statusURL := IsStubStatusEnabled()
return &StubStatusInfo{
Enabled: enabled,
URL: statusURL,
}
}
// IsStubStatusEnabled Check if the stub_status module is enabled and return the access address
// Only check the stub_status_nginx-ui.conf configuration file
func IsStubStatusEnabled() (bool, string) {
stubStatusConfPath := GetConfPath("conf.d", StubStatusConfigName)
if _, err := os.Stat(stubStatusConfPath); os.IsNotExist(err) {
return false, ""
}
ngxConfig, err := ParseNgxConfig(stubStatusConfPath)
if err != nil {
return false, ""
}
// Find the stub_status configuration
for _, server := range ngxConfig.Servers {
protocol := StubStatusProtocol
host := StubStatusHost
port := strconv.Itoa(StubStatusPort)
for _, location := range server.Locations {
// Check if the location content contains stub_status
if strings.Contains(location.Content, "stub_status") {
stubStatusURL := fmt.Sprintf("%s://%s:%s%s", protocol, host, port, StubStatusPath)
return true, stubStatusURL
}
}
}
return false, ""
}
// EnableStubStatus Enable stub_status module
func EnableStubStatus() error {
enabled, _ := IsStubStatusEnabled()
if enabled {
return nil
}
return CreateStubStatusConfig()
}
// DisableStubStatus Disable stub_status module
func DisableStubStatus() error {
stubStatusConfPath := GetConfPath("conf.d", StubStatusConfigName)
if _, err := os.Stat(stubStatusConfPath); os.IsNotExist(err) {
return nil
}
return os.Remove(stubStatusConfPath)
}
// CreateStubStatusConfig Create a new stub_status configuration file
func CreateStubStatusConfig() error {
httpConfPath := GetConfPath("conf.d", StubStatusConfigName)
stubStatusConfig := `
# DO NOT EDIT THIS FILE, IT IS AUTO GENERATED BY NGINX-UI
# Nginx stub_status configuration for Nginx-UI
# Modified at ` + time.Now().Format("2006-01-02 15:04:05") + `
server {
listen 51828; # Use non-standard port to avoid conflicts
server_name localhost;
# Status monitoring interface
location /stub_status {
stub_status;
allow 127.0.0.1; # Only allow local access
deny all;
}
}
`
ngxConfig, err := ParseNgxConfigByContent(stubStatusConfig)
if err != nil {
return errors.Wrap(err, "failed to parse new nginx config")
}
ngxConfig.FileName = httpConfPath
configText, err := ngxConfig.BuildConfig()
if err != nil {
return errors.Wrap(err, "failed to build nginx config")
}
return os.WriteFile(httpConfPath, []byte(configText), 0644)
}