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 }