// Copyright 2020 PingCAP, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // See the License for the specific language governing permissions and // limitations under the License. package errors import ( "fmt" "runtime" "strconv" "go.uber.org/atomic" ) // RedactLogEnabled defines whether the arguments of Error need to be redacted. var RedactLogEnabled atomic.Bool // ErrCode represents a specific error type in a error class. // Same error code can be used in different error classes. type ErrCode int // ErrCodeText is a textual error code that represents a specific error type in a error class. type ErrCodeText string type ErrorID string type RFCErrorCode string // Error is the 'prototype' of a type of errors. // Use DefineError to make a *Error: // var ErrUnavailable = errors.Normalize("Region %d is unavailable", errors.RFCCodeText("Unavailable")) // // "throw" it at runtime: // func Somewhat() error { // ... // if err != nil { // // generate a stackful error use the message template at defining, // // also see FastGen(it's stackless), GenWithStack(it uses custom message template). // return ErrUnavailable.GenWithStackByArgs(region.ID) // } // } // // testing whether an error belongs to a prototype: // if ErrUnavailable.Equal(err) { // // handle this error. // } type Error struct { code ErrCode // codeText is the textual describe of the error code codeText ErrCodeText // message is a template of the description of this error. // printf-style formatting is enabled. message string // redactArgsPos defines the positions of arguments in message that need to be redacted. // And it is controlled by the global var RedactLogEnabled. // For example, an original error is `Duplicate entry 'PRIMARY' for key 'key'`, // when RedactLogEnabled is ON and redactArgsPos is [0, 1], the error is `Duplicate entry '?' for key '?'`. redactArgsPos []int // Cause is used to warp some third party error. cause error args []interface{} file string line int } // Code returns the numeric code of this error. // ID() will return textual error if there it is, // when you just want to get the purely numeric error // (e.g., for mysql protocol transmission.), this would be useful. func (e *Error) Code() ErrCode { return e.code } // Code returns ErrorCode, by the RFC: // // The error code is a 3-tuple of abbreviated component name, error class and error code, // joined by a colon like {Component}:{ErrorClass}:{InnerErrorCode}. func (e *Error) RFCCode() RFCErrorCode { return RFCErrorCode(e.ID()) } // ID returns the ID of this error. func (e *Error) ID() ErrorID { if e.codeText != "" { return ErrorID(e.codeText) } return ErrorID(strconv.Itoa(int(e.code))) } // Location returns the location where the error is created, // implements juju/errors locationer interface. func (e *Error) Location() (file string, line int) { return e.file, e.line } // MessageTemplate returns the error message template of this error. func (e *Error) MessageTemplate() string { return e.message } // Error implements error interface. func (e *Error) Error() string { if e == nil { return "" } describe := e.codeText if len(describe) == 0 { describe = ErrCodeText(strconv.Itoa(int(e.code))) } if e.cause != nil { return fmt.Sprintf("[%s]%s: %s", e.RFCCode(), e.GetMsg(), e.cause.Error()) } return fmt.Sprintf("[%s]%s", e.RFCCode(), e.GetMsg()) } func (e *Error) GetMsg() string { if len(e.args) > 0 { return fmt.Sprintf(e.message, e.args...) } return e.message } func (e *Error) fillLineAndFile(skip int) { // skip this _, file, line, ok := runtime.Caller(skip + 1) if !ok { e.file = "" e.line = -1 return } e.file = file e.line = line } // GenWithStack generates a new *Error with the same class and code, and a new formatted message. func (e *Error) GenWithStack(format string, args ...interface{}) error { // TODO: RedactErrorArg err := *e err.message = format err.args = args err.fillLineAndFile(1) return AddStack(&err) } // GenWithStackByArgs generates a new *Error with the same class and code, and new arguments. func (e *Error) GenWithStackByArgs(args ...interface{}) error { RedactErrorArg(args, e.redactArgsPos) err := *e err.args = args err.fillLineAndFile(1) return AddStack(&err) } // FastGen generates a new *Error with the same class and code, and a new formatted message. // This will not call runtime.Caller to get file and line. func (e *Error) FastGen(format string, args ...interface{}) error { // TODO: RedactErrorArg err := *e err.message = format err.args = args return SuspendStack(&err) } // FastGen generates a new *Error with the same class and code, and a new arguments. // This will not call runtime.Caller to get file and line. func (e *Error) FastGenByArgs(args ...interface{}) error { RedactErrorArg(args, e.redactArgsPos) err := *e err.args = args return SuspendStack(&err) } // Equal checks if err is equal to e. func (e *Error) Equal(err error) bool { originErr := Cause(err) if originErr == nil { return false } if error(e) == originErr { return true } inErr, ok := originErr.(*Error) if !ok { return false } idEquals := e.ID() == inErr.ID() return idEquals } // NotEqual checks if err is not equal to e. func (e *Error) NotEqual(err error) bool { return !e.Equal(err) } // RedactErrorArg redacts the args by position if RedactLogEnabled is enabled. func RedactErrorArg(args []interface{}, position []int) { if RedactLogEnabled.Load() { for _, pos := range position { if len(args) > pos { args[pos] = "?" } } } } // ErrorEqual returns a boolean indicating whether err1 is equal to err2. func ErrorEqual(err1, err2 error) bool { e1 := Cause(err1) e2 := Cause(err2) if e1 == e2 { return true } if e1 == nil || e2 == nil { return e1 == e2 } te1, ok1 := e1.(*Error) te2, ok2 := e2.(*Error) if ok1 && ok2 { return te1.Equal(te2) } return e1.Error() == e2.Error() } // ErrorNotEqual returns a boolean indicating whether err1 isn't equal to err2. func ErrorNotEqual(err1, err2 error) bool { return !ErrorEqual(err1, err2) } type jsonError struct { // Deprecated field, please use `RFCCode` instead. Class int `json:"class"` Code int `json:"code"` Msg string `json:"message"` RFCCode string `json:"rfccode"` } func (e *Error) Wrap(err error) *Error { if err != nil { newErr := *e newErr.cause = err return &newErr } return nil } func (e *Error) Cause() error { root := Unwrap(e.cause) if root == nil { return e.cause } return root } func (e *Error) FastGenWithCause(args ...interface{}) error { err := *e if e.cause != nil { err.message = e.cause.Error() } err.args = args return SuspendStack(&err) } func (e *Error) GenWithStackByCause(args ...interface{}) error { err := *e if e.cause != nil { err.message = e.cause.Error() } err.args = args err.fillLineAndFile(1) return AddStack(&err) } type NormalizeOption func(*Error) func RedactArgs(pos []int) NormalizeOption { return func(e *Error) { e.redactArgsPos = pos } } // RFCCodeText returns a NormalizeOption to set RFC error code. func RFCCodeText(codeText string) NormalizeOption { return func(e *Error) { e.codeText = ErrCodeText(codeText) } } // MySQLErrorCode returns a NormalizeOption to set error code. func MySQLErrorCode(code int) NormalizeOption { return func(e *Error) { e.code = ErrCode(code) } } // Normalize creates a new Error object. func Normalize(message string, opts ...NormalizeOption) *Error { e := &Error{ message: message, } for _, opt := range opts { opt(e) } return e }