feat(performance): implement system performance monitoring - #2835
Conversation
…gurable thresholds
WalkthroughA new system performance monitoring feature is introduced that periodically collects CPU, memory, and disk usage metrics, stores them in atomic state, and enforces configurable thresholds via a middleware component. Configuration is managed through settings with corresponding UI controls, and platform-specific disk space information collection is centralized to the common package. Changes
Sequence DiagramsequenceDiagram
participant Client
participant Middleware
participant Common
participant SystemMonitor as System Monitoring<br/>(Background)
participant Resources as System Resources
Note over SystemMonitor: Every 5 seconds
SystemMonitor->>Common: updateSystemStatus()
Common->>Resources: Collect CPU, Memory, Disk
Resources-->>Common: Metrics
Common->>Common: Store in atomic.Value
Client->>Middleware: HTTP Request
Middleware->>Common: GetPerformanceMonitorConfig()
Common-->>Middleware: Config (Enabled, Thresholds)
alt Monitoring Disabled
Middleware->>Middleware: Skip checks
else Monitoring Enabled
Middleware->>Common: GetSystemStatus()
Common-->>Middleware: Current Metrics
alt CPU > Threshold
Middleware->>Middleware: NewAPIError<br/>system_cpu_overloaded
Middleware-->>Client: HTTP 503
else Memory > Threshold
Middleware->>Middleware: NewAPIError<br/>system_memory_overloaded
Middleware-->>Client: HTTP 503
else Disk > Threshold
Middleware->>Middleware: NewAPIError<br/>system_disk_overloaded
Middleware-->>Client: HTTP 503
else All OK
Middleware->>Client: c.Next() → Handler
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
web/src/pages/Setting/Performance/SettingsPerformance.jsx (1)
63-71:⚠️ Potential issue | 🟡 MinorUI default for
monitor_enabledconflicts with backend default.Server defaults to enabled, but the UI default is false; if the option isn’t persisted yet, the UI can show “off” while monitoring is actually on. Align the default or derive it from backend options.
✅ Suggested fix
- 'performance_setting.monitor_enabled': false, + 'performance_setting.monitor_enabled': true,
🤖 Fix all issues with AI agents
In `@middleware/performance.go`:
- Around line 49-61: The threshold checks cast float usage values to int causing
truncation; update the three comparisons (the CPU, Memory and Disk checks that
reference config.CPUThreshold, config.MemoryThreshold, config.DiskThreshold and
status.CPUUsage, status.MemoryUsage, status.DiskUsage) to compare floats instead
(e.g., convert the int thresholds to float64 or compare status.*Usage >=
float64(config.*Threshold)) and keep the same error returns via
types.NewErrorWithStatusCode when the condition is met.
In `@web/src/i18n/locales/en.json`:
- Around line 2729-2769: Remove the duplicated Disk Cache/Performance
translation block and keep only the first set of keys; locate the repeated keys
such as "磁盘缓存设置(磁盘换内存)", "启用磁盘缓存", "磁盘缓存阈值 (MB)", "性能监控", "请求体磁盘缓存", "磁盘缓存已清理",
and "输出价格" and delete the redundant second block that introduces the English
keys (which may override earlier entries), ensuring each translation key appears
exactly once and translations remain the original English values from the first
occurrence.
🧹 Nitpick comments (4)
common/system_monitor.go (1)
3-49: MakeStartSystemMonitoridempotent to avoid multiple goroutines.
If this gets called more than once (tests, re-init paths), it will spawn duplicate loops. Async.Onceguard keeps it safe.♻️ Suggested change
import ( + "sync" "sync/atomic" "time" "github.com/shirou/gopsutil/cpu" "github.com/shirou/gopsutil/mem" ) @@ -var latestSystemStatus atomic.Value +var ( + latestSystemStatus atomic.Value + startSystemMonitorOnce sync.Once +) @@ func StartSystemMonitor() { - go func() { - for { - config := GetPerformanceMonitorConfig() - if !config.Enabled { - time.Sleep(30 * time.Second) - continue - } - - updateSystemStatus() - time.Sleep(5 * time.Second) - } - }() + startSystemMonitorOnce.Do(func() { + go func() { + for { + config := GetPerformanceMonitorConfig() + if !config.Enabled { + time.Sleep(30 * time.Second) + continue + } + + updateSystemStatus() + time.Sleep(5 * time.Second) + } + }() + }) }setting/performance_setting/config.go (1)
58-63: Clamp monitor thresholds before syncing.Values can originate from DB/env and might fall outside 0–100, which can silently disable monitoring or make it overly aggressive. Consider clamping at sync time.
♻️ Suggested clamp before SetPerformanceMonitorConfig
common.SetDiskCacheConfig(common.DiskCacheConfig{ Enabled: performanceSetting.DiskCacheEnabled, ThresholdMB: performanceSetting.DiskCacheThresholdMB, MaxSizeMB: performanceSetting.DiskCacheMaxSizeMB, Path: performanceSetting.DiskCachePath, }) + cpu := performanceSetting.MonitorCPUThreshold + if cpu < 0 { cpu = 0 } else if cpu > 100 { cpu = 100 } + mem := performanceSetting.MonitorMemoryThreshold + if mem < 0 { mem = 0 } else if mem > 100 { mem = 100 } + disk := performanceSetting.MonitorDiskThreshold + if disk < 0 { disk = 0 } else if disk > 100 { disk = 100 } common.SetPerformanceMonitorConfig(common.PerformanceMonitorConfig{ Enabled: performanceSetting.MonitorEnabled, - CPUThreshold: performanceSetting.MonitorCPUThreshold, - MemoryThreshold: performanceSetting.MonitorMemoryThreshold, - DiskThreshold: performanceSetting.MonitorDiskThreshold, + CPUThreshold: cpu, + MemoryThreshold: mem, + DiskThreshold: disk, })middleware/performance.go (1)
14-36: Reduce duplicated error-handling branches.You can call
checkSystemPerformance()once and then choose the error shape based on the path, which trims duplication.♻️ Suggested refactor
return func(c *gin.Context) { // 仅检查 Relay 接口 (/v1, /v1beta 等) // 这里简单判断路径前缀,可以根据实际路由调整 path := c.Request.URL.Path - if strings.HasPrefix(path, "/v1/messages") { - if err := checkSystemPerformance(); err != nil { - c.JSON(err.StatusCode, gin.H{ - "error": err.ToClaudeError(), - }) - c.Abort() - return - } - } else { - if err := checkSystemPerformance(); err != nil { - c.JSON(err.StatusCode, gin.H{ - "error": err.ToOpenAIError(), - }) - c.Abort() - return - } - } + if err := checkSystemPerformance(); err != nil { + var apiErr any + if strings.HasPrefix(path, "/v1/messages") { + apiErr = err.ToClaudeError() + } else { + apiErr = err.ToOpenAIError() + } + c.JSON(err.StatusCode, gin.H{"error": apiErr}) + c.Abort() + return + } c.Next() } }controller/performance.go (1)
105-114: Remove redundant diskSpaceInfo prefill.
diskSpaceInfois initialized fromSystemStatusand then immediately overwritten, so the first assignment is dead code. Either drop it or makeGetDiskSpaceInfo()conditional.♻️ Suggested simplification
- systemStatus := common.GetSystemStatus() - diskSpaceInfo := common.DiskSpaceInfo{ - UsedPercent: systemStatus.DiskUsage, - } - // 如果需要详细信息,可以按需获取,或者扩展 SystemStatus - // 这里为了保持接口兼容性,我们仍然调用 GetDiskSpaceInfo,但注意这可能会有性能开销 - // 考虑到 GetPerformanceStats 是管理接口,频率较低,直接调用是可以接受的 - // 但为了一致性,我们也可以考虑从 SystemStatus 中获取部分信息 - diskSpaceInfo = common.GetDiskSpaceInfo() + diskSpaceInfo := common.GetDiskSpaceInfo()
| // 检查 CPU | ||
| if config.CPUThreshold > 0 && int(status.CPUUsage) > config.CPUThreshold { | ||
| return types.NewErrorWithStatusCode(errors.New("system cpu overloaded"), "system_cpu_overloaded", http.StatusServiceUnavailable) | ||
| } | ||
|
|
||
| // 检查内存 | ||
| if config.MemoryThreshold > 0 && int(status.MemoryUsage) > config.MemoryThreshold { | ||
| return types.NewErrorWithStatusCode(errors.New("system memory overloaded"), "system_memory_overloaded", http.StatusServiceUnavailable) | ||
| } | ||
|
|
||
| // 检查磁盘 | ||
| if config.DiskThreshold > 0 && int(status.DiskUsage) > config.DiskThreshold { | ||
| return types.NewErrorWithStatusCode(errors.New("system disk overloaded"), "system_disk_overloaded", http.StatusServiceUnavailable) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's locate and examine the middleware/performance.go file
fd middleware -type f -name "performance.go"Repository: QuantumNous/new-api
Length of output: 233
🏁 Script executed:
# Also check for the types/status struct definition
fd -e go | head -20Repository: QuantumNous/new-api
Length of output: 469
🏁 Script executed:
# Search for the status struct definition that contains CPUUsage, MemoryUsage, DiskUsage
rg "type.*Status.*struct" -A 10Repository: QuantumNous/new-api
Length of output: 7205
🏁 Script executed:
# Search for config struct with threshold fields
rg "CPUThreshold|MemoryThreshold|DiskThreshold" -B 5 -A 2Repository: QuantumNous/new-api
Length of output: 5987
Fix float-to-int truncation in threshold checks.
The status.CPUUsage, status.MemoryUsage, and status.DiskUsage fields are float64, while the threshold config values are int. Casting to int truncates (e.g., 90.9 → 90), allowing values just below the integer threshold to bypass the check. Compare as floats instead:
- if config.CPUThreshold > 0 && int(status.CPUUsage) > config.CPUThreshold {
+ if config.CPUThreshold > 0 && status.CPUUsage > float64(config.CPUThreshold) {
return types.NewErrorWithStatusCode(errors.New("system cpu overloaded"), "system_cpu_overloaded", http.StatusServiceUnavailable)
}
- if config.MemoryThreshold > 0 && int(status.MemoryUsage) > config.MemoryThreshold {
+ if config.MemoryThreshold > 0 && status.MemoryUsage > float64(config.MemoryThreshold) {
return types.NewErrorWithStatusCode(errors.New("system memory overloaded"), "system_memory_overloaded", http.StatusServiceUnavailable)
}
- if config.DiskThreshold > 0 && int(status.DiskUsage) > config.DiskThreshold {
+ if config.DiskThreshold > 0 && status.DiskUsage > float64(config.DiskThreshold) {
return types.NewErrorWithStatusCode(errors.New("system disk overloaded"), "system_disk_overloaded", http.StatusServiceUnavailable)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // 检查 CPU | |
| if config.CPUThreshold > 0 && int(status.CPUUsage) > config.CPUThreshold { | |
| return types.NewErrorWithStatusCode(errors.New("system cpu overloaded"), "system_cpu_overloaded", http.StatusServiceUnavailable) | |
| } | |
| // 检查内存 | |
| if config.MemoryThreshold > 0 && int(status.MemoryUsage) > config.MemoryThreshold { | |
| return types.NewErrorWithStatusCode(errors.New("system memory overloaded"), "system_memory_overloaded", http.StatusServiceUnavailable) | |
| } | |
| // 检查磁盘 | |
| if config.DiskThreshold > 0 && int(status.DiskUsage) > config.DiskThreshold { | |
| return types.NewErrorWithStatusCode(errors.New("system disk overloaded"), "system_disk_overloaded", http.StatusServiceUnavailable) | |
| // 检查 CPU | |
| if config.CPUThreshold > 0 && status.CPUUsage > float64(config.CPUThreshold) { | |
| return types.NewErrorWithStatusCode(errors.New("system cpu overloaded"), "system_cpu_overloaded", http.StatusServiceUnavailable) | |
| } | |
| // 检查内存 | |
| if config.MemoryThreshold > 0 && status.MemoryUsage > float64(config.MemoryThreshold) { | |
| return types.NewErrorWithStatusCode(errors.New("system memory overloaded"), "system_memory_overloaded", http.StatusServiceUnavailable) | |
| } | |
| // 检查磁盘 | |
| if config.DiskThreshold > 0 && status.DiskUsage > float64(config.DiskThreshold) { | |
| return types.NewErrorWithStatusCode(errors.New("system disk overloaded"), "system_disk_overloaded", http.StatusServiceUnavailable) | |
| } |
🤖 Prompt for AI Agents
In `@middleware/performance.go` around lines 49 - 61, The threshold checks cast
float usage values to int causing truncation; update the three comparisons (the
CPU, Memory and Disk checks that reference config.CPUThreshold,
config.MemoryThreshold, config.DiskThreshold and status.CPUUsage,
status.MemoryUsage, status.DiskUsage) to compare floats instead (e.g., convert
the int thresholds to float64 or compare status.*Usage >=
float64(config.*Threshold)) and keep the same error returns via
types.NewErrorWithStatusCode when the condition is met.
| "管理员未开启在线支付功能,请联系管理员配置。": "Online payment is not enabled by the admin. Please contact the administrator.", | ||
| "磁盘缓存设置(磁盘换内存)": "Disk Cache Settings (Disk Swap Memory)", | ||
| "启用磁盘缓存后,大请求体将临时存储到磁盘而非内存,可显著降低内存占用,适用于处理包含大量图片/文件的请求。建议在 SSD 环境下使用。": "When enabled, large request bodies are temporarily stored on disk instead of memory, significantly reducing memory usage. Suitable for requests with large images/files. SSD recommended.", | ||
| "启用磁盘缓存": "Enable Disk Cache", | ||
| "将大请求体临时存储到磁盘": "Store large request bodies temporarily on disk", | ||
| "磁盘缓存阈值 (MB)": "Disk Cache Threshold (MB)", | ||
| "请求体超过此大小时使用磁盘缓存": "Use disk cache when request body exceeds this size", | ||
| "磁盘缓存最大总量 (MB)": "Max Disk Cache Size (MB)", | ||
| "可用空间: {{free}} / 总空间: {{total}}": "Free: {{free}} / Total: {{total}}", | ||
| "磁盘缓存占用的最大空间": "Maximum space occupied by disk cache", | ||
| "留空使用系统临时目录": "Leave empty to use system temp directory", | ||
| "例如 /var/cache/new-api": "e.g. /var/cache/new-api", | ||
| "性能监控": "Performance Monitor", | ||
| "刷新统计": "Refresh Stats", | ||
| "重置统计": "Reset Stats", | ||
| "执行 GC": "Run GC", | ||
| "请求体磁盘缓存": "Request Body Disk Cache", | ||
| "活跃文件": "Active Files", | ||
| "磁盘命中": "Disk Hits", | ||
| "请求体内存缓存": "Request Body Memory Cache", | ||
| "当前缓存大小": "Current Cache Size", | ||
| "活跃缓存数": "Active Cache Count", | ||
| "内存命中": "Memory Hits", | ||
| "缓存目录磁盘空间": "Cache Directory Disk Space", | ||
| "磁盘可用空间小于缓存最大总量设置": "Disk free space is less than max cache size setting", | ||
| "已分配内存": "Allocated Memory", | ||
| "总分配内存": "Total Allocated Memory", | ||
| "系统内存": "System Memory", | ||
| "GC 次数": "GC Count", | ||
| "Goroutine 数": "Goroutine Count", | ||
| "目录文件数": "Directory File Count", | ||
| "目录总大小": "Directory Total Size", | ||
| "磁盘缓存已清理": "Disk cache cleared", | ||
| "清理失败": "Cleanup failed", | ||
| "统计已重置": "Statistics reset", | ||
| "重置失败": "Reset failed", | ||
| "GC 已执行": "GC executed", | ||
| "GC execution failed": "GC execution failed", | ||
| "Cache Directory": "Cache Directory", | ||
| "Available": "Available", | ||
| "输出价格": "Output Price" |
There was a problem hiding this comment.
Remove the duplicated Disk Cache/Performance strings block.
These keys are repeated (and the second block introduces English keys), which can override earlier entries and break key consistency. Please keep only one block.
🧹 Proposed cleanup (drop the duplicated block)
- "管理员未开启在线支付功能,请联系管理员配置。": "Online payment is not enabled by the admin. Please contact the administrator.",
- "磁盘缓存设置(磁盘换内存)": "Disk Cache Settings (Disk Swap Memory)",
- "启用磁盘缓存后,大请求体将临时存储到磁盘而非内存,可显著降低内存占用,适用于处理包含大量图片/文件的请求。建议在 SSD 环境下使用。": "When enabled, large request bodies are temporarily stored on disk instead of memory, significantly reducing memory usage. Suitable for requests with large images/files. SSD recommended.",
- "启用磁盘缓存": "Enable Disk Cache",
- "将大请求体临时存储到磁盘": "Store large request bodies temporarily on disk",
- "磁盘缓存阈值 (MB)": "Disk Cache Threshold (MB)",
- "请求体超过此大小时使用磁盘缓存": "Use disk cache when request body exceeds this size",
- "磁盘缓存最大总量 (MB)": "Max Disk Cache Size (MB)",
- "可用空间: {{free}} / 总空间: {{total}}": "Free: {{free}} / Total: {{total}}",
- "磁盘缓存占用的最大空间": "Maximum space occupied by disk cache",
- "留空使用系统临时目录": "Leave empty to use system temp directory",
- "例如 /var/cache/new-api": "e.g. /var/cache/new-api",
- "性能监控": "Performance Monitor",
- "刷新统计": "Refresh Stats",
- "重置统计": "Reset Stats",
- "执行 GC": "Run GC",
- "请求体磁盘缓存": "Request Body Disk Cache",
- "活跃文件": "Active Files",
- "磁盘命中": "Disk Hits",
- "请求体内存缓存": "Request Body Memory Cache",
- "当前缓存大小": "Current Cache Size",
- "活跃缓存数": "Active Cache Count",
- "内存命中": "Memory Hits",
- "缓存目录磁盘空间": "Cache Directory Disk Space",
- "磁盘可用空间小于缓存最大总量设置": "Disk free space is less than max cache size setting",
- "已分配内存": "Allocated Memory",
- "总分配内存": "Total Allocated Memory",
- "系统内存": "System Memory",
- "GC 次数": "GC Count",
- "Goroutine 数": "Goroutine Count",
- "目录文件数": "Directory File Count",
- "目录总大小": "Directory Total Size",
- "磁盘缓存已清理": "Disk cache cleared",
- "清理失败": "Cleanup failed",
- "统计已重置": "Statistics reset",
- "重置失败": "Reset failed",
- "GC 已执行": "GC executed",
- "GC execution failed": "GC execution failed",
- "Cache Directory": "Cache Directory",
- "Available": "Available",
- "输出价格": "Output Price"
+ "管理员未开启在线支付功能,请联系管理员配置。": "Online payment is not enabled by the admin. Please contact the administrator."🤖 Prompt for AI Agents
In `@web/src/i18n/locales/en.json` around lines 2729 - 2769, Remove the duplicated
Disk Cache/Performance translation block and keep only the first set of keys;
locate the repeated keys such as "磁盘缓存设置(磁盘换内存)", "启用磁盘缓存", "磁盘缓存阈值 (MB)",
"性能监控", "请求体磁盘缓存", "磁盘缓存已清理", and "输出价格" and delete the redundant second block
that introduces the English keys (which may override earlier entries), ensuring
each translation key appears exactly once and translations remain the original
English values from the first occurrence.
…-monitoring feat(performance): implement system performance monitoring
Summary by CodeRabbit
Release Notes
New Features
Localization