package logging import ( "fmt" "io" "log" "os" "strings" "sync" "sync/atomic" "time" ) type LogEntry struct { ID int64 `json:"id"` Time time.Time `json:"time"` Topics string `json:"topics"` // e.g. "system,info", "auth,warning" Message string `json:"message"` } type Logger struct { mu sync.RWMutex maxSize int entries []LogEntry nextID int64 } var ( defaultLogger *Logger once sync.Once ) func GetLogger() *Logger { once.Do(func() { defaultLogger = &Logger{ maxSize: 2000, entries: make([]LogEntry, 0, 2000), } // Also hook standard library log output so existing logs are captured writer := &logCaptureWriter{logger: defaultLogger} log.SetOutput(io.MultiWriter(os.Stderr, writer)) }) return defaultLogger } type logCaptureWriter struct { logger *Logger } func (w *logCaptureWriter) Write(p []byte) (n int, err error) { msg := strings.TrimSpace(string(p)) if msg == "" { return len(p), nil } // Remove leading timestamp from standard log format if present if len(msg) > 20 && msg[4] == '/' && msg[7] == '/' { if idx := strings.Index(msg, " "); idx != -1 { if idx2 := strings.Index(msg[idx+1:], " "); idx2 != -1 { msg = strings.TrimSpace(msg[idx+1+idx2:]) } } } topics := "system,info" lower := strings.ToLower(msg) if strings.Contains(lower, "auth") || strings.Contains(lower, "login") { topics = "auth,info" } else if strings.Contains(lower, "dhcp") { topics = "dhcp,info" } else if strings.Contains(lower, "route") { topics = "route,info" } else if strings.Contains(lower, "firewall") || strings.Contains(lower, "iptables") { topics = "firewall,info" } else if strings.Contains(lower, "error") || strings.Contains(lower, "fail") { topics = "system,error" } w.logger.Add(topics, msg) return len(p), nil } func (l *Logger) Add(topics, message string) { l.mu.Lock() defer l.mu.Unlock() id := atomic.AddInt64(&l.nextID, 1) entry := LogEntry{ ID: id, Time: time.Now(), Topics: topics, Message: message, } if len(l.entries) >= l.maxSize { // Evict oldest l.entries = append(l.entries[1:], entry) } else { l.entries = append(l.entries, entry) } } func Log(topics, format string, args ...any) { GetLogger().Add(topics, fmt.Sprintf(format, args...)) } func (l *Logger) List(limit int, filterTopic string) []LogEntry { l.mu.RLock() defer l.mu.RUnlock() if limit <= 0 || limit > len(l.entries) { limit = len(l.entries) } var matched []LogEntry filterLower := strings.ToLower(filterTopic) for i := len(l.entries) - 1; i >= 0; i-- { e := l.entries[i] if filterLower != "" && !strings.Contains(strings.ToLower(e.Topics), filterLower) && !strings.Contains(strings.ToLower(e.Message), filterLower) { continue } matched = append([]LogEntry{e}, matched...) if len(matched) >= limit { break } } return matched }