github.com/kubeshop/testkube@v1.17.23/pkg/executor/output/parser.go (about) 1 package output 2 3 import ( 4 "bufio" 5 "bytes" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "io" 10 "strings" 11 "time" 12 13 "github.com/kubeshop/testkube/pkg/api/v1/testkube" 14 "github.com/kubeshop/testkube/pkg/utils" 15 ) 16 17 func GetLogEntry(b []byte) (out Output) { 18 if len(b) == 0 { 19 return Output{ 20 Type_: TypeUnknown, 21 Content: "", 22 Time: time.Now(), 23 } 24 } 25 26 // not json 27 if b[0] != byte('{') { 28 return Output{ 29 Type_: TypeLogLine, 30 Content: string(b), 31 Time: time.Now(), 32 } 33 } 34 35 err := json.Unmarshal(b, &out) 36 if err != nil { 37 return Output{ 38 Type_: TypeLogLine, 39 Content: string(b), 40 Time: time.Now(), 41 } 42 } 43 44 if out.Type_ == "" { 45 out.Type_ = TypeUnknown 46 } 47 48 // fallback to raw content if no content in the parsed log 49 if out.Content == "" { 50 out.Content = string(b) 51 } 52 53 return out 54 } 55 56 // ParseRunnerOutput goes over the raw logs in b and parses possible runner output 57 // The input is a json stream of the form 58 // {"type": "line", "message": "runner execution started ------------", "time": "..."} 59 // {"type": "line", "message": "GET /results", "time": "..."} 60 // {"type": "result", "result": {"id": "2323", "output": "-----"}, "time": "..."} 61 func ParseRunnerOutput(b []byte, attachLogs bool) (*testkube.ExecutionResult, error) { 62 result := &testkube.ExecutionResult{} 63 logs, err := parseLogs(b) 64 if err != nil { 65 err := fmt.Errorf("could not parse logs \"%s\": %v", b, err.Error()) 66 result.Output = err.Error() 67 result.Err(err) 68 return result.Err(err), err 69 } 70 71 log := getDecidingLogLine(logs) 72 if log == nil { 73 result.Err(errors.New("no logs found")) 74 return result, nil 75 } 76 77 switch log.Type_ { 78 case TypeResult: 79 if log.Result != nil { 80 result = log.Result 81 break 82 } 83 result.Err(errors.New("found result log with no content")) 84 case TypeError, TypeParsingError: 85 result.Err(fmt.Errorf(log.Content)) 86 default: 87 result.Err(fmt.Errorf("wrong log type was found as last log: %v", log)) 88 } 89 90 if attachLogs { 91 result.Output = sanitizeLogs(logs) 92 } 93 94 return result, nil 95 } 96 97 // ParseContainerOutput goes over the raw logs in b and parses possible container output 98 // The input is a mixed stream of the json form and plain text 99 // runner execution started ------------ 100 // {"type": "result", "result": {"id": "2323", "output": "-----"}, "time": "..."} 101 func ParseContainerOutput(b []byte) (*testkube.ExecutionResult, string, error) { 102 result := &testkube.ExecutionResult{} 103 logs, err := parseContainerLogs(b) 104 if err != nil { 105 err = fmt.Errorf("could not parse logs \"%s\": %v", b, err.Error()) 106 return nil, err.Error(), err 107 } 108 109 output := sanitizeLogs(logs) 110 log := getDecidingContainerLogLine(logs) 111 if log == nil { 112 return nil, output, nil 113 } 114 115 switch log.Type_ { 116 case TypeResult: 117 if log.Result != nil { 118 result = log.Result 119 } 120 case TypeError: 121 result.Err(fmt.Errorf(log.Content)) 122 } 123 124 return result, output, nil 125 } 126 127 // sanitizeLogs creates a human-readable string from a list of Outputs 128 func sanitizeLogs(logs []Output) string { 129 var sb strings.Builder 130 for _, l := range logs { 131 sb.WriteString(fmt.Sprintf("%s\n", l.Content)) 132 } 133 return sb.String() 134 } 135 136 // parseLogs gets a list of Outputs from raw logs 137 func parseLogs(b []byte) ([]Output, error) { 138 logs := []Output{} 139 reader := bufio.NewReader(bytes.NewReader(b)) 140 141 for { 142 b, err := utils.ReadLongLine(reader) 143 if err != nil { 144 if err == io.EOF { 145 err = nil 146 break 147 } 148 return logs, fmt.Errorf("could not read line: %w", err) 149 } 150 if len(b) < 2 || b[0] != byte('{') { 151 // empty or non json line 152 continue 153 } 154 155 log := GetLogEntry(b) 156 157 if log.Type_ == TypeResult { 158 if log.Result == nil { 159 logs = append(logs, Output{ 160 Type_: TypeError, 161 Content: fmt.Sprintf("ERROR empty result entry: %s, (((%s)))", err, string(b)), 162 }) 163 continue 164 } 165 message := getResultMessage(*log.Result) 166 logs = append(logs, Output{ 167 Type_: TypeResult, 168 Content: message, 169 Result: log.Result, 170 }) 171 continue 172 } 173 // skip appending log entry if log content is empty 174 // this can happen due to scraper logging progress or other libraries having internal logs 175 // and GetLogEntry returns an empty Output 176 if log.Content == "" { 177 continue 178 } 179 logs = append(logs, log) 180 } 181 return logs, nil 182 } 183 184 // parseContainerLogs gets a list of Outputs from raw logs 185 func parseContainerLogs(b []byte) ([]Output, error) { 186 logs := []Output{} 187 reader := bufio.NewReader(bytes.NewReader(b)) 188 189 for { 190 b, err := utils.ReadLongLine(reader) 191 192 if err != nil { 193 if err == io.EOF { 194 err = nil 195 break 196 } 197 198 return logs, fmt.Errorf("could not read line: %w", err) 199 } 200 201 log := GetLogEntry(b) 202 203 fmt.Printf("LOG: %s => '%+v'\n", b, log.Content) 204 205 if log.Type_ == TypeResult && 206 log.Result != nil && log.Result.Status != nil { 207 message := getResultMessage(*log.Result) 208 logs = append(logs, Output{ 209 Type_: TypeResult, 210 Content: message, 211 Result: log.Result, 212 }) 213 214 continue 215 } 216 217 logs = append(logs, log) 218 } 219 220 return logs, nil 221 } 222 223 // getDecidingLogLine returns the log line of type result 224 // if there is no log line of type result it will return the last log based on time 225 // if there are no timestamps, it will return the last error log from the list, 226 // if there are no errors, the last log line 227 // The latest logline will contain either the Result, the last error or the last log 228 // See: pkg/executor/agent/agent.go: func Run(r runner.Runner, args []string) 229 func getDecidingLogLine(logs []Output) *Output { 230 if len(logs) == 0 { 231 return nil 232 } 233 resultLog := Output{ 234 Type_: TypeLogLine, 235 Time: time.Time{}, 236 } 237 238 for _, log := range logs { 239 if log.Type_ == TypeResult && log.Result.IsRunning() { 240 // this is the result of the init-container on success, let's ignore it 241 continue 242 } 243 244 if moreSevere(log.Type_, resultLog.Type_) { 245 resultLog = log 246 continue 247 } 248 249 if sameSeverity(log.Type_, resultLog.Type_) { 250 if log.Time.Before(resultLog.Time) { 251 continue 252 } 253 resultLog = log 254 } 255 } 256 if resultLog.Content == "" { 257 resultLog = logs[len(logs)-1] 258 } 259 260 return &resultLog 261 } 262 263 // getDecidingContainerLogLine returns the log line of type result 264 // if there are no timestamps, it will return the last error log from the list, 265 // if there are no errors, nothing is returned 266 func getDecidingContainerLogLine(logs []Output) *Output { 267 if len(logs) == 0 { 268 return nil 269 } 270 271 resultLog := Output{ 272 Type_: TypeLogLine, 273 Time: time.Time{}, 274 } 275 276 for _, log := range logs { 277 if log.Type_ == TypeResult && 278 (log.Result == nil || log.Result.Status == nil || log.Result.IsRunning()) { 279 // this is the result of the init-container or scraper pod on success, let's ignore it 280 continue 281 } 282 283 if moreSevere(log.Type_, resultLog.Type_) { 284 resultLog = log 285 continue 286 } 287 288 if sameSeverity(log.Type_, resultLog.Type_) { 289 if log.Time.Before(resultLog.Time) { 290 continue 291 } 292 293 resultLog = log 294 } 295 } 296 297 if resultLog.Type_ != TypeResult && resultLog.Type_ != TypeError { 298 return nil 299 } 300 301 return &resultLog 302 } 303 304 // getResultMessage returns a message from the result regardless of its type 305 func getResultMessage(result testkube.ExecutionResult) string { 306 if result.IsFailed() { 307 return result.ErrorMessage 308 } 309 if result.IsPassed() { 310 return result.Output 311 } 312 313 return string(*result.Status) 314 } 315 316 // sameSeverity decides if a and b are of the same severity type 317 func sameSeverity(a string, b string) bool { 318 return a == b 319 } 320 321 // moreSevere decides if a is more severe than b 322 func moreSevere(a string, b string) bool { 323 if sameSeverity(a, b) { 324 return false 325 } 326 327 if a == TypeResult { 328 return true 329 } 330 331 if a == TypeError { 332 return b != TypeResult 333 } 334 335 // a is either log or event 336 return b != TypeResult && b != TypeError 337 }