314 lines
7.7 KiB
Go
314 lines
7.7 KiB
Go
|
// 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 "<nil>"
|
||
|
}
|
||
|
describe := e.codeText
|
||
|
if len(describe) == 0 {
|
||
|
describe = ErrCodeText(strconv.Itoa(int(e.code)))
|
||
|
}
|
||
|
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 = "<unknown>"
|
||
|
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
|
||
|
}
|