Skip to content

Commit 82b3518

Browse files
committed
cap log pattern cardinality
1 parent 511ad5b commit 82b3518

File tree

4 files changed

+89
-22
lines changed

4 files changed

+89
-22
lines changed

cmd/logparser.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ func main() {
2323

2424
reader := bufio.NewReader(os.Stdin)
2525
ch := make(chan logparser.LogEntry)
26-
parser := logparser.NewParser(ch, nil, nil, time.Second)
26+
parser := logparser.NewParser(ch, nil, nil, time.Second, 256)
2727
t := time.Now()
2828
for {
2929
line, err := reader.ReadString('\n')

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/coroot/logparser
22

3-
go 1.21
3+
go 1.24
44

55
require github.com/stretchr/testify v1.8.4
66

parser.go

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import (
66
"time"
77
)
88

9+
var (
10+
unclassifiedPatternLabel = "unclassified pattern (pattern limit reached)"
11+
)
12+
913
type LogEntry struct {
1014
Timestamp time.Time
1115
Content string
@@ -22,8 +26,10 @@ type LogCounter struct {
2226
type Parser struct {
2327
decoder Decoder
2428

25-
patterns map[patternKey]*patternStat
26-
lock sync.RWMutex
29+
patterns map[patternKey]*patternStat
30+
patternsPerLevel map[Level]int
31+
patternsPerLevelLimit int
32+
lock sync.RWMutex
2733

2834
multilineCollector *MultilineCollector
2935

@@ -34,11 +40,13 @@ type Parser struct {
3440

3541
type OnMsgCallbackF func(ts time.Time, level Level, patternHash string, msg string)
3642

37-
func NewParser(ch <-chan LogEntry, decoder Decoder, onMsgCallback OnMsgCallbackF, multilineCollectorTimeout time.Duration) *Parser {
43+
func NewParser(ch <-chan LogEntry, decoder Decoder, onMsgCallback OnMsgCallbackF, multilineCollectorTimeout time.Duration, patternsPerLevelLimit int) *Parser {
3844
p := &Parser{
39-
decoder: decoder,
40-
patterns: map[patternKey]*patternStat{},
41-
onMsgCb: onMsgCallback,
45+
decoder: decoder,
46+
patterns: map[patternKey]*patternStat{},
47+
patternsPerLevel: map[Level]int{},
48+
patternsPerLevelLimit: patternsPerLevelLimit,
49+
onMsgCb: onMsgCallback,
4250
}
4351
ctx, stop := context.WithCancel(context.Background())
4452
p.stop = stop
@@ -96,26 +104,43 @@ func (p *Parser) inc(msg Message) {
96104
}
97105

98106
pattern := NewPattern(msg.Content)
99-
key := patternKey{level: msg.Level, hash: pattern.Hash()}
100-
stat := p.patterns[key]
101-
if stat == nil {
102-
for k, ps := range p.patterns {
103-
if k.level == msg.Level && ps.pattern.WeakEqual(pattern) {
104-
stat = ps
105-
break
106-
}
107-
}
108-
if stat == nil {
109-
stat = &patternStat{pattern: pattern, sample: msg.Content}
110-
p.patterns[key] = stat
111-
}
112-
}
107+
stat, key := p.getPatternStat(msg.Level, pattern, msg.Content)
113108
if p.onMsgCb != nil {
114109
p.onMsgCb(msg.Timestamp, msg.Level, key.hash, msg.Content)
115110
}
116111
stat.messages++
117112
}
118113

114+
func (p *Parser) getPatternStat(level Level, pattern *Pattern, sample string) (*patternStat, patternKey) {
115+
key := patternKey{level: level, hash: pattern.Hash()}
116+
if stat := p.patterns[key]; stat != nil {
117+
return stat, key
118+
}
119+
for k, ps := range p.patterns {
120+
if k.level != level || ps.pattern == nil {
121+
continue
122+
}
123+
if ps.pattern.WeakEqual(pattern) {
124+
return ps, k
125+
}
126+
}
127+
128+
if p.patternsPerLevel[level] >= p.patternsPerLevelLimit {
129+
fallbackKey := patternKey{level: level, hash: ""}
130+
stat := p.patterns[fallbackKey]
131+
if stat == nil {
132+
stat = &patternStat{sample: unclassifiedPatternLabel}
133+
p.patterns[fallbackKey] = stat
134+
}
135+
return stat, fallbackKey
136+
}
137+
138+
stat := &patternStat{pattern: pattern, sample: sample}
139+
p.patterns[key] = stat
140+
p.patternsPerLevel[level]++
141+
return stat, key
142+
}
143+
119144
func (p *Parser) GetCounters() []LogCounter {
120145
p.lock.RLock()
121146
defer p.lock.RUnlock()

parser_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package logparser
2+
3+
import (
4+
"sort"
5+
"testing"
6+
"time"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestParserCardinalityLimit(t *testing.T) {
13+
p := &Parser{
14+
patterns: map[patternKey]*patternStat{},
15+
patternsPerLevel: map[Level]int{},
16+
patternsPerLevelLimit: 2,
17+
}
18+
19+
msgs := []string{
20+
"error alpha beta gamma",
21+
"error delta epsilon zeta",
22+
"error eta theta iota",
23+
}
24+
for _, m := range msgs {
25+
p.inc(Message{Timestamp: time.Now(), Content: m, Level: LevelError})
26+
}
27+
assert.Equal(t, 2, p.patternsPerLevel[LevelError])
28+
29+
fallbackKey := patternKey{level: LevelError, hash: ""}
30+
stat, ok := p.patterns[fallbackKey]
31+
require.True(t, ok)
32+
assert.Equal(t, 1, stat.messages)
33+
assert.Equal(t, unclassifiedPatternLabel, stat.sample)
34+
35+
counters := p.GetCounters()
36+
sort.Slice(counters, func(i, j int) bool { return counters[i].Sample < counters[j].Sample })
37+
38+
assert.Equal(t, 3, len(counters))
39+
assert.Equal(t, msgs[0], counters[0].Sample)
40+
assert.Equal(t, msgs[1], counters[1].Sample)
41+
assert.Equal(t, unclassifiedPatternLabel, counters[2].Sample)
42+
}

0 commit comments

Comments
 (0)