pkg/parser/k8s/klog.go (159 lines of code) (raw):

// Copyright 2024 Google LLC // // 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 k8s import ( "regexp" "strings" "github.com/GoogleCloudPlatform/khi/pkg/model/enum" ) // severityStringNotation maps string notation of severity found in logs to the severity types used in KHI. var severityStringNotation = map[string]enum.Severity{ "INFO": enum.SeverityInfo, "info": enum.SeverityInfo, "WARN": enum.SeverityWarning, "warn": enum.SeverityWarning, "WARNING": enum.SeverityWarning, "warning": enum.SeverityWarning, "ERROR": enum.SeverityError, "error": enum.SeverityError, "ERR": enum.SeverityError, "err": enum.SeverityError, "FATAL": enum.SeverityFatal, "fatal": enum.SeverityFatal, "panic": enum.SeverityFatal, } var severityKlogFieldNames = []string{"level", "severity"} // https://github.com/kubernetes/klog/blob/v2.80.1/klog.go#L626-L645 // TODO: We need to handle time field in later, but ignore it for now because times can be obtained from the other source. type klogHeader struct { Severity enum.Severity Message string } // ignore `file`,`threadid` and `line` part. var klogHeaderMatcher = regexp.MustCompile(`^([IWEF])(\d{2})(\d{2}) (\d{2}):(\d{2}):(\d{2})\.(\d{6})\s+.*\](.*)$`) func parseKLogHeader(klog string) *klogHeader { matches := klogHeaderMatcher.FindStringSubmatch(klog) if len(matches) > 0 { severityStr := matches[1] severity := enum.SeverityUnknown switch severityStr { case "I": severity = enum.SeverityInfo case "W": severity = enum.SeverityWarning case "E": severity = enum.SeverityError case "F": severity = enum.SeverityFatal } return &klogHeader{ Severity: severity, Message: strings.TrimSpace(matches[len(matches)-1]), } } return nil } func parseKLogMessageFragment(klogMessageFragment string) map[string]string { result := map[string]string{} inQuotes := false inGoBrace := false parsingKey := true escaping := false currentKey := "" currentGroup := "" // For the log format not starting from the double quote // Example: // Error foo" fieldWithQuotes="foo" fieldWithEscape="foo \"bar\"" fieldWithoutQuotes=qux1234 if strings.Count(klogMessageFragment, "\"")%2 == 1 { klogMessageFragment = `"` + klogMessageFragment } for i := 0; i < len(klogMessageFragment); i++ { // For log body beginning with `"`, it should be regarded as the msg field. if i == 0 && klogMessageFragment[i] == '"' { inQuotes = true // `msg` is reserved for the main message currentKey = "msg" parsingKey = false continue } if !escaping { if klogMessageFragment[i] == '\\' { escaping = true continue } if klogMessageFragment[i] == '{' && !inQuotes { inGoBrace = true currentGroup += string(klogMessageFragment[i]) continue } if klogMessageFragment[i] == '}' && !inQuotes && inGoBrace { inGoBrace = false currentGroup += string(klogMessageFragment[i]) continue } if klogMessageFragment[i] == '"' && !inGoBrace { if !parsingKey && inQuotes { result[currentKey] = currentGroup parsingKey = true currentGroup = "" } inQuotes = !inQuotes continue } if klogMessageFragment[i] == '=' && !inQuotes && !inGoBrace { if parsingKey { currentKey = currentGroup currentGroup = "" parsingKey = false continue } } } if klogMessageFragment[i] == ' ' && !inQuotes && !inGoBrace { if !parsingKey { result[currentKey] = currentGroup parsingKey = true currentGroup = "" } continue } if escaping { escaping = false } currentGroup += string(klogMessageFragment[i]) } if !parsingKey { result[currentKey] = currentGroup } return result } // https://kubernetes.io/docs/concepts/cluster-administration/system-logs/#klog-output func ExtractKLogField(klogBody string, field string) (string, error) { header := parseKLogHeader(klogBody) message := klogBody if header != nil { message = header.Message } fields := parseKLogMessageFragment(message) if field == "" { if message, hasMsg := fields["msg"]; hasMsg { return message, nil } if header != nil { return header.Message, nil } return klogBody, nil } else { if fieldValue, hasField := fields[field]; hasField { return fieldValue, nil } else { return "", nil } } } // ExractKLogSeverity returns severity from klog formatted logs. func ExractKLogSeverity(klogBody string) enum.Severity { header := parseKLogHeader(klogBody) if header != nil { klogBody = header.Message } fields := parseKLogMessageFragment(klogBody) for _, fieldName := range severityKlogFieldNames { if severityInStr, hasLevel := fields[fieldName]; hasLevel { if khiSeverity, isKnownSeverity := severityStringNotation[severityInStr]; isKnownSeverity { return khiSeverity } } } if header != nil { return header.Severity } return enum.SeverityUnknown }