performance.go 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. package controller
  2. import (
  3. "fmt"
  4. "net/http"
  5. "os"
  6. "path/filepath"
  7. "runtime"
  8. "sort"
  9. "strconv"
  10. "strings"
  11. "time"
  12. "github.com/QuantumNous/new-api/common"
  13. "github.com/QuantumNous/new-api/logger"
  14. "github.com/gin-gonic/gin"
  15. )
  16. // PerformanceStats 性能统计信息
  17. type PerformanceStats struct {
  18. // 缓存统计
  19. CacheStats common.DiskCacheStats `json:"cache_stats"`
  20. // 系统内存统计
  21. MemoryStats MemoryStats `json:"memory_stats"`
  22. // 磁盘缓存目录信息
  23. DiskCacheInfo DiskCacheInfo `json:"disk_cache_info"`
  24. // 磁盘空间信息
  25. DiskSpaceInfo common.DiskSpaceInfo `json:"disk_space_info"`
  26. // 配置信息
  27. Config PerformanceConfig `json:"config"`
  28. }
  29. // MemoryStats 内存统计
  30. type MemoryStats struct {
  31. // 已分配内存(字节)
  32. Alloc uint64 `json:"alloc"`
  33. // 总分配内存(字节)
  34. TotalAlloc uint64 `json:"total_alloc"`
  35. // 系统内存(字节)
  36. Sys uint64 `json:"sys"`
  37. // GC 次数
  38. NumGC uint32 `json:"num_gc"`
  39. // Goroutine 数量
  40. NumGoroutine int `json:"num_goroutine"`
  41. }
  42. // DiskCacheInfo 磁盘缓存目录信息
  43. type DiskCacheInfo struct {
  44. // 缓存目录路径
  45. Path string `json:"path"`
  46. // 目录是否存在
  47. Exists bool `json:"exists"`
  48. // 文件数量
  49. FileCount int `json:"file_count"`
  50. // 总大小(字节)
  51. TotalSize int64 `json:"total_size"`
  52. }
  53. // PerformanceConfig 性能配置
  54. type PerformanceConfig struct {
  55. // 是否启用磁盘缓存
  56. DiskCacheEnabled bool `json:"disk_cache_enabled"`
  57. // 磁盘缓存阈值(MB)
  58. DiskCacheThresholdMB int `json:"disk_cache_threshold_mb"`
  59. // 磁盘缓存最大大小(MB)
  60. DiskCacheMaxSizeMB int `json:"disk_cache_max_size_mb"`
  61. // 磁盘缓存路径
  62. DiskCachePath string `json:"disk_cache_path"`
  63. // 是否在容器中运行
  64. IsRunningInContainer bool `json:"is_running_in_container"`
  65. // MonitorEnabled 是否启用性能监控
  66. MonitorEnabled bool `json:"monitor_enabled"`
  67. // MonitorCPUThreshold CPU 使用率阈值(%)
  68. MonitorCPUThreshold int `json:"monitor_cpu_threshold"`
  69. // MonitorMemoryThreshold 内存使用率阈值(%)
  70. MonitorMemoryThreshold int `json:"monitor_memory_threshold"`
  71. // MonitorDiskThreshold 磁盘使用率阈值(%)
  72. MonitorDiskThreshold int `json:"monitor_disk_threshold"`
  73. }
  74. // GetPerformanceStats 获取性能统计信息
  75. func GetPerformanceStats(c *gin.Context) {
  76. // 不再每次获取统计都全量扫描磁盘,依赖原子计数器保证性能
  77. // 仅在系统启动或显式清理时同步
  78. cacheStats := common.GetDiskCacheStats()
  79. // 获取内存统计
  80. var memStats runtime.MemStats
  81. runtime.ReadMemStats(&memStats)
  82. // 获取磁盘缓存目录信息
  83. diskCacheInfo := getDiskCacheInfo()
  84. // 获取配置信息
  85. diskConfig := common.GetDiskCacheConfig()
  86. monitorConfig := common.GetPerformanceMonitorConfig()
  87. config := PerformanceConfig{
  88. DiskCacheEnabled: diskConfig.Enabled,
  89. DiskCacheThresholdMB: diskConfig.ThresholdMB,
  90. DiskCacheMaxSizeMB: diskConfig.MaxSizeMB,
  91. DiskCachePath: diskConfig.Path,
  92. IsRunningInContainer: common.IsRunningInContainer(),
  93. MonitorEnabled: monitorConfig.Enabled,
  94. MonitorCPUThreshold: monitorConfig.CPUThreshold,
  95. MonitorMemoryThreshold: monitorConfig.MemoryThreshold,
  96. MonitorDiskThreshold: monitorConfig.DiskThreshold,
  97. }
  98. // 获取磁盘空间信息
  99. // 使用缓存的系统状态,避免频繁调用系统 API
  100. systemStatus := common.GetSystemStatus()
  101. diskSpaceInfo := common.DiskSpaceInfo{
  102. UsedPercent: systemStatus.DiskUsage,
  103. }
  104. // 如果需要详细信息,可以按需获取,或者扩展 SystemStatus
  105. // 这里为了保持接口兼容性,我们仍然调用 GetDiskSpaceInfo,但注意这可能会有性能开销
  106. // 考虑到 GetPerformanceStats 是管理接口,频率较低,直接调用是可以接受的
  107. // 但为了一致性,我们也可以考虑从 SystemStatus 中获取部分信息
  108. diskSpaceInfo = common.GetDiskSpaceInfo()
  109. stats := PerformanceStats{
  110. CacheStats: cacheStats,
  111. MemoryStats: MemoryStats{
  112. Alloc: memStats.Alloc,
  113. TotalAlloc: memStats.TotalAlloc,
  114. Sys: memStats.Sys,
  115. NumGC: memStats.NumGC,
  116. NumGoroutine: runtime.NumGoroutine(),
  117. },
  118. DiskCacheInfo: diskCacheInfo,
  119. DiskSpaceInfo: diskSpaceInfo,
  120. Config: config,
  121. }
  122. c.JSON(http.StatusOK, gin.H{
  123. "success": true,
  124. "data": stats,
  125. })
  126. }
  127. // ClearDiskCache 清理不活跃的磁盘缓存
  128. func ClearDiskCache(c *gin.Context) {
  129. // 清理超过 10 分钟未使用的缓存文件
  130. // 10 分钟是一个安全的阈值,确保正在进行的请求不会被误删
  131. err := common.CleanupOldDiskCacheFiles(10 * time.Minute)
  132. if err != nil {
  133. common.ApiError(c, err)
  134. return
  135. }
  136. c.JSON(http.StatusOK, gin.H{
  137. "success": true,
  138. "message": "不活跃的磁盘缓存已清理",
  139. })
  140. }
  141. // ResetPerformanceStats 重置性能统计
  142. func ResetPerformanceStats(c *gin.Context) {
  143. common.ResetDiskCacheStats()
  144. c.JSON(http.StatusOK, gin.H{
  145. "success": true,
  146. "message": "统计信息已重置",
  147. })
  148. }
  149. // ForceGC 强制执行 GC
  150. func ForceGC(c *gin.Context) {
  151. runtime.GC()
  152. c.JSON(http.StatusOK, gin.H{
  153. "success": true,
  154. "message": "GC 已执行",
  155. })
  156. }
  157. // LogFileInfo 日志文件信息
  158. type LogFileInfo struct {
  159. Name string `json:"name"`
  160. Size int64 `json:"size"`
  161. ModTime time.Time `json:"mod_time"`
  162. }
  163. // LogFilesResponse 日志文件列表响应
  164. type LogFilesResponse struct {
  165. LogDir string `json:"log_dir"`
  166. Enabled bool `json:"enabled"`
  167. FileCount int `json:"file_count"`
  168. TotalSize int64 `json:"total_size"`
  169. OldestTime *time.Time `json:"oldest_time,omitempty"`
  170. NewestTime *time.Time `json:"newest_time,omitempty"`
  171. Files []LogFileInfo `json:"files"`
  172. }
  173. // getLogFiles 读取日志目录中的日志文件列表
  174. func getLogFiles() ([]LogFileInfo, error) {
  175. if *common.LogDir == "" {
  176. return nil, nil
  177. }
  178. entries, err := os.ReadDir(*common.LogDir)
  179. if err != nil {
  180. return nil, err
  181. }
  182. var files []LogFileInfo
  183. for _, entry := range entries {
  184. if entry.IsDir() {
  185. continue
  186. }
  187. name := entry.Name()
  188. if !strings.HasPrefix(name, "oneapi-") || !strings.HasSuffix(name, ".log") {
  189. continue
  190. }
  191. info, err := entry.Info()
  192. if err != nil {
  193. continue
  194. }
  195. files = append(files, LogFileInfo{
  196. Name: name,
  197. Size: info.Size(),
  198. ModTime: info.ModTime(),
  199. })
  200. }
  201. // 按文件名降序排列(最新在前)
  202. sort.Slice(files, func(i, j int) bool {
  203. return files[i].Name > files[j].Name
  204. })
  205. return files, nil
  206. }
  207. // GetLogFiles 获取日志文件列表
  208. func GetLogFiles(c *gin.Context) {
  209. if *common.LogDir == "" {
  210. common.ApiSuccess(c, LogFilesResponse{Enabled: false})
  211. return
  212. }
  213. files, err := getLogFiles()
  214. if err != nil {
  215. common.ApiError(c, err)
  216. return
  217. }
  218. var totalSize int64
  219. var oldest, newest time.Time
  220. for i, f := range files {
  221. totalSize += f.Size
  222. if i == 0 || f.ModTime.Before(oldest) {
  223. oldest = f.ModTime
  224. }
  225. if i == 0 || f.ModTime.After(newest) {
  226. newest = f.ModTime
  227. }
  228. }
  229. resp := LogFilesResponse{
  230. LogDir: *common.LogDir,
  231. Enabled: true,
  232. FileCount: len(files),
  233. TotalSize: totalSize,
  234. Files: files,
  235. }
  236. if len(files) > 0 {
  237. resp.OldestTime = &oldest
  238. resp.NewestTime = &newest
  239. }
  240. common.ApiSuccess(c, resp)
  241. }
  242. // CleanupLogFiles 清理过期日志文件
  243. func CleanupLogFiles(c *gin.Context) {
  244. mode := c.Query("mode")
  245. valueStr := c.Query("value")
  246. if mode != "by_count" && mode != "by_days" {
  247. common.ApiErrorMsg(c, "invalid mode, must be by_count or by_days")
  248. return
  249. }
  250. value, err := strconv.Atoi(valueStr)
  251. if err != nil || value < 1 {
  252. common.ApiErrorMsg(c, "invalid value, must be a positive integer")
  253. return
  254. }
  255. if *common.LogDir == "" {
  256. common.ApiErrorMsg(c, "log directory not configured")
  257. return
  258. }
  259. files, err := getLogFiles()
  260. if err != nil {
  261. common.ApiError(c, err)
  262. return
  263. }
  264. activeLogPath := logger.GetCurrentLogPath()
  265. var toDelete []LogFileInfo
  266. switch mode {
  267. case "by_count":
  268. // files 已按名称降序(最新在前),保留前 value 个
  269. for i, f := range files {
  270. if i < value {
  271. continue
  272. }
  273. fullPath := filepath.Join(*common.LogDir, f.Name)
  274. if fullPath == activeLogPath {
  275. continue
  276. }
  277. toDelete = append(toDelete, f)
  278. }
  279. case "by_days":
  280. cutoff := time.Now().AddDate(0, 0, -value)
  281. for _, f := range files {
  282. if f.ModTime.Before(cutoff) {
  283. fullPath := filepath.Join(*common.LogDir, f.Name)
  284. if fullPath == activeLogPath {
  285. continue
  286. }
  287. toDelete = append(toDelete, f)
  288. }
  289. }
  290. }
  291. var deletedCount int
  292. var freedBytes int64
  293. var failedFiles []string
  294. for _, f := range toDelete {
  295. fullPath := filepath.Join(*common.LogDir, f.Name)
  296. if err := os.Remove(fullPath); err != nil {
  297. failedFiles = append(failedFiles, f.Name)
  298. continue
  299. }
  300. deletedCount++
  301. freedBytes += f.Size
  302. }
  303. result := gin.H{
  304. "deleted_count": deletedCount,
  305. "freed_bytes": freedBytes,
  306. "failed_files": failedFiles,
  307. }
  308. if len(failedFiles) > 0 {
  309. c.JSON(http.StatusOK, gin.H{
  310. "success": false,
  311. "message": fmt.Sprintf("部分文件删除失败(%d/%d)", len(failedFiles), len(toDelete)),
  312. "data": result,
  313. })
  314. return
  315. }
  316. c.JSON(http.StatusOK, gin.H{
  317. "success": true,
  318. "message": "",
  319. "data": result,
  320. })
  321. }
  322. // getDiskCacheInfo 获取磁盘缓存目录信息
  323. func getDiskCacheInfo() DiskCacheInfo {
  324. // 使用统一的缓存目录
  325. dir := common.GetDiskCacheDir()
  326. info := DiskCacheInfo{
  327. Path: dir,
  328. Exists: false,
  329. }
  330. entries, err := os.ReadDir(dir)
  331. if err != nil {
  332. return info
  333. }
  334. info.Exists = true
  335. info.FileCount = 0
  336. info.TotalSize = 0
  337. for _, entry := range entries {
  338. if entry.IsDir() {
  339. continue
  340. }
  341. info.FileCount++
  342. if fileInfo, err := entry.Info(); err == nil {
  343. info.TotalSize += fileInfo.Size()
  344. }
  345. }
  346. return info
  347. }