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  }