logger.go 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. package logging
  2. import (
  3. "fmt"
  4. "io"
  5. "log"
  6. "os"
  7. "strings"
  8. "sync"
  9. "sync/atomic"
  10. "time"
  11. )
  12. type LogEntry struct {
  13. ID int64 `json:"id"`
  14. Time time.Time `json:"time"`
  15. Topics string `json:"topics"` // e.g. "system,info", "auth,warning"
  16. Message string `json:"message"`
  17. }
  18. type Logger struct {
  19. mu sync.RWMutex
  20. maxSize int
  21. entries []LogEntry
  22. nextID int64
  23. }
  24. var (
  25. defaultLogger *Logger
  26. once sync.Once
  27. )
  28. func GetLogger() *Logger {
  29. once.Do(func() {
  30. defaultLogger = &Logger{
  31. maxSize: 2000,
  32. entries: make([]LogEntry, 0, 2000),
  33. }
  34. // Also hook standard library log output so existing logs are captured
  35. writer := &logCaptureWriter{logger: defaultLogger}
  36. log.SetOutput(io.MultiWriter(os.Stderr, writer))
  37. })
  38. return defaultLogger
  39. }
  40. type logCaptureWriter struct {
  41. logger *Logger
  42. }
  43. func (w *logCaptureWriter) Write(p []byte) (n int, err error) {
  44. msg := strings.TrimSpace(string(p))
  45. if msg == "" {
  46. return len(p), nil
  47. }
  48. // Remove leading timestamp from standard log format if present
  49. if len(msg) > 20 && msg[4] == '/' && msg[7] == '/' {
  50. if idx := strings.Index(msg, " "); idx != -1 {
  51. if idx2 := strings.Index(msg[idx+1:], " "); idx2 != -1 {
  52. msg = strings.TrimSpace(msg[idx+1+idx2:])
  53. }
  54. }
  55. }
  56. topics := "system,info"
  57. lower := strings.ToLower(msg)
  58. if strings.Contains(lower, "auth") || strings.Contains(lower, "login") {
  59. topics = "auth,info"
  60. } else if strings.Contains(lower, "dhcp") {
  61. topics = "dhcp,info"
  62. } else if strings.Contains(lower, "route") {
  63. topics = "route,info"
  64. } else if strings.Contains(lower, "firewall") || strings.Contains(lower, "iptables") {
  65. topics = "firewall,info"
  66. } else if strings.Contains(lower, "error") || strings.Contains(lower, "fail") {
  67. topics = "system,error"
  68. }
  69. w.logger.Add(topics, msg)
  70. return len(p), nil
  71. }
  72. func (l *Logger) Add(topics, message string) {
  73. l.mu.Lock()
  74. defer l.mu.Unlock()
  75. id := atomic.AddInt64(&l.nextID, 1)
  76. entry := LogEntry{
  77. ID: id,
  78. Time: time.Now(),
  79. Topics: topics,
  80. Message: message,
  81. }
  82. if len(l.entries) >= l.maxSize {
  83. // Evict oldest
  84. l.entries = append(l.entries[1:], entry)
  85. } else {
  86. l.entries = append(l.entries, entry)
  87. }
  88. }
  89. func Log(topics, format string, args ...any) {
  90. GetLogger().Add(topics, fmt.Sprintf(format, args...))
  91. }
  92. func (l *Logger) List(limit int, filterTopic string) []LogEntry {
  93. l.mu.RLock()
  94. defer l.mu.RUnlock()
  95. if limit <= 0 || limit > len(l.entries) {
  96. limit = len(l.entries)
  97. }
  98. var matched []LogEntry
  99. filterLower := strings.ToLower(filterTopic)
  100. for i := len(l.entries) - 1; i >= 0; i-- {
  101. e := l.entries[i]
  102. if filterLower != "" && !strings.Contains(strings.ToLower(e.Topics), filterLower) && !strings.Contains(strings.ToLower(e.Message), filterLower) {
  103. continue
  104. }
  105. matched = append([]LogEntry{e}, matched...)
  106. if len(matched) >= limit {
  107. break
  108. }
  109. }
  110. return matched
  111. }