// SPDX-License-Identifier: GPL-3.0-or-later package eventlog import ( "bufio" "encoding/json" "fmt" "os" ) const maxRecordBytes = 1 << 20 // ReadRecent returns at most limit records in chronological order, reading the // older rotated segment before the active file. Every returned row is valid // JSON; corruption is reported instead of passed to an operator as evidence. func ReadRecent(path string, limit int) ([][]byte, error) { if path == "" { return nil, fmt.Errorf("event log path is required") } if limit <= 0 { return nil, fmt.Errorf("event limit must be positive") } buffer := recentBuffer{limit: limit} if _, err := os.Stat(path + ".1"); err == nil { if err := scanRecords(path+".1", buffer.add); err != nil { return nil, err } } else if !os.IsNotExist(err) { return nil, fmt.Errorf("stat rotated event log: %w", err) } if err := scanRecords(path, buffer.add); err != nil { return nil, err } return buffer.values(), nil } func scanRecords(path string, consume func([]byte)) error { file, err := os.Open(path) if err != nil { return fmt.Errorf("open event log %s: %w", path, err) } defer file.Close() scanner := bufio.NewScanner(file) scanner.Buffer(make([]byte, 64*1024), maxRecordBytes) line := 0 for scanner.Scan() { line++ raw := scanner.Bytes() if len(raw) == 0 { continue } if !json.Valid(raw) { return fmt.Errorf("invalid JSON in %s at line %d", path, line) } consume(append([]byte(nil), raw...)) } if err := scanner.Err(); err != nil { return fmt.Errorf("read event log %s: %w", path, err) } return nil } type recentBuffer struct { limit int rows [][]byte next int } func (b *recentBuffer) add(raw []byte) { if len(b.rows) < b.limit { b.rows = append(b.rows, raw) return } b.rows[b.next] = raw b.next = (b.next + 1) % b.limit } func (b *recentBuffer) values() [][]byte { if len(b.rows) < b.limit || b.next == 0 { return b.rows } result := make([][]byte, 0, len(b.rows)) result = append(result, b.rows[b.next:]...) result = append(result, b.rows[:b.next]...) return result }