fix(level-guesser): recognize Zigbee2MQTT-style log levels

Detect ` <level>:` (e.g. `[ts] info: msg`) and `:<level> ` (e.g.
`Zigbee2MQTT:info  msg`) so containers with these formats get a real
level instead of falling through to "unknown".

Also collapse the per-level pattern struct into a single combined regex
per level (6 patterns total instead of 30) and drop the unreachable
map[string]any/map[string]string panic branches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Amir Raminfar
2026-05-23 10:20:43 -07:00
parent 450d5dc9de
commit 54cecbb5d8
2 changed files with 33 additions and 29 deletions
+24 -29
View File
@@ -23,15 +23,20 @@ var logLevels = [][]string{
// aliasToCanonical maps every alias to its canonical level name.
var aliasToCanonical = map[string]string{}
type levelPatterns struct {
plain *regexp.Regexp // e.g. ^error[^a-z]
bracket *regexp.Regexp // e.g. [ error ]
separator *regexp.Regexp // e.g. " error/"
quoted *regexp.Regexp // e.g. "ERROR"
spaced *regexp.Regexp // e.g. " ERROR "
}
var levelRegexes = map[string]levelPatterns{}
// levelRegexes holds one combined regex per canonical level. Each regex is an
// alternation of all the shapes a level can take in a log line:
//
// (?i:^<alt>[^a-z] // plain prefix: "error: ..."
// |\[ ?<alt> ?\] // bracketed: "[ERROR]" / "[ error ]"
// | <alt>[/|:-] // separator: " error|", " info:" (z2m)
// |:<alt>\s) // colon prefix: "Tag:info " (z2m)
// |"<UPPER>" // quoted: "\"ERROR\""
// |\s<UPPER>\s // spaced: " ERROR "
//
// The case-insensitive group covers the boundary-anchored forms; the trailing
// uppercase-only branches catch mid-line `ERROR` tokens without false-firing on
// the word "error" in prose.
var levelRegexes = map[string]*regexp.Regexp{}
// singleLetterBracket matches single-letter levels in brackets, e.g. [I], [E], [W]
var singleLetterBracket = regexp.MustCompile(`\[([EWIDFTV])\]`)
@@ -51,19 +56,16 @@ func init() {
}
alt := "(?:" + strings.Join(group, "|") + ")"
upperAlt := make([]string, len(group))
for i, l := range group {
upperAlt[i] = strings.ToUpper(l)
}
upperGroup := "(?:" + strings.Join(upperAlt, "|") + ")"
upper := strings.ToUpper(alt)
levelRegexes[canonical] = levelPatterns{
plain: regexp.MustCompile("(?i)^" + alt + "[^a-z]"),
bracket: regexp.MustCompile("(?i)\\[ ?" + alt + " ?\\]"),
separator: regexp.MustCompile("(?i) " + alt + "[/|-]"),
quoted: regexp.MustCompile("\"" + upperGroup + "\""),
spaced: regexp.MustCompile(" " + upperGroup + " "),
}
levelRegexes[canonical] = regexp.MustCompile(
`(?i:^` + alt + `[^a-z]` +
`|\[ ?` + alt + ` ?\]` +
`| ` + alt + `[/|:-]` +
`|:` + alt + `\s)` +
`|"` + upper + `"` +
`|\s` + upper + `\s`,
)
}
SupportedLogLevels["unknown"] = struct{}{}
}
@@ -95,12 +97,6 @@ func guessLogLevel(logEvent *LogEvent) string {
}
}
case map[string]any:
panic("not implemented")
case map[string]string:
panic("not implemented")
default:
log.Debug().Type("type", value).Msg("unknown logEvent type")
}
@@ -122,8 +118,7 @@ func guessFromString(value string) string {
value = StripANSI(value)
value = timestampRegex.ReplaceAllString(value, "")
for _, group := range logLevels {
p := levelRegexes[group[0]]
if p.plain.MatchString(value) || p.bracket.MatchString(value) || p.separator.MatchString(value) || p.quoted.MatchString(value) || p.spaced.MatchString(value) {
if levelRegexes[group[0]].MatchString(value) {
return group[0]
}
}
+9
View File
@@ -85,6 +85,15 @@ func TestGuessLogLevel(t *testing.T) {
orderedmap.Pair[string, string]{Key: "@t", Value: "2024-01-01T00:00:00Z"},
),
), "error"},
// Zigbee2MQTT-style: bracketed timestamp + " <level>:" inside the line.
{"[2025-12-22 12:00:00] info: z2m: started", "info"},
{"[2025-12-22 12:00:00] warn: z2m: queue full", "warn"},
{"[2025-12-22 12:00:00] error: z2m: connection failed", "error"},
{"[2025-12-22 12:00:00] debug: z2m: handling message", "debug"},
// "<tag>:<level> " style (no space before the colon).
{"Zigbee2MQTT:info 2025-12-22 12:00:00: started", "info"},
{"Zigbee2MQTT:warn 2025-12-22 12:00:00: queue full", "warn"},
{"Zigbee2MQTT:error 2025-12-22 12:00:00: failure", "error"},
// Pipe-delimited
{"2024-01-01 12:00:00 | ERROR | something went wrong", "error"},
{"2024-01-01 12:00:00 | INFO | starting up", "info"},