github.com/grafana/synthetic-monitoring-agent@v0.64.2/internal/k6runner/error.go (about)

     1  package k6runner
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"os/exec"
     8  	"strings"
     9  
    10  	"github.com/go-logfmt/logfmt"
    11  )
    12  
    13  var (
    14  	ErrStacktrace = errors.New("fatal error occurred while running the script")
    15  	ErrThrown     = errors.New("uncaught error occurred while running the script")
    16  )
    17  
    18  const (
    19  	// ErrorCodeNone indicates no error: The script executed successfully.
    20  	ErrorCodeNone = ""
    21  	// ErrorCodeFailed indicates the k6 test failed and exited with a controlled, non-zero status.
    22  	// This typically happens due to calling fail(), breached thresholds, etc.
    23  	ErrorCodeFailed = "failed"
    24  	// ErrorCodeTimeout occurs when the execution context is cancelled.
    25  	ErrorCodeTimeout = "timeout"
    26  	// ErrorCodeKilled signals that k6 exited with an error code >=128, which means that the process was killed.
    27  	// If it is killed due to the timeout logic, ErrorCodeTimeout is returned instead.
    28  	ErrorCodeKilled = "killed"
    29  	// ErrorCodeAborted signals k6 exiting with an status code we map to uncontrolled failures, such as config errors,
    30  	// javascript exceptions, etc.
    31  	ErrorCodeAborted = "aborted"
    32  	// ErrorCodeUnknown reperesents a non-nil error we cannot map to any known cause.
    33  	ErrorCodeUnknown = "unknown"
    34  )
    35  
    36  // errorFromLogs scans k6 logs for signs of significant errors that should be reported as such.
    37  func errorFromLogs(logs []byte) error {
    38  	dec := logfmt.NewDecoder(bytes.NewReader(logs))
    39  
    40  	keyVals := make(map[string]string, 8) // Typically will be less than 8 fields.
    41  
    42  	for dec.ScanRecord() {
    43  		clear(keyVals)
    44  
    45  		for dec.ScanKeyval() {
    46  			// dec.Key and dec.Value values are not reusable across calls of ScanRecord, but that should be fine.
    47  			keyVals[string(dec.Key())] = string(dec.Value())
    48  		}
    49  
    50  		if dec.Err() != nil {
    51  			// Ignore errors.
    52  			continue
    53  		}
    54  
    55  		// Stacktrace errors are often fatal, like syntax errors or accessing properties of undefined objects.
    56  		if keyVals["level"] == "error" && keyVals["source"] == "stacktrace" {
    57  			return ErrStacktrace
    58  		}
    59  
    60  		// Uncaught exceptions and errors.
    61  		// These do not have an identifiable attribute (https://github.com/grafana/k6/issues/3842), so we rely in
    62  		// string matching.
    63  		if keyVals["level"] == "error" && strings.HasPrefix(keyVals["msg"], "Uncaught (in promise)") {
    64  			return ErrThrown
    65  		}
    66  	}
    67  
    68  	return nil
    69  }
    70  
    71  // isUserError returns whether we attribute this error to the user, i.e. to a combination of the k6 script contents and
    72  // settings. This includes timeouts and exit codes returned by k6.
    73  func isUserError(err error) bool {
    74  	if errors.Is(err, context.DeadlineExceeded) {
    75  		return true
    76  	}
    77  
    78  	if exitErr := (&exec.ExitError{}); errors.As(err, &exitErr) && exitErr.ExitCode() < 127 {
    79  		// If this is an ExitError and the result code is < 127, this is a user error.
    80  		// https://github.com/grafana/k6/blob/v0.50.0/errext/exitcodes/codes.go
    81  		return true
    82  	}
    83  
    84  	return false
    85  }
    86  
    87  // errorType returns a string representation of the error, which can be serialized and sent downstream to be later
    88  // interpreted by the proxy and other consumers.
    89  // TODO: This conceptually shares some logic with isUserError, we should probably refactor this at some point.
    90  func errorType(err error) string {
    91  	if err == nil {
    92  		return ErrorCodeNone
    93  	}
    94  
    95  	if errors.Is(err, context.DeadlineExceeded) {
    96  		return ErrorCodeTimeout
    97  	}
    98  
    99  	if errors.Is(err, ErrStacktrace) {
   100  		// Syntax error, or accessing properties of undefined variables.
   101  		return ErrorCodeAborted
   102  	}
   103  
   104  	if errors.Is(err, ErrThrown) {
   105  		// Script threw an exception.
   106  		return ErrorCodeFailed
   107  	}
   108  
   109  	exitErr := &exec.ExitError{}
   110  	if !errors.As(err, &exitErr) {
   111  		// If at this point it is not an ExitError, we don't know what happened here.
   112  		return ErrorCodeUnknown
   113  	}
   114  
   115  	if exitErr.ExitCode() >= 128 {
   116  		// As per POSIX spec, return code of a process when exiting due to an unhandled signal will be
   117  		// 128 + signal number. This includes OOM scenarios, where the kernel sends SIGKILL to the offending process.
   118  		// https://en.wikipedia.org/wiki/Exit_status#POSIX
   119  		return ErrorCodeKilled
   120  	}
   121  
   122  	return ErrorCodeAborted
   123  }