瀏覽代碼

feat(nginx): performance optimization #850

Jacky 3 周之前
父節點
當前提交
9d4070a211

+ 2 - 2
.cursor/rules/backend.mdc

@@ -1,6 +1,6 @@
 ---
-description: backend
-globs: 
+description: 
+globs: **/**/*.go
 alwaysApply: false
 ---
 # Cursor Rules

+ 2 - 2
.cursor/rules/frontend.mdc

@@ -1,6 +1,6 @@
 ---
-description: frontend
-globs: 
+description: 
+globs: app/**/*.tsx,app/**/*.vue,app/**/*.ts,app/**/*.js,app/**/*.json
 alwaysApply: false
 ---
 You are an expert in TypeScript, Node.js, Vite, Vue.js, Vue Router, Pinia, VueUse, Ant Design Vue, and UnoCSS, with a deep understanding of best practices and performance optimization techniques in these technologies.

+ 36 - 0
api/nginx/performance.go

@@ -0,0 +1,36 @@
+package nginx
+
+import (
+	"net/http"
+
+	"github.com/0xJacky/Nginx-UI/internal/nginx"
+	"github.com/gin-gonic/gin"
+	"github.com/uozi-tech/cosy"
+)
+
+// GetPerformanceSettings retrieves current Nginx performance settings
+func GetPerformanceSettings(c *gin.Context) {
+	// Get Nginx worker configuration info
+	perfInfo, err := nginx.GetNginxWorkerConfigInfo()
+	if err != nil {
+		cosy.ErrHandler(c, err)
+		return
+	}
+	c.JSON(http.StatusOK, perfInfo)
+}
+
+// UpdatePerformanceSettings updates Nginx performance settings
+func UpdatePerformanceSettings(c *gin.Context) {
+	var perfOpt nginx.PerfOpt
+	if !cosy.BindAndValid(c, &perfOpt) {
+		return
+	}
+
+	err := nginx.UpdatePerfOpt(&perfOpt)
+	if err != nil {
+		cosy.ErrHandler(c, err)
+		return
+	}
+
+	GetPerformanceSettings(c)
+}

+ 4 - 0
api/nginx/router.go

@@ -23,4 +23,8 @@ func InitRouter(r *gin.RouterGroup) {
 	r.POST("nginx/stub_status", ToggleStubStatus)
 	r.POST("nginx_log", nginx_log.GetNginxLogPage)
 	r.GET("nginx/directives", GetDirectives)
+
+	// Performance optimization endpoints
+	r.GET("nginx/performance", GetPerformanceSettings)
+	r.POST("nginx/performance", UpdatePerformanceSettings)
 }

+ 0 - 1
app/components.d.ts

