Files
node-problem-detector/pkg/systemlogmonitor/log_buffer.go
T
Veer Singh 4334390423 Cache the joined log buffer and skip it for single-line rules
The log buffer joins every buffered line and scans all of them for
each pattern on every push, which is wasted work when a pattern
cannot even match across lines. The buffer now caches the joined
string, and Push invalidates the cache. CompilePattern marks the
patterns that cannot match across lines (no start anchor, no way to
match a newline, and the appended \z anchor binds every branch) and
Match checks them against the most recent line alone.
2026-07-27 16:37:28 -07:00

193 lines
5.1 KiB
Go

/*
Copyright 2016 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package systemlogmonitor
import (
"regexp"
"regexp/syntax"
"slices"
"strings"
"k8s.io/node-problem-detector/pkg/systemlogmonitor/types"
)
// LogBuffer buffers the logs and matches a compiled pattern.
type LogBuffer interface {
// Push pushes log into the log buffer.
Push(*types.Log)
// Match with a compiled pattern in the log buffer.
Match(*Pattern) []*types.Log
}
// Pattern is a compiled rule plus the facts that let Match narrow its scan.
type Pattern struct {
// regexp is the rule anchored to the end of the buffered logs.
regexp *regexp.Regexp
// lastLineOnly reports that the rule can match only in the last pushed line.
// Match then skips building the joined buffer.
lastLineOnly bool
}
// logBuffer is not safe for concurrent use.
type logBuffer struct {
// buffer is a simple ring buffer.
buffer []*types.Log
msg []string
max int
current int
// joined caches the result of String. Push clears it.
joined string
// joinedOK reports whether joined is current.
joinedOK bool
}
// NewLogBuffer creates log buffer with max line number limit. Because we only match logs
// in the log buffer, the max buffer line number is also the max pattern line number we
// support. Smaller buffer line number means less memory and cpu usage, but also means less
// lines of patterns we support.
func NewLogBuffer(maxLines int) *logBuffer {
return &logBuffer{
buffer: make([]*types.Log, maxLines),
msg: make([]string, maxLines),
max: maxLines,
}
}
// CompilePattern compiles a log buffer pattern that must match to the end of
// the buffered logs.
func CompilePattern(expr string) (*Pattern, error) {
// Compile expr alone first so an error cites the pattern as written.
if _, err := regexp.Compile(expr); err != nil {
return nil, err
}
anchored := expr + `\z`
reg, err := regexp.Compile(anchored)
if err != nil {
return nil, err
}
p := &Pattern{regexp: reg}
tree, err := syntax.Parse(anchored, syntax.Perl)
if err != nil {
return p, nil
}
// A top-level alternation binds the appended anchor to its last branch only.
// Equal trees prove that the anchor covers every branch.
grouped, err := syntax.Parse(`(?:`+expr+`)\z`, syntax.Perl)
if err != nil {
return p, nil
}
p.lastLineOnly = tree.Equal(grouped) && isLastLineOnly(tree)
return p, nil
}
// isLastLineOnly reports whether the rule accepts no newline and has no start anchor.
func isLastLineOnly(re *syntax.Regexp) bool {
switch re.Op {
case syntax.OpAnyChar:
// `(?s).` accepts a newline.
return false
case syntax.OpBeginText, syntax.OpBeginLine, syntax.OpEndLine:
// A start anchor marks the start of the whole buffer.
return false
case syntax.OpLiteral:
if slices.Contains(re.Rune, '\n') {
return false
}
case syntax.OpCharClass:
// Rune stores the character class as inclusive lo, hi pairs.
for i := 0; i+1 < len(re.Rune); i += 2 {
if re.Rune[i] <= '\n' && '\n' <= re.Rune[i+1] {
return false
}
}
}
for _, sub := range re.Sub {
if !isLastLineOnly(sub) {
return false
}
}
return true
}
func (b *logBuffer) Push(log *types.Log) {
b.buffer[b.current%b.max] = log
b.msg[b.current%b.max] = log.Message
b.current++
b.joinedOK = false
b.joined = ""
}
func (b *logBuffer) Match(p *Pattern) []*types.Log {
if p.lastLineOnly {
return b.matchLastLine(p.regexp)
}
log := b.String()
loc := p.regexp.FindStringIndex(log)
if loc == nil {
// No match
return nil
}
// reverse index
s := len(log) - loc[0] - 1
total := 0
matched := []*types.Log{}
for i := b.tail(); i >= b.current && b.buffer[i%b.max] != nil; i-- {
matched = append(matched, b.buffer[i%b.max])
total += len(b.msg[i%b.max]) + 1 // Add '\n'
if total > s {
break
}
}
slices.Reverse(matched)
return matched
}
// matchLastLine matches a lastLineOnly rule against the most recently pushed line.
func (b *logBuffer) matchLastLine(reg *regexp.Regexp) []*types.Log {
if b.current == 0 {
return nil
}
last := (b.current - 1) % b.max
if !reg.MatchString(b.msg[last]) {
return nil
}
return []*types.Log{b.buffer[last]}
}
func (b *logBuffer) String() string {
if b.joinedOK {
return b.joined
}
head := b.current % b.max
lines := make([]string, 0, b.max)
lines = append(lines, b.msg[head:]...)
lines = append(lines, b.msg[:head]...)
b.joined = concatLogs(lines)
b.joinedOK = true
return b.joined
}
// tail returns current tail index.
func (b *logBuffer) tail() int {
return b.current + b.max - 1
}
// concatLogs concatenates multiple lines of logs into one string.
func concatLogs(logs []string) string {
return strings.Join(logs, "\n")
}