-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathparser.go
More file actions
194 lines (165 loc) · 4.92 KB
/
parser.go
File metadata and controls
194 lines (165 loc) · 4.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
package buildkitelogs
import (
"bufio"
"io"
"iter"
"strings"
"time"
)
const (
defaultBufferSize = 64 * 1024
maxBufferSize = 1024 * 1024
)
// LogEntry represents a parsed Buildkite log entry
type LogEntry struct {
Timestamp time.Time
Content string // Parsed content after OSC processing, may still contain ANSI codes
RawLine []byte // Original line bytes including all OSC sequences and formatting
Group string // The current section/group this entry belongs to
}
// Parser handles parsing of Buildkite log files
type Parser struct {
currentGroup string
}
// LogIterator provides an iterator interface for processing log entries.
//
// Deprecated: Use Parser.All() which returns an iter.Seq2 instead.
type LogIterator struct {
scanner *bufio.Scanner
parser *Parser
currentGroup string
current *LogEntry
err error
}
// NewParser creates a new Buildkite log parser
func NewParser() *Parser {
return &Parser{}
}
// Reset clears the parser's internal state, useful for reusing the parser
// for multiple independent parsing operations.
//
// Deprecated: State isolation is now handled internally by All() and LogIterator.
// This method will be removed in a future major version.
func (p *Parser) Reset() {
p.currentGroup = ""
}
// ParseLine parses a single log line
func (p *Parser) ParseLine(line string) (*LogEntry, error) {
entry, err := parseLine(line)
if err != nil {
return nil, err
}
// Update current group if this is a group header
if entry.IsGroup() {
p.currentGroup = entry.Content
}
// Set the group for this entry
entry.Group = p.currentGroup
return entry, nil
}
// configureScanner configures a bufio.Scanner with appropriate buffer settings
// for handling potentially very long log lines
func configureScanner(scanner *bufio.Scanner) {
// Set a large buffer to handle very long log lines (default is 64KB, set to 1MB)
// see https://pkg.go.dev/bufio#MaxScanTokenSize
scanner.Buffer(make([]byte, 0, defaultBufferSize), maxBufferSize)
}
// NewIterator creates a new LogIterator for memory-efficient processing.
//
// Deprecated: Use Parser.All() which returns an iter.Seq2 instead.
func (p *Parser) NewIterator(reader io.Reader) *LogIterator {
scanner := bufio.NewScanner(reader)
configureScanner(scanner)
return &LogIterator{
scanner: scanner,
parser: p,
}
}
// All returns an iterator over all log entries using Go 1.23+ iter.Seq2 pattern
// Each iteration yields a *LogEntry and an error, following Go's idiomatic error handling
// This method creates isolated parser state to prevent contamination between iterations
func (p *Parser) All(reader io.Reader) iter.Seq2[*LogEntry, error] {
return func(yield func(*LogEntry, error) bool) {
scanner := bufio.NewScanner(reader)
configureScanner(scanner)
// Create isolated parser state for this iteration to prevent state contamination
localCurrentGroup := ""
for scanner.Scan() {
line := scanner.Text()
// Parse line using local state to avoid state contamination
entry, err := parseLine(line)
if err != nil {
yield(nil, err)
return
}
// Handle group tracking with local state
if entry.IsGroup() {
localCurrentGroup = entry.Content
}
entry.Group = localCurrentGroup
// Yield the processed entry
if !yield(entry, nil) {
return
}
}
// Check for scanner errors and yield final error if any
if err := scanner.Err(); err != nil {
_ = yield(nil, err)
}
}
}
// Next advances the iterator to the next log entry
// Returns true if there is a next entry, false if EOF or error
func (li *LogIterator) Next() bool {
if li.err != nil {
return false
}
if !li.scanner.Scan() {
li.err = li.scanner.Err()
return false
}
line := li.scanner.Text()
entry, err := parseLine(line)
if err != nil {
li.err = err
return false
}
if entry.IsGroup() {
li.currentGroup = entry.Content
}
entry.Group = li.currentGroup
li.current = entry
return true
}
// Entry returns the current log entry
// Only valid after a successful call to Next()
func (li *LogIterator) Entry() *LogEntry {
return li.current
}
// Err returns any error encountered during iteration
func (li *LogIterator) Err() error {
return li.err
}
// HasTimestamp returns true if the log entry has a valid timestamp
func (entry *LogEntry) HasTimestamp() bool {
return !entry.Timestamp.IsZero()
}
// IsGroup returns true if the log entry appears to be a group header
func (entry *LogEntry) IsGroup() bool {
return strings.HasPrefix(entry.Content, "~~~ ") || strings.HasPrefix(entry.Content, "--- ") || strings.HasPrefix(entry.Content, "+++ ")
}
// Deprecated: IsSection is an alias for IsGroup. Use IsGroup instead.
func (entry *LogEntry) IsSection() bool {
return entry.IsGroup()
}
// ComputeFlags returns the consolidated flags for this log entry
func (entry *LogEntry) ComputeFlags() LogFlags {
var flags LogFlags
if entry.HasTimestamp() {
flags.Set(HasTimestamp)
}
if entry.IsGroup() {
flags.Set(IsGroup)
}
return flags
}