@@ -71,7 +71,6 @@ declare module 'vue' {
     ATag: typeof import('ant-design-vue/es')['Tag']
     ATextarea: typeof import('ant-design-vue/es')['Textarea']
     ATooltip: typeof import('ant-design-vue/es')['Tooltip']
-    AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
     BreadcrumbBreadcrumb: typeof import('./src/components/Breadcrumb/Breadcrumb.vue')['default']
     ChartAreaChart: typeof import('./src/components/Chart/AreaChart.vue')['default']
     ChartRadialBarChart: typeof import('./src/components/Chart/RadialBarChart.vue')['default']

+ 35 - 0
app/src/api/ngx.ts

@@ -54,6 +54,33 @@ export interface NginxPerformanceInfo {
   process_mode: string // worker进程配置模式:'auto'或'manual'
 }
 
+export interface NginxConfigInfo {
+  worker_processes: number
+  worker_connections: number
+  process_mode: string
+  keepalive_timeout: number
+  gzip: string
+  gzip_min_length: number
+  gzip_comp_level: number
+  client_max_body_size: string
+  server_names_hash_bucket_size: number
+  client_header_buffer_size: string
+  client_body_buffer_size: string
+}
+
+export interface NginxPerfOpt {
+  worker_processes: string
+  worker_connections: string
+  keepalive_timeout: string
+  gzip: string
+  gzip_min_length: string
+  gzip_comp_level: string
+  client_max_body_size: string
+  server_names_hash_bucket_size: string
+  client_header_buffer_size: string
+  client_body_buffer_size: string
+}
+
 const ngx = {
   build_config(ngxConfig: NgxConfig) {
     return http.post('/ngx/build_config', ngxConfig)
@@ -94,6 +121,14 @@ const ngx = {
   get_directives(): Promise<DirectiveMap> {
     return http.get('/nginx/directives')
   },
+
+  get_performance(): Promise<NginxConfigInfo> {
+    return http.get('/nginx/performance')
+  },
+
+  update_performance(params: NginxPerfOpt): Promise<NginxConfigInfo> {
+    return http.post('/nginx/performance', params)
+  },
 }
 
 export default ngx

+ 66 - 66
app/src/components/Notification/notifications.ts

@@ -4,72 +4,6 @@
 
 const notifications: Record<string, { title: () => string, content: (args: any) => string }> = {
 
-  // cluster module notifications
-  'Reload Remote Nginx Error': {
-    title: () => $gettext('Reload Remote Nginx Error'),
-    content: (args: any) => $gettext('Reload Nginx on %{node} failed, response: %{resp}', args),
-  },
-  'Reload Remote Nginx Success': {
-    title: () => $gettext('Reload Remote Nginx Success'),
-    content: (args: any) => $gettext('Reload Nginx on %{node} successfully', args),
-  },
-  'Restart Remote Nginx Error': {
-    title: () => $gettext('Restart Remote Nginx Error'),
-    content: (args: any) => $gettext('Restart Nginx on %{node} failed, response: %{resp}', args),
-  },
-  'Restart Remote Nginx Success': {
-    title: () => $gettext('Restart Remote Nginx Success'),
-    content: (args: any) => $gettext('Restart Nginx on %{node} successfully', args),
-  },
-
-  // cert module notifications
-  'Certificate Expired': {
-    title: () => $gettext('Certificate Expired'),
-    content: (args: any) => $gettext('Certificate %{name} has expired', args),
-  },
-  'Certificate Expiration Notice': {
-    title: () => $gettext('Certificate Expiration Notice'),
-    content: (args: any) => $gettext('Certificate %{name} will expire in %{days} days', args),
-  },
-  'Certificate Expiring Soon': {
-    title: () => $gettext('Certificate Expiring Soon'),
-    content: (args: any) => $gettext('Certificate %{name} will expire in %{days} days', args),
-  },
-  'Certificate Expiring Soon_1': {
-    title: () => $gettext('Certificate Expiring Soon'),
-    content: (args: any) => $gettext('Certificate %{name} will expire in %{days} days', args),
-  },
-  'Certificate Expiring Soon_2': {
-    title: () => $gettext('Certificate Expiring Soon'),
-    content: (args: any) => $gettext('Certificate %{name} will expire in 1 day', args),
-  },
-  'Sync Certificate Error': {
-    title: () => $gettext('Sync Certificate Error'),
-    content: (args: any) => $gettext('Sync Certificate %{cert_name} to %{env_name} failed', args),
-  },
-  'Sync Certificate Success': {
-    title: () => $gettext('Sync Certificate Success'),
-    content: (args: any) => $gettext('Sync Certificate %{cert_name} to %{env_name} successfully', args),
-  },
-
-  // config module notifications
-  'Sync Config Error': {
-    title: () => $gettext('Sync Config Error'),
-    content: (args: any) => $gettext('Sync config %{config_name} to %{env_name} failed', args),
-  },
-  'Sync Config Success': {
-    title: () => $gettext('Sync Config Success'),
-    content: (args: any) => $gettext('Sync config %{config_name} to %{env_name} successfully', args),
-  },
-  'Rename Remote Config Error': {
-    title: () => $gettext('Rename Remote Config Error'),
-    content: (args: any) => $gettext('Rename %{orig_path} to %{new_path} on %{env_name} failed', args),
-  },
-  'Rename Remote Config Success': {
-    title: () => $gettext('Rename Remote Config Success'),
-    content: (args: any) => $gettext('Rename %{orig_path} to %{new_path} on %{env_name} successfully', args),
-  },
-
   // site module notifications
   'Delete Remote Site Error': {
     title: () => $gettext('Delete Remote Site Error'),
@@ -175,6 +109,72 @@ const notifications: Record<string, { title: () => string, content: (args: any)
     title: () => $gettext('All Recovery Codes Have Been Used'),
     content: (args: any) => $gettext('Please generate new recovery codes in the preferences immediately to prevent lockout.', args),
   },
+
+  // cluster module notifications
+  'Reload Remote Nginx Error': {
+    title: () => $gettext('Reload Remote Nginx Error'),
+    content: (args: any) => $gettext('Reload Nginx on %{node} failed, response: %{resp}', args),
+  },
+  'Reload Remote Nginx Success': {
+    title: () => $gettext('Reload Remote Nginx Success'),
+    content: (args: any) => $gettext('Reload Nginx on %{node} successfully', args),
+  },
+  'Restart Remote Nginx Error': {
+    title: () => $gettext('Restart Remote Nginx Error'),
+    content: (args: any) => $gettext('Restart Nginx on %{node} failed, response: %{resp}', args),
+  },
+  'Restart Remote Nginx Success': {
+    title: () => $gettext('Restart Remote Nginx Success'),
+    content: (args: any) => $gettext('Restart Nginx on %{node} successfully', args),
+  },
+
+  // cert module notifications
+  'Certificate Expired': {
+    title: () => $gettext('Certificate Expired'),
+    content: (args: any) => $gettext('Certificate %{name} has expired', args),
+  },
+  'Certificate Expiration Notice': {
+    title: () => $gettext('Certificate Expiration Notice'),
+    content: (args: any) => $gettext('Certificate %{name} will expire in %{days} days', args),
+  },
+  'Certificate Expiring Soon': {
+    title: () => $gettext('Certificate Expiring Soon'),
+    content: (args: any) => $gettext('Certificate %{name} will expire in %{days} days', args),
+  },
+  'Certificate Expiring Soon_1': {
+    title: () => $gettext('Certificate Expiring Soon'),
+    content: (args: any) => $gettext('Certificate %{name} will expire in %{days} days', args),
+  },
+  'Certificate Expiring Soon_2': {
+    title: () => $gettext('Certificate Expiring Soon'),
+    content: (args: any) => $gettext('Certificate %{name} will expire in 1 day', args),
+  },
+  'Sync Certificate Error': {
+    title: () => $gettext('Sync Certificate Error'),
+    content: (args: any) => $gettext('Sync Certificate %{cert_name} to %{env_name} failed', args),
+  },
+  'Sync Certificate Success': {
+    title: () => $gettext('Sync Certificate Success'),
+    content: (args: any) => $gettext('Sync Certificate %{cert_name} to %{env_name} successfully', args),
+  },
+
+  // config module notifications
+  'Sync Config Error': {
+    title: () => $gettext('Sync Config Error'),
+    content: (args: any) => $gettext('Sync config %{config_name} to %{env_name} failed', args),
+  },
+  'Sync Config Success': {
+    title: () => $gettext('Sync Config Success'),
+    content: (args: any) => $gettext('Sync config %{config_name} to %{env_name} successfully', args),
+  },
+  'Rename Remote Config Error': {
+    title: () => $gettext('Rename Remote Config Error'),
+    content: (args: any) => $gettext('Rename %{orig_path} to %{new_path} on %{env_name} failed', args),
+  },
+  'Rename Remote Config Success': {
+    title: () => $gettext('Rename Remote Config Success'),
+    content: (args: any) => $gettext('Rename %{orig_path} to %{new_path} on %{env_name} successfully', args),
+  },
 }
 
 export default notifications

+ 1 - 0
app/src/constants/errors/self_check.ts

@@ -11,4 +11,5 @@ export default {
   4047: () => $gettext('Sites-enabled directory not exist'),
   4048: () => $gettext('Streams-available directory not exist'),
   4049: () => $gettext('Streams-enabled directory not exist'),
+  4050: () => $gettext('Nginx conf not include conf.d directory'),
 }

文件差異過大導致無法顯示
+ 232 - 117
app/src/language/ar/app.po


文件差異過大導致無法顯示
+ 232 - 117
app/src/language/de_DE/app.po


文件差異過大導致無法顯示
+ 231 - 117
app/src/language/en/app.po


文件差異過大導致無法顯示
+ 232 - 117
app/src/language/es/app.po


文件差異過大導致無法顯示
+ 231 - 117
app/src/language/fr_FR/app.po


文件差異過大導致無法顯示
+ 232 - 117
app/src/language/ko_KR/app.po


文件差異過大導致無法顯示
+ 230 - 121
app/src/language/messages.pot


文件差異過大導致無法顯示
+ 232 - 117
app/src/language/ru_RU/app.po


文件差異過大導致無法顯示
+ 232 - 117
app/src/language/tr_TR/app.po


文件差異過大導致無法顯示
+ 232 - 117
app/src/language/vi_VN/app.po


文件差異過大導致無法顯示
+ 229 - 120
app/src/language/zh_CN/app.po


文件差異過大導致無法顯示
+ 238 - 123
app/src/language/zh_TW/app.po


+ 6 - 0
app/src/views/config/ConfigEditor.vue

@@ -61,6 +61,9 @@ const newPath = computed(() => {
 const relativePath = computed(() => (basePath.value ? `${basePath.value}/${route.params.name}` : route.params.name) as string)
 const breadcrumbs = useBreadcrumbs()
 
+// Use Vue 3.4+ useTemplateRef for InspectConfig component
+const inspectConfigRef = useTemplateRef<InstanceType<typeof InspectConfig>>('inspectConfig')
+
 async function init() {
   const { name } = route.params
 
@@ -200,6 +203,8 @@ function save() {
       }
       else {
         data.value = r
+        // Run test after saving to verify configuration
+        inspectConfigRef.value?.test()
       }
     })
   })
@@ -254,6 +259,7 @@ function openHistory() {
 
         <InspectConfig
           v-show="!addMode"
+          ref="inspectConfig"
         />
         <CodeEditor v-model:content="data.content" />
         <FooterToolBar>

+ 4 - 0
app/src/views/dashboard/NginxDashBoard.vue

@@ -7,6 +7,7 @@ import { useUserStore } from '@/pinia'
 import { useGlobalStore } from '@/pinia/moudule/global'
 import { ClockCircleOutlined, ReloadOutlined } from '@ant-design/icons-vue'
 import ConnectionMetricsCard from './components/ConnectionMetricsCard.vue'
+import PerformanceOptimization from './components/PerformanceOptimization.vue'
 import PerformanceStatisticsCard from './components/PerformanceStatisticsCard.vue'
 import PerformanceTablesCard from './components/PerformanceTablesCard.vue'
 import ProcessDistributionCard from './components/ProcessDistributionCard.vue'
@@ -182,6 +183,9 @@ onMounted(() => {
       <div v-if="nginxInfo" class="performance-dashboard">
         <!-- Top performance metrics card -->
         <ACard class="mb-4" :title="$gettext('Performance Metrics')" :bordered="false">
+          <template #extra>
+            <PerformanceOptimization />
+          </template>
           <PerformanceStatisticsCard :nginx-info="nginxInfo" />
         </ACard>
 

+ 0 - 1
app/src/views/dashboard/components/ConnectionMetricsCard.vue

@@ -1,6 +1,5 @@
 <script setup lang="ts">
 import type { NginxPerformanceInfo } from '@/api/ngx'
-import { computed, defineProps } from 'vue'
 
 const props = defineProps<{
   nginxInfo: NginxPerformanceInfo

+ 327 - 0
app/src/views/dashboard/components/PerformanceOptimization.vue

@@ -0,0 +1,327 @@
+<script setup lang="ts">
+import type { NginxConfigInfo, NginxPerfOpt } from '@/api/ngx'
+import type { CheckedType } from '@/types'
+import ngx from '@/api/ngx'
+import {
+  SettingOutlined,
+} from '@ant-design/icons-vue'
+import { message } from 'ant-design-vue'
+
+// Size units
+const sizeUnits = ['k', 'm', 'g']
+
+// Size values and units
+const maxBodySizeValue = ref<number>(1)
+const maxBodySizeUnit = ref<string>('m')
+const headerBufferSizeValue = ref<number>(1)
+const headerBufferSizeUnit = ref<string>('k')
+const bodyBufferSizeValue = ref<number>(8)
+const bodyBufferSizeUnit = ref<string>('k')
+
+// Performance settings modal
+const visible = ref(false)
+const loading = ref(false)
+const performanceConfig = ref<NginxConfigInfo>({
+  worker_processes: 1,
+  worker_connections: 1024,
+  process_mode: 'manual',
+  keepalive_timeout: 65,
+  gzip: 'off',
+  gzip_min_length: 1,
+  gzip_comp_level: 1,
+  client_max_body_size: '1m',
+  server_names_hash_bucket_size: 32,
+  client_header_buffer_size: '1k',
+  client_body_buffer_size: '8k',
+})
+
+// Open modal and load performance settings
+async function openPerformanceModal() {
+  visible.value = true
+  await fetchPerformanceSettings()
+}
+
+// Load performance settings
+async function fetchPerformanceSettings() {
+  loading.value = true
+  try {
+    const data = await ngx.get_performance()
+    performanceConfig.value = data
+
+    // Parse size values and units
+    parseSizeValues()
+  }
+  catch (error) {
+    console.error('Failed to get Nginx performance settings:', error)
+    message.error($gettext('Failed to get Nginx performance settings'))
+  }
+  finally {
+    loading.value = false
+  }
+}
+
+// Parse size values from config
+function parseSizeValues() {
+  // Parse client_max_body_size
+  const maxBodySize = performanceConfig.value.client_max_body_size
+  const maxBodyMatch = maxBodySize.match(/^(\d+)([kmg])?$/i)
+  if (maxBodyMatch) {
+    maxBodySizeValue.value = Number.parseInt(maxBodyMatch[1])
+    maxBodySizeUnit.value = (maxBodyMatch[2] || 'm').toLowerCase()
+  }
+
+  // Parse client_header_buffer_size
+  const headerSize = performanceConfig.value.client_header_buffer_size
+  const headerMatch = headerSize.match(/^(\d+)([kmg])?$/i)
+  if (headerMatch) {
+    headerBufferSizeValue.value = Number.parseInt(headerMatch[1])
+    headerBufferSizeUnit.value = (headerMatch[2] || 'k').toLowerCase()
+  }
+
+  // Parse client_body_buffer_size
+  const bodySize = performanceConfig.value.client_body_buffer_size
+  const bodyMatch = bodySize.match(/^(\d+)([kmg])?$/i)
+  if (bodyMatch) {
+    bodyBufferSizeValue.value = Number.parseInt(bodyMatch[1])
+    bodyBufferSizeUnit.value = (bodyMatch[2] || 'k').toLowerCase()
+  }
+}
+
+// Format size values before saving
+function formatSizeValues() {
+  performanceConfig.value.client_max_body_size = `${maxBodySizeValue.value}${maxBodySizeUnit.value}`
+  performanceConfig.value.client_header_buffer_size = `${headerBufferSizeValue.value}${headerBufferSizeUnit.value}`
+  performanceConfig.value.client_body_buffer_size = `${bodyBufferSizeValue.value}${bodyBufferSizeUnit.value}`
+}
+
+// Save performance settings
+async function savePerformanceSettings() {
+  loading.value = true
+  try {
+    // Format size values
+    formatSizeValues()
+
+    const params: NginxPerfOpt = {
+      worker_processes: performanceConfig.value.process_mode === 'auto' ? 'auto' : performanceConfig.value.worker_processes.toString(),
+      worker_connections: performanceConfig.value.worker_connections.toString(),
+      keepalive_timeout: performanceConfig.value.keepalive_timeout.toString(),
+      gzip: performanceConfig.value.gzip,
+      gzip_min_length: performanceConfig.value.gzip_min_length.toString(),
+      gzip_comp_level: performanceConfig.value.gzip_comp_level.toString(),
+      client_max_body_size: performanceConfig.value.client_max_body_size,
+      server_names_hash_bucket_size: performanceConfig.value.server_names_hash_bucket_size.toString(),
+      client_header_buffer_size: performanceConfig.value.client_header_buffer_size,
+      client_body_buffer_size: performanceConfig.value.client_body_buffer_size,
+    }
+    const data = await ngx.update_performance(params)
+    performanceConfig.value = data
+
+    // Parse the returned values
+    parseSizeValues()
+
+    message.success($gettext('Performance settings saved successfully'))
+  }
+  catch (error) {
+    console.error('Failed to save Nginx performance settings:', error)
+    message.error($gettext('Failed to save Nginx performance settings'))
+  }
+  finally {
+    loading.value = false
+  }
+}
+
+// Toggle worker process mode
+function handleProcessModeChange(checked: CheckedType) {
+  performanceConfig.value.process_mode = checked ? 'auto' : 'manual'
+  if (checked) {
+    performanceConfig.value.worker_processes = navigator.hardwareConcurrency || 4
+  }
+}
+
+// Toggle GZIP compression
+function handleGzipChange(checked: CheckedType) {
+  performanceConfig.value.gzip = checked ? 'on' : 'off'
+}
+</script>
+
+<template>
+  <div>
+    <!-- Performance Optimization Button -->
+    <AButton
+      type="link"
+      size="small"
+      @click="openPerformanceModal"
+    >
+      <template #icon>
+        <SettingOutlined />
+      </template>
+      {{ $gettext('Optimize Performance') }}
+    </AButton>
+
+    <!-- Performance Optimization Modal -->
+    <AModal
+      v-model:open="visible"
+      :title="$gettext('Optimize Nginx Performance')"
+      :mask-closable="false"
+      :ok-button-props="{ loading }"
+      @ok="savePerformanceSettings"
+    >
+      <ASpin :spinning="loading">
+        <AForm layout="vertical">
+          <AFormItem
+            :label="$gettext('Worker Processes')"
+            :help="$gettext('Number of concurrent worker processes, auto sets to CPU core count')"
+          >
+            <ASpace>
+              <ASwitch
+                :checked="performanceConfig.process_mode === 'auto'"
+                :checked-children="$gettext('Auto')"
+                :un-checked-children="$gettext('Manual')"
+                @change="handleProcessModeChange"
+              />
+              <AInputNumber
+                v-if="performanceConfig.process_mode !== 'auto'"
+                v-model:value="performanceConfig.worker_processes"
+                :min="1"
+                :max="32"
+                style="width: 120px"
+              />
+              <span v-else>{{ performanceConfig.worker_processes }} ({{ $gettext('Auto') }})</span>
+            </ASpace>
+          </AFormItem>
+
+          <AFormItem
+            :label="$gettext('Worker Connections')"
+            :help="$gettext('Maximum number of concurrent connections')"
+          >
+            <AInputNumber
+              v-model:value="performanceConfig.worker_connections"
+              :min="512"
+              :max="65536"
+              style="width: 120px"
+            />
+          </AFormItem>
+
+          <AFormItem
+            :label="$gettext('Keepalive Timeout')"
+            :help="$gettext('Connection timeout period')"
+          >
+            <ASpace>
+              <AInputNumber
+                v-model:value="performanceConfig.keepalive_timeout"
+                :min="0"
+                :max="999"
+                style="width: 120px"
+              />
+              <span>{{ $gettext('seconds') }}</span>
+            </ASpace>
+          </AFormItem>
+
+          <AFormItem
+            :label="$gettext('GZIP Compression')"
+            :help="$gettext('Enable compression for content transfer')"
+          >
+            <ASwitch
+              :checked="performanceConfig.gzip === 'on'"
+              :checked-children="$gettext('On')"
+              :un-checked-children="$gettext('Off')"
+              @change="handleGzipChange"
+            />
+          </AFormItem>
+
+          <AFormItem
+            :label="$gettext('GZIP Min Length')"
+            :help="$gettext('Minimum file size for compression')"
+          >
+            <ASpace>
+              <AInputNumber
+                v-model:value="performanceConfig.gzip_min_length"
+                :min="0"
+                style="width: 120px"
+              />
+              <span>{{ $gettext('KB') }}</span>
+            </ASpace>
+          </AFormItem>
+
+          <AFormItem
+            :label="$gettext('GZIP Compression Level')"
+            :help="$gettext('Compression level, 1 is lowest, 9 is highest')"
+          >
+            <AInputNumber
+              v-model:value="performanceConfig.gzip_comp_level"
+              :min="1"
+              :max="9"
+              style="width: 120px"
+            />
+          </AFormItem>
+
+          <AFormItem
+            :label="$gettext('Client Max Body Size')"
+            :help="$gettext('Maximum client request body size')"
+          >
+            <AInputGroup compact style="width: 180px">
+              <AInputNumber
+                v-model:value="maxBodySizeValue"
+                :min="1"
+                style="width: 120px"
+              />
+              <ASelect v-model:value="maxBodySizeUnit" style="width: 60px">
+                <ASelectOption v-for="unit in sizeUnits" :key="unit" :value="unit">
+                  {{ unit.toUpperCase() }}
+                </ASelectOption>
+              </ASelect>
+            </AInputGroup>
+          </AFormItem>
+
+          <AFormItem
+            :label="$gettext('Server Names Hash Bucket Size')"
+            :help="$gettext('Server names hash table size')"
+          >
+            <AInputNumber
+              v-model:value="performanceConfig.server_names_hash_bucket_size"
+              :min="32"
+              :step="32"
+              style="width: 120px"
+            />
+          </AFormItem>
+
+          <AFormItem
+            :label="$gettext('Client Header Buffer Size')"
+            :help="$gettext('Client request header buffer size')"
+          >
+            <AInputGroup compact style="width: 180px">
+              <AInputNumber
+                v-model:value="headerBufferSizeValue"
+                :min="1"
+                style="width: 120px"
+              />
+              <ASelect v-model:value="headerBufferSizeUnit" style="width: 60px">
+                <ASelectOption v-for="unit in sizeUnits" :key="unit" :value="unit">
+                  {{ unit.toUpperCase() }}
+                </ASelectOption>
+              </ASelect>
+            </AInputGroup>
+          </AFormItem>
+
+          <AFormItem
+            :label="$gettext('Client Body Buffer Size')"
+            :help="$gettext('Client request body buffer size')"
+          >
+            <AInputGroup compact style="width: 180px">
+              <AInputNumber
+                v-model:value="bodyBufferSizeValue"
+                :min="1"
+                style="width: 120px"
+              />
+              <ASelect v-model:value="bodyBufferSizeUnit" style="width: 60px">
+                <ASelectOption v-for="unit in sizeUnits" :key="unit" :value="unit">
+                  {{ unit.toUpperCase() }}
+                </ASelectOption>
+              </ASelect>
+            </AInputGroup>
+          </AFormItem>
+        </AForm>
+      </ASpin>
+    </AModal>
+  </div>
+</template>

+ 75 - 74
app/src/views/dashboard/components/PerformanceStatisticsCard.vue

@@ -7,7 +7,6 @@ import {
   InfoCircleOutlined,
   ThunderboltOutlined,
 } from '@ant-design/icons-vue'
-import { computed, defineProps } from 'vue'
 
 const props = defineProps<{
   nginxInfo: NginxPerformanceInfo
@@ -28,80 +27,82 @@ const maxRPS = computed(() => {
 </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>
+  <div>
+    <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>
+      <!-- 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>
+      <!-- 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>
+      <!-- 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>
+  </div>
 </template>

+ 0 - 1
app/src/views/dashboard/components/PerformanceTablesCard.vue

@@ -2,7 +2,6 @@
 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

+ 1 - 1
app/src/views/system/About.vue

@@ -50,7 +50,7 @@ const thisYear = new Date().getFullYear()
     <h3>
       {{ $gettext('Project Team') }}
     </h3>
-    <p><a href="https://jackyu.cn/">@0xJacky</a> <a href="https://blog.kugeek.com/">@Hintay</a></p>
+    <p><a href="https://jackyu.cn/">@0xJacky</a> <a href="https://blog.kugeek.com/">@Hintay</a> <a href="https://github.com/akinoccc">@Akino</a></p>
     <h3>
       {{ $gettext('Build with') }}
     </h3>

+ 83 - 13
internal/nginx/config_info.go

@@ -1,7 +1,7 @@
 package nginx
 
 import (
-	"os/exec"
+	"os"
 	"regexp"
 	"runtime"
 	"strconv"
@@ -10,29 +10,51 @@ import (
 )
 
 type NginxConfigInfo struct {
-	WorkerProcesses   int    `json:"worker_processes"`
-	WorkerConnections int    `json:"worker_connections"`
-	ProcessMode       string `json:"process_mode"`
+	WorkerProcesses           int    `json:"worker_processes"`
+	WorkerConnections         int    `json:"worker_connections"`
+	ProcessMode               string `json:"process_mode"`
+	KeepaliveTimeout          int    `json:"keepalive_timeout"`
+	Gzip                      string `json:"gzip"`
+	GzipMinLength             int    `json:"gzip_min_length"`
+	GzipCompLevel             int    `json:"gzip_comp_level"`
+	ClientMaxBodySize         string `json:"client_max_body_size"` // with unit
+	ServerNamesHashBucketSize int    `json:"server_names_hash_bucket_size"`
+	ClientHeaderBufferSize    string `json:"client_header_buffer_size"` // with unit
+	ClientBodyBufferSize      string `json:"client_body_buffer_size"`   // with unit
 }
 
 // GetNginxWorkerConfigInfo Get Nginx config info of worker_processes and worker_connections
 func GetNginxWorkerConfigInfo() (*NginxConfigInfo, error) {
 	result := &NginxConfigInfo{
-		WorkerProcesses:   1,
-		WorkerConnections: 1024,
-		ProcessMode:       "manual",
+		WorkerProcesses:           1,
+		WorkerConnections:         1024,
+		ProcessMode:               "manual",
+		KeepaliveTimeout:          65,
+		Gzip:                      "off",
+		GzipMinLength:             1,
+		GzipCompLevel:             1,
+		ClientMaxBodySize:         "1m",
+		ServerNamesHashBucketSize: 32,
+		ClientHeaderBufferSize:    "1k",
+		ClientBodyBufferSize:      "8k",
 	}
 
-	// Get worker_processes config
-	cmd := exec.Command("nginx", "-T")
-	output, err := cmd.CombinedOutput()
+	confPath := GetConfPath("nginx.conf")
+	if confPath == "" {
+		return nil, errors.New("failed to get nginx.conf path")
+	}
+
+	// Read the current configuration
+	content, err := os.ReadFile(confPath)
 	if err != nil {
-		return result, errors.Wrap(err, "failed to get nginx config")
+		return nil, errors.Wrap(err, "failed to read nginx.conf")
 	}
 
+	outputStr := string(content)
+
 	// Parse worker_processes
 	wpRe := regexp.MustCompile(`worker_processes\s+(\d+|auto);`)
-	if matches := wpRe.FindStringSubmatch(string(output)); len(matches) > 1 {
+	if matches := wpRe.FindStringSubmatch(outputStr); len(matches) > 1 {
 		if matches[1] == "auto" {
 			result.WorkerProcesses = runtime.NumCPU()
 			result.ProcessMode = "auto"
@@ -44,9 +66,57 @@ func GetNginxWorkerConfigInfo() (*NginxConfigInfo, error) {
 
 	// Parse worker_connections
 	wcRe := regexp.MustCompile(`worker_connections\s+(\d+);`)
-	if matches := wcRe.FindStringSubmatch(string(output)); len(matches) > 1 {
+	if matches := wcRe.FindStringSubmatch(outputStr); len(matches) > 1 {
 		result.WorkerConnections, _ = strconv.Atoi(matches[1])
 	}
 
+	// Parse keepalive_timeout
+	ktRe := regexp.MustCompile(`keepalive_timeout\s+(\d+);`)
+	if matches := ktRe.FindStringSubmatch(outputStr); len(matches) > 1 {
+		result.KeepaliveTimeout, _ = strconv.Atoi(matches[1])
+	}
+
+	// Parse gzip
+	gzipRe := regexp.MustCompile(`gzip\s+(on|off);`)
+	if matches := gzipRe.FindStringSubmatch(outputStr); len(matches) > 1 {
+		result.Gzip = matches[1]
+	}
+
+	// Parse gzip_min_length
+	gzipMinRe := regexp.MustCompile(`gzip_min_length\s+(\d+);`)
+	if matches := gzipMinRe.FindStringSubmatch(outputStr); len(matches) > 1 {
+		result.GzipMinLength, _ = strconv.Atoi(matches[1])
+	}
+
+	// Parse gzip_comp_level
+	gzipCompRe := regexp.MustCompile(`gzip_comp_level\s+(\d+);`)
+	if matches := gzipCompRe.FindStringSubmatch(outputStr); len(matches) > 1 {
+		result.GzipCompLevel, _ = strconv.Atoi(matches[1])
+	}
+
+	// Parse client_max_body_size with any unit (k, m, g)
+	cmaxRe := regexp.MustCompile(`client_max_body_size\s+(\d+[kmg]?);`)
+	if matches := cmaxRe.FindStringSubmatch(outputStr); len(matches) > 1 {
+		result.ClientMaxBodySize = matches[1]
+	}
+
+	// Parse server_names_hash_bucket_size
+	hashRe := regexp.MustCompile(`server_names_hash_bucket_size\s+(\d+);`)
+	if matches := hashRe.FindStringSubmatch(outputStr); len(matches) > 1 {
+		result.ServerNamesHashBucketSize, _ = strconv.Atoi(matches[1])
+	}
+
+	// Parse client_header_buffer_size with any unit (k, m, g)
+	headerRe := regexp.MustCompile(`client_header_buffer_size\s+(\d+[kmg]?);`)
+	if matches := headerRe.FindStringSubmatch(outputStr); len(matches) > 1 {
+		result.ClientHeaderBufferSize = matches[1]
+	}
+
+	// Parse client_body_buffer_size with any unit (k, m, g)
+	bodyRe := regexp.MustCompile(`client_body_buffer_size\s+(\d+[kmg]?);`)
+	if matches := bodyRe.FindStringSubmatch(outputStr); len(matches) > 1 {
+		result.ClientBodyBufferSize = matches[1]
+	}
+
 	return result, nil
 }

+ 148 - 0
internal/nginx/perf_opt.go

@@ -0,0 +1,148 @@
+package nginx
+
+import (
+	"fmt"
+	"os"
+	"sort"
+	"time"
+
+	"github.com/pkg/errors"
+	"github.com/tufanbarisyildirim/gonginx/config"
+	"github.com/tufanbarisyildirim/gonginx/dumper"
+	"github.com/tufanbarisyildirim/gonginx/parser"
+)
+
+// PerfOpt represents Nginx performance optimization settings
+type PerfOpt struct {
+	WorkerProcesses           string `json:"worker_processes"`              // auto or number
+	WorkerConnections         string `json:"worker_connections"`            // max connections
+	KeepaliveTimeout          string `json:"keepalive_timeout"`             // timeout in seconds
+	Gzip                      string `json:"gzip"`                          // on or off
+	GzipMinLength             string `json:"gzip_min_length"`               // min length to compress
+	GzipCompLevel             string `json:"gzip_comp_level"`               // compression level
+	ClientMaxBodySize         string `json:"client_max_body_size"`          // max body size (with unit: k, m, g)
+	ServerNamesHashBucketSize string `json:"server_names_hash_bucket_size"` // hash bucket size
+	ClientHeaderBufferSize    string `json:"client_header_buffer_size"`     // header buffer size (with unit: k, m, g)
+	ClientBodyBufferSize      string `json:"client_body_buffer_size"`       // body buffer size (with unit: k, m, g)
+}
+
+// UpdatePerfOpt updates the Nginx performance optimization settings
+func UpdatePerfOpt(opt *PerfOpt) error {
+	confPath := GetConfPath("nginx.conf")
+	if confPath == "" {
+		return errors.New("failed to get nginx.conf path")
+	}
+
+	// Read the current configuration
+	content, err := os.ReadFile(confPath)
+	if err != nil {
+		return errors.Wrap(err, "failed to read nginx.conf")
+	}
+
+	// Create a backup file
+	backupPath := fmt.Sprintf("%s.backup.%d", confPath, time.Now().Unix())
+	err = os.WriteFile(backupPath, content, 0644)
+	if err != nil {
+		return errors.Wrap(err, "failed to create backup file")
+	}
+
+	// Parse the configuration
+	p := parser.NewStringParser(string(content), parser.WithSkipValidDirectivesErr())
+	conf, err := p.Parse()
+	if err != nil {
+		return errors.Wrap(err, "failed to parse nginx.conf")
+	}
+
+	// Process the configuration and update performance settings
+	updateNginxConfig(conf.Block, opt)
+
+	// Dump the updated configuration
+	updatedConf := dumper.DumpBlock(conf.Block, dumper.IndentedStyle)
+
+	// Write the updated configuration
+	err = os.WriteFile(confPath, []byte(updatedConf), 0644)
+	if err != nil {
+		return errors.Wrap(err, "failed to write updated nginx.conf")
+	}
+
+	return nil
+}
+
+// updateNginxConfig updates the performance settings in the Nginx configuration
+func updateNginxConfig(block config.IBlock, opt *PerfOpt) {
+	if block == nil {
+		return
+	}
+
+	directives := block.GetDirectives()
+	// Update main context directives
+	updateOrAddDirective(block, directives, "worker_processes", opt.WorkerProcesses)
+
+	// Look for events, http, and other blocks
+	for _, directive := range directives {
+		if directive.GetName() == "events" && directive.GetBlock() != nil {
+			// Update events block directives
+			eventsBlock := directive.GetBlock()
+			eventsDirectives := eventsBlock.GetDirectives()
+			updateOrAddDirective(eventsBlock, eventsDirectives, "worker_connections", opt.WorkerConnections)
+		} else if directive.GetName() == "http" && directive.GetBlock() != nil {
+			// Update http block directives
+			httpBlock := directive.GetBlock()
+			httpDirectives := httpBlock.GetDirectives()
+			updateOrAddDirective(httpBlock, httpDirectives, "keepalive_timeout", opt.KeepaliveTimeout)
+			updateOrAddDirective(httpBlock, httpDirectives, "gzip", opt.Gzip)
+			updateOrAddDirective(httpBlock, httpDirectives, "gzip_min_length", opt.GzipMinLength)
+			updateOrAddDirective(httpBlock, httpDirectives, "gzip_comp_level", opt.GzipCompLevel)
+			updateOrAddDirective(httpBlock, httpDirectives, "client_max_body_size", opt.ClientMaxBodySize)
+			updateOrAddDirective(httpBlock, httpDirectives, "server_names_hash_bucket_size", opt.ServerNamesHashBucketSize)
+			updateOrAddDirective(httpBlock, httpDirectives, "client_header_buffer_size", opt.ClientHeaderBufferSize)
+			updateOrAddDirective(httpBlock, httpDirectives, "client_body_buffer_size", opt.ClientBodyBufferSize)
+		}
+	}
+}
+
+// updateOrAddDirective updates a directive if it exists, or adds it to the block if it doesn't
+func updateOrAddDirective(block config.IBlock, directives []config.IDirective, name string, value string) {
+	if value == "" {
+		return
+	}
+
+	// Search for existing directive
+	for _, directive := range directives {
+		if directive.GetName() == name {
+			// Update existing directive
+			if len(directive.GetParameters()) > 0 {
+				directive.GetParameters()[0].Value = value
+			}
+			return
+		}
+	}
+
+	// If we get here, we need to add a new directive
+	// Create a new directive and add it to the block
+	// This requires knowledge of the underlying implementation
+	// For now, we'll use the Directive type from gonginx/config
+	newDirective := &config.Directive{
+		Name:       name,
+		Parameters: []config.Parameter{{Value: value}},
+	}
+
+	// Add the new directive to the block
+	// This is specific to the gonginx library implementation
+	switch block := block.(type) {
+	case *config.Config:
+		block.Block.Directives = append(block.Block.Directives, newDirective)
+	case *config.Block:
+		block.Directives = append(block.Directives, newDirective)
+	case *config.HTTP:
+		block.Directives = append(block.Directives, newDirective)
+	}
+}
+
+// sortDirectives sorts directives alphabetically by name
+func sortDirectives(directives []config.IDirective) {
+	sort.SliceStable(directives, func(i, j int) bool {
+		// Ensure both i and j can return valid names
+		return directives[i].GetName() < directives[j].GetName()
+	})
+}

部分文件因文件數量過多而無法顯示