mirror of
https://github.com/amir20/dozzle.git
synced 2026-08-07 10:54:44 +00:00
177 lines
4.9 KiB
Go
177 lines
4.9 KiB
Go
package container
|
|
|
|
import (
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
orderedmap "github.com/wk8/go-ordered-map/v2"
|
|
)
|
|
|
|
var SupportedLogLevels map[string]struct{}
|
|
|
|
// Changing this also needs to change the logContext.ts file
|
|
var logLevels = [][]string{
|
|
{"error", "err"},
|
|
{"warn", "warning", "wrn"},
|
|
{"info", "inf", "information"},
|
|
{"debug", "dbg"},
|
|
{"trace", "verbose", "ver", "vbs"},
|
|
{"fatal", "sev", "severe", "crit", "critical"},
|
|
}
|
|
|
|
// aliasToCanonical maps every alias to its canonical level name.
|
|
var aliasToCanonical = map[string]string{}
|
|
|
|
// levelMatcher extracts a canonical log level from a line. re must expose a
|
|
// single capture group holding the level alias, or a single-letter code when
|
|
// single is true (mapped via singleLetterToLevel).
|
|
type levelMatcher struct {
|
|
re *regexp.Regexp
|
|
single bool
|
|
}
|
|
|
|
// levelTiers groups matchers by how confidently their shape identifies the log
|
|
// level, highest confidence first:
|
|
//
|
|
// 1. ^<level> start-of-line prefix: "ERROR: ...", "INF ..."
|
|
// 2. [<level>] bracketed tag / single-letter: "[ERROR]", "[E]"
|
|
// 3. <tag>:<level> structured prefix: "Zigbee2MQTT:info "
|
|
// 4. "<LEVEL>" quoted upper-case value: LL="ERROR"
|
|
// 5. <sp><level>[/|:-] separator: " error:", " info|"
|
|
// 6. <sp><LEVEL><sp> bare upper-case token mid-line: "123 ERROR foo"
|
|
//
|
|
// guessFromString walks the tiers in order and stops at the first that matches,
|
|
// so a real level prefix at the front of the line always beats a level word
|
|
// buried in the message body. Within a tier, two different levels mean the line
|
|
// is ambiguous and we return "unknown" rather than guess. Match position and
|
|
// level severity are deliberately not used as tie-breakers.
|
|
var levelTiers [][]levelMatcher
|
|
|
|
// singleLetterBracket matches single-letter levels in brackets, e.g. [I], [E], [W]
|
|
var singleLetterBracket = regexp.MustCompile(`\[([EWIDFTV])\]`)
|
|
|
|
var timestampRegex = regexp.MustCompile(`^(?:\d{4}[-/]\d{2}[-/]\d{2}(?:[T ](?:\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?|\d{2}:\d{2}(?:AM|PM)))?\s+)`)
|
|
|
|
// JSON keys to check for log level (in priority order).
|
|
var levelKeys = []string{"@l", "level", "log.level", "severity"}
|
|
|
|
func init() {
|
|
SupportedLogLevels = make(map[string]struct{}, len(logLevels)+1)
|
|
var aliases []string
|
|
for _, group := range logLevels {
|
|
canonical := group[0]
|
|
SupportedLogLevels[canonical] = struct{}{}
|
|
for _, alias := range group {
|
|
aliasToCanonical[alias] = canonical
|
|
aliases = append(aliases, alias)
|
|
}
|
|
}
|
|
SupportedLogLevels["unknown"] = struct{}{}
|
|
|
|
// Longest aliases first so e.g. "warning" is preferred over "warn".
|
|
sort.SliceStable(aliases, func(i, j int) bool { return len(aliases[i]) > len(aliases[j]) })
|
|
joined := strings.Join(aliases, "|")
|
|
upper := strings.ToUpper(joined)
|
|
|
|
levelTiers = [][]levelMatcher{
|
|
{{re: regexp.MustCompile(`(?i)^(` + joined + `)[^a-z]`)}},
|
|
{
|
|
{re: regexp.MustCompile(`(?i)\[ ?(` + joined + `) ?\]`)},
|
|
{re: singleLetterBracket, single: true},
|
|
},
|
|
{{re: regexp.MustCompile(`(?i):(` + joined + `)\s`)}},
|
|
{{re: regexp.MustCompile(`"(` + upper + `)"`)}},
|
|
{{re: regexp.MustCompile(`(?i) (` + joined + `)[/|:-]`)}},
|
|
{{re: regexp.MustCompile(`\s(` + upper + `)\s`)}},
|
|
}
|
|
}
|
|
|
|
func guessLogLevel(logEvent *LogEvent) string {
|
|
switch value := logEvent.Message.(type) {
|
|
case string:
|
|
return guessFromString(value)
|
|
|
|
case *orderedmap.OrderedMap[string, any]:
|
|
if value == nil {
|
|
return "unknown"
|
|
}
|
|
for _, key := range levelKeys {
|
|
if v, ok := value.Get(key); ok {
|
|
if s, ok := v.(string); ok {
|
|
return normalizeLogLevel(s)
|
|
}
|
|
}
|
|
}
|
|
|
|
case *orderedmap.OrderedMap[string, string]:
|
|
if value == nil {
|
|
return "unknown"
|
|
}
|
|
for _, key := range levelKeys {
|
|
if v, ok := value.Get(key); ok {
|
|
return normalizeLogLevel(v)
|
|
}
|
|
}
|
|
|
|
default:
|
|
log.Debug().Type("type", value).Msg("unknown logEvent type")
|
|
}
|
|
|
|
return "unknown"
|
|
}
|
|
|
|
var singleLetterToLevel = map[byte]string{
|
|
'E': "error",
|
|
'W': "warn",
|
|
'I': "info",
|
|
'D': "debug",
|
|
'T': "trace",
|
|
'F': "fatal",
|
|
'V': "trace",
|
|
}
|
|
|
|
func guessFromString(value string) string {
|
|
value = StripANSI(value)
|
|
value = timestampRegex.ReplaceAllString(value, "")
|
|
for _, tier := range levelTiers {
|
|
level := ""
|
|
for _, m := range tier {
|
|
for _, match := range m.re.FindAllStringSubmatch(value, -1) {
|
|
var canonical string
|
|
if m.single {
|
|
canonical = singleLetterToLevel[match[1][0]]
|
|
} else {
|
|
canonical = aliasToCanonical[strings.ToLower(match[1])]
|
|
}
|
|
if canonical == "" {
|
|
continue
|
|
}
|
|
if level == "" {
|
|
level = canonical
|
|
} else if level != canonical {
|
|
// Two different levels at the same confidence: ambiguous.
|
|
return "unknown"
|
|
}
|
|
}
|
|
}
|
|
if level != "" {
|
|
return level
|
|
}
|
|
}
|
|
return "unknown"
|
|
}
|
|
|
|
func normalizeLogLevel(level string) string {
|
|
level = StripANSI(level)
|
|
level = strings.ToLower(level)
|
|
if canonical, ok := aliasToCanonical[level]; ok {
|
|
return canonical
|
|
}
|
|
if _, ok := SupportedLogLevels[level]; ok {
|
|
return level
|
|
}
|
|
return "unknown"
|
|
}
|