loadlogs.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. package executor
  2. import (
  3. "os"
  4. "path/filepath"
  5. "sort"
  6. "strings"
  7. log "github.com/sirupsen/logrus"
  8. "gopkg.in/yaml.v3"
  9. )
  10. // LoadLogsFromDisk loads persisted logs from YAML files on disk and restores them to the executor.
  11. // This should be called during startup if saveLogs is configured.
  12. func (e *Executor) LoadLogsFromDisk() {
  13. resultsDir := e.Cfg.SaveLogs.ResultsDirectory
  14. if resultsDir == "" {
  15. return
  16. }
  17. entries, skippedCount := e.readLogDirectory(resultsDir)
  18. if entries == nil {
  19. return
  20. }
  21. loadedLogs, skippedCount := e.parseLogFiles(resultsDir, entries, skippedCount)
  22. sort.Slice(loadedLogs, func(i, j int) bool {
  23. return loadedLogs[i].DatetimeStarted.Before(loadedLogs[j].DatetimeStarted)
  24. })
  25. skippedCount = e.restoreLogsToExecutor(loadedLogs, skippedCount)
  26. log.WithFields(log.Fields{
  27. "loaded": len(loadedLogs),
  28. "skipped": skippedCount,
  29. }).Info("Finished loading persisted logs from disk")
  30. }
  31. // readLogDirectory reads the log directory and returns entries, or nil if the directory doesn't exist or can't be read.
  32. func (e *Executor) readLogDirectory(resultsDir string) ([]os.DirEntry, int) {
  33. if _, err := os.Stat(resultsDir); os.IsNotExist(err) {
  34. log.WithFields(log.Fields{
  35. "directory": resultsDir,
  36. }).Debug("Logs directory does not exist, skipping log loading")
  37. return nil, 0
  38. }
  39. log.WithFields(log.Fields{
  40. "directory": resultsDir,
  41. }).Info("Loading persisted logs from disk")
  42. entries, err := os.ReadDir(resultsDir)
  43. if err != nil {
  44. log.WithFields(log.Fields{
  45. "directory": resultsDir,
  46. "error": err,
  47. }).Warnf("Failed to read logs directory")
  48. return nil, 0
  49. }
  50. return entries, 0
  51. }
  52. // parseLogFiles parses YAML log files from the directory entries.
  53. func (e *Executor) parseLogFiles(resultsDir string, entries []os.DirEntry, skippedCount int) ([]*InternalLogEntry, int) {
  54. loadedLogs := make([]*InternalLogEntry, 0)
  55. for _, entry := range entries {
  56. if !e.shouldProcessLogEntry(entry) {
  57. continue
  58. }
  59. logEntry, newSkippedCount := e.processLogFileEntry(resultsDir, entry.Name())
  60. skippedCount += newSkippedCount
  61. if logEntry != nil {
  62. loadedLogs = append(loadedLogs, logEntry)
  63. }
  64. }
  65. return loadedLogs, skippedCount
  66. }
  67. // shouldProcessLogEntry checks if a directory entry should be processed as a log file.
  68. func (e *Executor) shouldProcessLogEntry(entry os.DirEntry) bool {
  69. return !entry.IsDir() && strings.HasSuffix(entry.Name(), ".yaml")
  70. }
  71. // processLogFileEntry processes a single log file entry and returns the log entry or nil if it should be skipped.
  72. func (e *Executor) processLogFileEntry(resultsDir, filename string) (*InternalLogEntry, int) {
  73. logEntry, ok := e.loadLogFileFromPath(resultsDir, filename)
  74. if !ok {
  75. return nil, 1
  76. }
  77. if logEntry.ExecutionTrackingID == "" {
  78. log.WithFields(log.Fields{
  79. "file": filepath.Join(resultsDir, filename),
  80. }).Warnf("Log file missing execution tracking ID, skipping")
  81. return nil, 1
  82. }
  83. e.restoreBindingForLogEntry(logEntry, filepath.Join(resultsDir, filename))
  84. return logEntry, 0
  85. }
  86. // loadLogFileFromPath loads and unmarshals a single log file.
  87. func (e *Executor) loadLogFileFromPath(resultsDir, filename string) (*InternalLogEntry, bool) {
  88. filepath := filepath.Join(resultsDir, filename)
  89. data, err := os.ReadFile(filepath)
  90. if err != nil {
  91. log.WithFields(log.Fields{
  92. "file": filepath,
  93. "error": err,
  94. }).Warnf("Failed to read log file")
  95. return nil, false
  96. }
  97. var logEntry InternalLogEntry
  98. if err := yaml.Unmarshal(data, &logEntry); err != nil {
  99. log.WithFields(log.Fields{
  100. "file": filepath,
  101. "error": err,
  102. }).Warnf("Failed to unmarshal log file")
  103. return nil, false
  104. }
  105. return &logEntry, true
  106. }
  107. // restoreBindingForLogEntry attempts to restore the binding for a log entry if it's missing or invalid.
  108. func (e *Executor) restoreBindingForLogEntry(logEntry *InternalLogEntry, filepath string) {
  109. if e.hasValidBinding(logEntry) || logEntry.ActionConfigTitle == "" {
  110. return
  111. }
  112. binding := e.findBindingByActionTitle(logEntry.ActionConfigTitle, logEntry.EntityPrefix)
  113. if binding != nil {
  114. logEntry.Binding = binding
  115. return
  116. }
  117. e.logBindingNotFound(logEntry, filepath)
  118. logEntry.Binding = nil
  119. }
  120. // hasValidBinding checks if a log entry has a valid binding.
  121. func (e *Executor) hasValidBinding(logEntry *InternalLogEntry) bool {
  122. return logEntry.Binding != nil && logEntry.Binding.Action != nil
  123. }
  124. // logBindingNotFound logs a debug message when a binding cannot be found for a log entry.
  125. func (e *Executor) logBindingNotFound(logEntry *InternalLogEntry, filepath string) {
  126. log.WithFields(log.Fields{
  127. "file": filepath,
  128. "actionTitle": logEntry.ActionConfigTitle,
  129. "entityPrefix": logEntry.EntityPrefix,
  130. "trackingId": logEntry.ExecutionTrackingID,
  131. }).Debug("Could not find binding for log entry, loading without binding")
  132. }
  133. // restoreLogsToExecutor restores loaded logs to the executor's internal structures.
  134. func (e *Executor) restoreLogsToExecutor(loadedLogs []*InternalLogEntry, skippedCount int) int {
  135. e.logmutex.Lock()
  136. defer e.logmutex.Unlock()
  137. for _, logEntry := range loadedLogs {
  138. if _, exists := e.logs[logEntry.ExecutionTrackingID]; exists {
  139. log.WithFields(log.Fields{
  140. "trackingId": logEntry.ExecutionTrackingID,
  141. }).Debug("Log entry already exists, skipping")
  142. skippedCount++
  143. continue
  144. }
  145. logEntry.Index = int64(len(e.logsTrackingIdsByDate))
  146. e.logs[logEntry.ExecutionTrackingID] = logEntry
  147. e.logsTrackingIdsByDate = append(e.logsTrackingIdsByDate, logEntry.ExecutionTrackingID)
  148. if logEntry.Binding != nil {
  149. e.addLogToBindingMap(logEntry)
  150. }
  151. }
  152. return skippedCount
  153. }
  154. // addLogToBindingMap adds a log entry to the LogsByBindingId map.
  155. func (e *Executor) addLogToBindingMap(logEntry *InternalLogEntry) {
  156. if _, containsKey := e.LogsByBindingId[logEntry.Binding.ID]; !containsKey {
  157. e.LogsByBindingId[logEntry.Binding.ID] = make([]*InternalLogEntry, 0)
  158. }
  159. e.LogsByBindingId[logEntry.Binding.ID] = append(e.LogsByBindingId[logEntry.Binding.ID], logEntry)
  160. }
  161. // findBindingByActionTitle attempts to find a binding by matching the action config title and entity prefix.
  162. func (e *Executor) findBindingByActionTitle(actionConfigTitle string, entityPrefix string) *ActionBinding {
  163. e.MapActionBindingsLock.RLock()
  164. defer e.MapActionBindingsLock.RUnlock()
  165. for _, binding := range e.MapActionBindings {
  166. if binding.Action.Title == actionConfigTitle && e.matchesEntityPrefix(binding, entityPrefix) {
  167. return binding
  168. }
  169. }
  170. return nil
  171. }
  172. // matchesEntityPrefix checks if a binding matches the given entity prefix.
  173. func (e *Executor) matchesEntityPrefix(binding *ActionBinding, entityPrefix string) bool {
  174. if entityPrefix == "" {
  175. return binding.Entity == nil
  176. }
  177. return binding.Entity != nil && binding.Entity.UniqueKey == entityPrefix
  178. }