1
0

log_cache_grouping.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. package nginx_log
  2. import (
  3. "path/filepath"
  4. "regexp"
  5. "strings"
  6. "time"
  7. "github.com/0xJacky/Nginx-UI/model"
  8. "github.com/uozi-tech/cosy/logger"
  9. )
  10. // GetAllLogsWithIndexGrouped returns logs grouped by their base name (e.g., access.log includes access.log.1, access.log.2.gz etc.)
  11. func GetAllLogsWithIndexGrouped(filters ...func(*NginxLogWithIndex) bool) []*NginxLogWithIndex {
  12. cacheMutex.RLock()
  13. defer cacheMutex.RUnlock()
  14. // Get all logs from both cache (config files) and persistence (indexed files)
  15. allLogsMap := make(map[string]*NginxLogWithIndex)
  16. // First, get logs from the cache (these are from nginx config)
  17. for _, cache := range logCache {
  18. logWithIndex := &NginxLogWithIndex{
  19. Path: cache.Path,
  20. Type: cache.Type,
  21. Name: cache.Name,
  22. ConfigFile: cache.ConfigFile,
  23. IndexStatus: IndexStatusNotIndexed,
  24. IsCompressed: false,
  25. HasTimeRange: false,
  26. }
  27. allLogsMap[cache.Path] = logWithIndex
  28. }
  29. // Get persistence manager for database index records
  30. persistence := NewPersistenceManager()
  31. persistenceIndexes, err := persistence.GetAllLogIndexes()
  32. if err != nil {
  33. logger.Warnf("Failed to get persistence indexes: %v", err)
  34. persistenceIndexes = []*model.NginxLogIndex{}
  35. }
  36. // Add all indexed files from persistence (including rotated files)
  37. for _, idx := range persistenceIndexes {
  38. if _, exists := allLogsMap[idx.Path]; !exists {
  39. // This is a rotated file not in config cache, create entry for it
  40. logType := "access"
  41. if strings.Contains(idx.Path, "error") {
  42. logType = "error"
  43. }
  44. logWithIndex := &NginxLogWithIndex{
  45. Path: idx.Path,
  46. Type: logType,
  47. Name: filepath.Base(idx.Path),
  48. ConfigFile: "", // Rotated files don't have config
  49. IndexStatus: IndexStatusNotIndexed,
  50. IsCompressed: strings.HasSuffix(idx.Path, ".gz") || strings.HasSuffix(idx.Path, ".bz2"),
  51. HasTimeRange: false,
  52. }
  53. allLogsMap[idx.Path] = logWithIndex
  54. }
  55. }
  56. // Now populate index information for all logs
  57. persistenceMap := make(map[string]*model.NginxLogIndex)
  58. for _, idx := range persistenceIndexes {
  59. persistenceMap[idx.Path] = idx
  60. }
  61. // Get analytics service for index status
  62. service := GetAnalyticsService()
  63. var indexStatus *IndexStatus
  64. if service != nil {
  65. status, err := service.GetIndexStatus()
  66. if err == nil {
  67. indexStatus = status
  68. }
  69. }
  70. // Create a map of indexed files for quick lookup
  71. indexedFiles := make(map[string]*FileStatus)
  72. if indexStatus != nil && indexStatus.Files != nil {
  73. for i := range indexStatus.Files {
  74. file := &indexStatus.Files[i]
  75. indexedFiles[file.Path] = file
  76. }
  77. }
  78. // Update index information for all logs
  79. for _, log := range allLogsMap {
  80. // Check if this file is currently being indexed
  81. if IsFileIndexing(log.Path) {
  82. log.IndexStatus = IndexStatusIndexing
  83. }
  84. // Check persistence data first (more accurate)
  85. if persistenceIndex, ok := persistenceMap[log.Path]; ok {
  86. // Set status based on persistence and current indexing state
  87. if log.IndexStatus != IndexStatusIndexing {
  88. if !persistenceIndex.LastIndexed.IsZero() {
  89. log.IndexStatus = IndexStatusIndexed
  90. }
  91. }
  92. // Use persistence data
  93. if !persistenceIndex.LastModified.IsZero() {
  94. log.LastModified = persistenceIndex.LastModified.Unix()
  95. }
  96. log.LastSize = persistenceIndex.LastSize
  97. if !persistenceIndex.LastIndexed.IsZero() {
  98. log.LastIndexed = persistenceIndex.LastIndexed.Unix()
  99. }
  100. if persistenceIndex.IndexStartTime != nil {
  101. log.IndexStartTime = persistenceIndex.IndexStartTime.Unix()
  102. }
  103. if persistenceIndex.IndexDuration != nil {
  104. log.IndexDuration = *persistenceIndex.IndexDuration
  105. }
  106. if persistenceIndex.TimeRangeStart != nil {
  107. log.TimeRangeStart = persistenceIndex.TimeRangeStart.Unix()
  108. log.HasTimeRange = true
  109. }
  110. if persistenceIndex.TimeRangeEnd != nil {
  111. log.TimeRangeEnd = persistenceIndex.TimeRangeEnd.Unix()
  112. log.HasTimeRange = true
  113. }
  114. log.DocumentCount = persistenceIndex.DocumentCount
  115. } else if fileStatus, ok := indexedFiles[log.Path]; ok {
  116. // Fallback to old index status system
  117. if log.IndexStatus != IndexStatusIndexing {
  118. log.IndexStatus = IndexStatusIndexed
  119. }
  120. if fileStatus.LastModified != 0 {
  121. log.LastModified = fileStatus.LastModified
  122. }
  123. log.LastSize = fileStatus.LastSize
  124. if fileStatus.LastIndexed != 0 {
  125. log.LastIndexed = fileStatus.LastIndexed
  126. }
  127. log.IsCompressed = fileStatus.IsCompressed
  128. log.HasTimeRange = fileStatus.HasTimeRange
  129. if fileStatus.TimeRangeStart != 0 {
  130. log.TimeRangeStart = fileStatus.TimeRangeStart
  131. }
  132. if fileStatus.TimeRangeEnd != 0 {
  133. log.TimeRangeEnd = fileStatus.TimeRangeEnd
  134. }
  135. }
  136. }
  137. // Convert map to slice
  138. allLogs := make([]*NginxLogWithIndex, 0, len(allLogsMap))
  139. for _, log := range allLogsMap {
  140. allLogs = append(allLogs, log)
  141. }
  142. // Group logs by their base log name
  143. logGroups := make(map[string][]*NginxLogWithIndex)
  144. for _, log := range allLogs {
  145. baseLogName := getBaseLogName(log.Path)
  146. logGroups[baseLogName] = append(logGroups[baseLogName], log)
  147. }
  148. result := make([]*NginxLogWithIndex, 0, len(logGroups))
  149. // Process each group
  150. for baseLogName, group := range logGroups {
  151. // Find the main log file (the one without rotation suffix)
  152. var mainLog *NginxLogWithIndex
  153. for _, log := range group {
  154. if isMainLogFile(log.Path, baseLogName) {
  155. mainLog = log
  156. break
  157. }
  158. }
  159. // If no main log file found, create one based on the base name
  160. if mainLog == nil {
  161. // Create a virtual main log based on the group's characteristics
  162. // Use the first log in the group as a template
  163. template := group[0]
  164. mainLog = &NginxLogWithIndex{
  165. Path: baseLogName,
  166. Type: template.Type,
  167. Name: filepath.Base(baseLogName),
  168. ConfigFile: template.ConfigFile,
  169. IndexStatus: IndexStatusNotIndexed,
  170. IsCompressed: false,
  171. HasTimeRange: false,
  172. }
  173. }
  174. // Aggregate statistics from all files in the group
  175. aggregateLogGroupStats(mainLog, group)
  176. // Apply filters
  177. flag := true
  178. if len(filters) > 0 {
  179. for _, filter := range filters {
  180. if !filter(mainLog) {
  181. flag = false
  182. break
  183. }
  184. }
  185. }
  186. if flag {
  187. result = append(result, mainLog)
  188. }
  189. }
  190. return result
  191. }
  192. // getBaseLogName extracts the base log name from a rotated log file path
  193. // Examples:
  194. // /var/log/nginx/access.log.1 -> /var/log/nginx/access.log
  195. // /var/log/nginx/access.log.10.gz -> /var/log/nginx/access.log
  196. // /var/log/nginx/access.20231201.gz -> /var/log/nginx/access.log
  197. func getBaseLogName(logPath string) string {
  198. dir := filepath.Dir(logPath)
  199. filename := filepath.Base(logPath)
  200. // Remove .gz compression suffix if present
  201. if strings.HasSuffix(filename, ".gz") {
  202. filename = strings.TrimSuffix(filename, ".gz")
  203. }
  204. // Handle numbered rotation (access.log.1, access.log.2, etc.)
  205. // Use a more specific pattern to avoid matching date patterns like "20231201"
  206. if match := regexp.MustCompile(`^(.+)\.(\d{1,3})$`).FindStringSubmatch(filename); len(match) > 1 {
  207. // Only match if the number is reasonable for rotation (1-999)
  208. baseFilename := match[1]
  209. return filepath.Join(dir, baseFilename)
  210. }
  211. // Handle date-based rotation (access.20231201, access.2023-12-01, etc.)
  212. // Check if filename itself contains date patterns that we should strip
  213. // Example: access.2023-12-01 -> access.log, access.20231201 -> access.log
  214. parts := strings.Split(filename, ".")
  215. if len(parts) >= 2 {
  216. lastPart := parts[len(parts)-1]
  217. if isDatePattern(lastPart) {
  218. baseFilename := strings.Join(parts[:len(parts)-1], ".")
  219. // If the base doesn't end with .log, add it
  220. if !strings.HasSuffix(baseFilename, ".log") {
  221. baseFilename += ".log"
  222. }
  223. return filepath.Join(dir, baseFilename)
  224. }
  225. }
  226. // No rotation pattern found, return as-is
  227. return logPath
  228. }
  229. // isMainLogFile checks if the given path is the main log file (no rotation suffix)
  230. func isMainLogFile(logPath, baseLogName string) bool {
  231. return logPath == baseLogName
  232. }
  233. // aggregateLogGroupStats aggregates statistics from all files in a log group
  234. func aggregateLogGroupStats(aggregatedLog *NginxLogWithIndex, group []*NginxLogWithIndex) {
  235. var totalSize int64
  236. var totalDocuments uint64
  237. var earliestTimeStart *time.Time
  238. var latestTimeEnd *time.Time
  239. var mostRecentIndexed *time.Time
  240. var indexingInProgress bool
  241. var hasIndexedFiles bool
  242. var earliestIndexStartTime *time.Time
  243. var totalIndexDuration *int64
  244. for _, log := range group {
  245. // Aggregate file sizes
  246. totalSize += log.LastSize
  247. // Aggregate document counts
  248. totalDocuments += log.DocumentCount
  249. // Check for indexing status
  250. if log.IndexStatus == IndexStatusIndexing {
  251. indexingInProgress = true
  252. } else if log.IndexStatus == IndexStatusIndexed {
  253. hasIndexedFiles = true
  254. }
  255. // Find the most recent indexed time
  256. if log.LastIndexed != 0 {
  257. indexedTime := time.Unix(log.LastIndexed, 0)
  258. if mostRecentIndexed == nil || indexedTime.After(*mostRecentIndexed) {
  259. mostRecentIndexed = &indexedTime
  260. }
  261. }
  262. // Aggregate time ranges
  263. if log.TimeRangeStart != 0 {
  264. startTime := time.Unix(log.TimeRangeStart, 0)
  265. if earliestTimeStart == nil || startTime.Before(*earliestTimeStart) {
  266. earliestTimeStart = &startTime
  267. }
  268. }
  269. if log.TimeRangeEnd != 0 {
  270. endTime := time.Unix(log.TimeRangeEnd, 0)
  271. if latestTimeEnd == nil || endTime.After(*latestTimeEnd) {
  272. latestTimeEnd = &endTime
  273. }
  274. }
  275. // Use properties from the most recent file
  276. if log.LastModified != 0 && (aggregatedLog.LastModified == 0 || log.LastModified > aggregatedLog.LastModified) {
  277. aggregatedLog.LastModified = log.LastModified
  278. }
  279. // Find the EARLIEST IndexStartTime for the log group (when the group indexing started)
  280. if log.IndexStartTime != 0 {
  281. startTime := time.Unix(log.IndexStartTime, 0)
  282. if earliestIndexStartTime == nil || startTime.Before(*earliestIndexStartTime) {
  283. earliestIndexStartTime = &startTime
  284. }
  285. }
  286. // Sum up individual file durations to get total group duration
  287. if log.IndexDuration != 0 {
  288. if totalIndexDuration == nil {
  289. totalIndexDuration = new(int64)
  290. }
  291. *totalIndexDuration += log.IndexDuration
  292. }
  293. }
  294. // Set aggregated values
  295. if earliestIndexStartTime != nil {
  296. aggregatedLog.IndexStartTime = earliestIndexStartTime.Unix()
  297. }
  298. aggregatedLog.LastSize = totalSize
  299. aggregatedLog.DocumentCount = totalDocuments
  300. if mostRecentIndexed != nil {
  301. aggregatedLog.LastIndexed = mostRecentIndexed.Unix()
  302. }
  303. if totalIndexDuration != nil {
  304. aggregatedLog.IndexDuration = *totalIndexDuration
  305. }
  306. // Set index status based on group status
  307. if indexingInProgress {
  308. aggregatedLog.IndexStatus = IndexStatusIndexing
  309. } else if hasIndexedFiles {
  310. aggregatedLog.IndexStatus = IndexStatusIndexed
  311. } else {
  312. aggregatedLog.IndexStatus = IndexStatusNotIndexed
  313. }
  314. // Set time range
  315. if earliestTimeStart != nil && latestTimeEnd != nil {
  316. aggregatedLog.TimeRangeStart = earliestTimeStart.Unix()
  317. aggregatedLog.TimeRangeEnd = latestTimeEnd.Unix()
  318. aggregatedLog.HasTimeRange = true
  319. }
  320. }