|
| 1 | +/* |
| 2 | +Copyright 2024 The Kubernetes Authors. |
| 3 | +
|
| 4 | +Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | +you may not use this file except in compliance with the License. |
| 6 | +You may obtain a copy of the License at |
| 7 | +
|
| 8 | + http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +
|
| 10 | +Unless required by applicable law or agreed to in writing, software |
| 11 | +distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | +See the License for the specific language governing permissions and |
| 14 | +limitations under the License. |
| 15 | +*/ |
| 16 | + |
| 17 | +// Package logparse provides a parser for the klog text format: |
| 18 | +// |
| 19 | +// I1007 13:16:55.727802 1146763 example.go:57] "Key/value encoding" logger="example" foo="bar" duration="1s" int=42 float=3.14 string="hello world" quotedString="hello \"world\"" multiLinString=< |
| 20 | +// hello |
| 21 | +// world |
| 22 | +// > |
| 23 | +// E1007 15:20:04.343375 1157755 example.go:41] Log using Errorf, err: fail |
| 24 | +// |
| 25 | +// It also supports the klog/ktesting unit test log output: |
| 26 | +// |
| 27 | +// === RUN TestKlogr |
| 28 | +// example_test.go:45: I1007 13:28:21.908998] hello world |
| 29 | +// example_test.go:46: E1007 13:28:21.909034] failed err="failed: some error" |
| 30 | +// example_test.go:47: I1007 13:28:21.909042] verbosity 1 |
| 31 | +// example_test.go:48: I1007 13:28:21.909047] main/helper: with prefix |
| 32 | +// example_test.go:50: I1007 13:28:21.909076] key/value pairs int=1 float=2 pair="(1, 2)" raw={"Name":"joe","NS":"kube-system"} slice=[1,2,3,"str"] map={"a":1,"b":2} kobj="kube-system/joe" string="hello world" quotedString="hello \"world\"" multiLineString=< |
| 33 | +// hello |
| 34 | +// world |
| 35 | +// > |
| 36 | +// example_test.go:63: I1007 13:28:21.909085] info message level 4 |
| 37 | +// example_test.go:64: I1007 13:28:21.909089] info message level 5 |
| 38 | +// --- PASS: TestKlogr (0.00s) |
| 39 | +// PASS |
| 40 | +// ok k8s.io/klog/v2/ktesting/example (cached) |
| 41 | +// |
| 42 | +// Arbitrary indention with space or tab is supported. All lines of |
| 43 | +// a klog log entry must be indented the same way. |
| 44 | +package logparse |
| 45 | + |
| 46 | +import ( |
| 47 | + "bufio" |
| 48 | + "errors" |
| 49 | + "fmt" |
| 50 | + "io" |
| 51 | + "iter" |
| 52 | + "regexp" |
| 53 | + "strings" |
| 54 | +) |
| 55 | + |
| 56 | +// Parse splits log output provided by the reader into individual log entries. |
| 57 | +// The original log can be reconstructed verbatim by concatenating these |
| 58 | +// entries. If the reader fails with an error, the last log entry will |
| 59 | +// capture that error. |
| 60 | +func Parse(in io.Reader) []Entry { |
| 61 | + var log []Entry |
| 62 | + for entry := range All(in) { |
| 63 | + log = append(log, entry) |
| 64 | + } |
| 65 | + return log |
| 66 | +} |
| 67 | + |
| 68 | +// All is like Parse except that it can be used in a for/range loop: |
| 69 | +// |
| 70 | +// for entry := range logparse.All(reader) { |
| 71 | +// // entry is the next log entry. |
| 72 | +// } |
| 73 | +func All(in io.Reader) iter.Seq[Entry] { |
| 74 | + return func(yield func(Entry) bool) { |
| 75 | + parse(in, yield) |
| 76 | + } |
| 77 | +} |
| 78 | + |
| 79 | +// Entry is the base type for a log entry. |
| 80 | +// |
| 81 | +// Use type assertions to check for additional information. All |
| 82 | +// of the instances behind this interface are pointers. |
| 83 | +type Entry interface { |
| 84 | + // LogData returns a verbatim copy of the original log chunk, |
| 85 | + // including one or more line breaks. Concatenating these chunks |
| 86 | + // from all entries will reconstruct the parsed log. |
| 87 | + LogData() string |
| 88 | +} |
| 89 | + |
| 90 | +// ErrorEntry captures the error encountered while reading the log input. |
| 91 | +type ErrorEntry struct { |
| 92 | + Err error |
| 93 | +} |
| 94 | + |
| 95 | +var _ Entry = &ErrorEntry{} |
| 96 | +var _ fmt.Stringer = &ErrorEntry{} |
| 97 | + |
| 98 | +func (e *ErrorEntry) LogData() string { return "" } |
| 99 | +func (e *ErrorEntry) String() string { return e.Err.Error() } |
| 100 | + |
| 101 | +// OtherEntry captures some log line which is not part of a klog log entry. |
| 102 | +type OtherEntry struct { |
| 103 | + Data string |
| 104 | +} |
| 105 | + |
| 106 | +var _ Entry = &OtherEntry{} |
| 107 | +var _ fmt.Stringer = &OtherEntry{} |
| 108 | + |
| 109 | +func (e *OtherEntry) LogData() string { return e.Data } |
| 110 | +func (e *OtherEntry) String() string { return e.Data } |
| 111 | + |
| 112 | +// LogEntry captures some log entry which was recognized as coming from klog. |
| 113 | +type KlogEntry struct { |
| 114 | + Data string |
| 115 | + Severity Severity |
| 116 | +} |
| 117 | + |
| 118 | +type Severity byte |
| 119 | + |
| 120 | +const ( |
| 121 | + SeverityInfo = Severity('I') |
| 122 | + SeverityWarning = Severity('W') |
| 123 | + SeverityError = Severity('E') |
| 124 | + SeverityFatal = Severity('F') |
| 125 | +) |
| 126 | + |
| 127 | +var _ Entry = &KlogEntry{} |
| 128 | +var _ fmt.Stringer = &KlogEntry{} |
| 129 | + |
| 130 | +func (e *KlogEntry) LogData() string { return e.Data } |
| 131 | +func (e *KlogEntry) String() string { return e.Data } |
| 132 | + |
| 133 | +func parse(in io.Reader, yield func(Entry) bool) { |
| 134 | + // Read lines using arbitrary length, which can't be done with a |
| 135 | + // bufio.Scanner. |
| 136 | + reader := bufio.NewReader(in) |
| 137 | + line, err := reader.ReadString('\n') |
| 138 | + for { |
| 139 | + // First deliver the current line. This may need to look |
| 140 | + // ahead and thus returns the next line. |
| 141 | + var nextLine string |
| 142 | + var nextErr error |
| 143 | + if len(line) > 0 { |
| 144 | + var cont bool |
| 145 | + nextLine, cont, nextErr = parseLine(reader, line, yield) |
| 146 | + if !cont { |
| 147 | + return |
| 148 | + } |
| 149 | + } else { |
| 150 | + nextLine, nextErr = reader.ReadString('\n') |
| 151 | + } |
| 152 | + |
| 153 | + // Finalize parsing? |
| 154 | + switch { |
| 155 | + case err == nil: |
| 156 | + // Okay. |
| 157 | + case errors.Is(err, io.EOF): |
| 158 | + return |
| 159 | + default: |
| 160 | + yield(&ErrorEntry{Err: err}) |
| 161 | + return |
| 162 | + } |
| 163 | + |
| 164 | + line = nextLine |
| 165 | + err = nextErr |
| 166 | + } |
| 167 | +} |
| 168 | + |
| 169 | +const ( |
| 170 | + pid = `(?<pid>[[:digit:]]+)` |
| 171 | + source = `(?<source>[^:]+:[[:digit:]]+)` |
| 172 | + severity = `(?<severity>[IWEF])` |
| 173 | + datetime = `(?<month>[[:digit:]]{2})(?<day>[[:digit:]]{2}) (?<hour>[[:digit:]]{2}):(?<minutes>[[:digit:]]{2}):(?<seconds>[[:digit:]]{2})\.(?<microseconds>[[:digit:]]{6})` |
| 174 | +) |
| 175 | + |
| 176 | +var ( |
| 177 | + klogPrefix = regexp.MustCompile(`^(?<indention>[[:blank:]]*)` + |
| 178 | + `(?:` + source + `: )?` + // `go test` source code |
| 179 | + severity + |
| 180 | + datetime + |
| 181 | + `(?: +` + pid + ` ` + source + `)?` + // klog pid + source code |
| 182 | + `\] `) |
| 183 | + |
| 184 | + indentionIndex = lookupSubmatch("indention") |
| 185 | + severityIndex = lookupSubmatch("severity") |
| 186 | +) |
| 187 | + |
| 188 | +func lookupSubmatch(name string) int { |
| 189 | + names := klogPrefix.SubexpNames() |
| 190 | + for i, n := range names { |
| 191 | + if n == name { |
| 192 | + return i |
| 193 | + } |
| 194 | + } |
| 195 | + panic(fmt.Errorf("named submatch %q not found in %q", name, klogPrefix.String())) |
| 196 | +} |
| 197 | + |
| 198 | +// parseLine deals with one non-empty line. It returns the result of yield and |
| 199 | +// potentially the next line and/or a read error. If it doesn't have any new |
| 200 | +// data to process, it returns the empty string and a nil error. |
| 201 | +func parseLine(reader *bufio.Reader, line string, yield func(Entry) bool) (string, bool, error) { |
| 202 | + match := klogPrefix.FindStringSubmatchIndex(line) |
| 203 | + if match == nil { |
| 204 | + cont := yield(&OtherEntry{Data: line}) |
| 205 | + return "", cont, nil |
| 206 | + } |
| 207 | + |
| 208 | + e := &KlogEntry{ |
| 209 | + Data: line, |
| 210 | + Severity: Severity(line[match[2*severityIndex]]), |
| 211 | + // TODO (?): store more of the fields that have been identified |
| 212 | + } |
| 213 | + // Deal with potential line continuation of multi-line string value, |
| 214 | + // if necessary. |
| 215 | + if !strings.HasSuffix(line, "=<\n") { |
| 216 | + return "", yield(e), nil |
| 217 | + } |
| 218 | + indention := line[match[2*indentionIndex]:match[2*indentionIndex+1]] |
| 219 | + for { |
| 220 | + var err error |
| 221 | + line, err = reader.ReadString('\n') |
| 222 | + if !strings.HasPrefix(line, indention) || |
| 223 | + !strings.HasPrefix(line[len(indention):], "\t") && !strings.HasPrefix(line[len(indention):], " >") { |
| 224 | + // Some other line (wrong indention or wrong continuation). |
| 225 | + // Yield what we have so far and the go back to processing that new line. |
| 226 | + cont := yield(e) |
| 227 | + return line, cont, err |
| 228 | + } |
| 229 | + e.Data += line |
| 230 | + if err != nil { |
| 231 | + return "", yield(e), err |
| 232 | + } |
| 233 | + } |
| 234 | +} |
0 commit comments