mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-24 17:47:46 +01:00
Add trace information into internal error
Fixes #10839 Add a StackTrace func in to Error, and log it when Harbor gets a internal Signed-off-by: wang yan <wangyan@vmware.com>
This commit is contained in:
parent
542fefb9eb
commit
8bd2dc6394
66
src/lib/errors/const.go
Normal file
66
src/lib/errors/const.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package errors
|
||||||
|
|
||||||
|
const (
|
||||||
|
// NotFoundCode is code for the error of no object found
|
||||||
|
NotFoundCode = "NOT_FOUND"
|
||||||
|
// ConflictCode ...
|
||||||
|
ConflictCode = "CONFLICT"
|
||||||
|
// UnAuthorizedCode ...
|
||||||
|
UnAuthorizedCode = "UNAUTHORIZED"
|
||||||
|
// BadRequestCode ...
|
||||||
|
BadRequestCode = "BAD_REQUEST"
|
||||||
|
// ForbiddenCode ...
|
||||||
|
ForbiddenCode = "FORBIDDEN"
|
||||||
|
// PreconditionCode ...
|
||||||
|
PreconditionCode = "PRECONDITION"
|
||||||
|
// GeneralCode ...
|
||||||
|
GeneralCode = "UNKNOWN"
|
||||||
|
// DENIED it's used by middleware(readonly, vul and content trust) and returned to docker client to index the request is denied.
|
||||||
|
DENIED = "DENIED"
|
||||||
|
// PROJECTPOLICYVIOLATION ...
|
||||||
|
PROJECTPOLICYVIOLATION = "PROJECTPOLICYVIOLATION"
|
||||||
|
// ViolateForeignKeyConstraintCode is the error code for violating foreign key constraint error
|
||||||
|
ViolateForeignKeyConstraintCode = "VIOLATE_FOREIGN_KEY_CONSTRAINT"
|
||||||
|
// DIGESTINVALID ...
|
||||||
|
DIGESTINVALID = "DIGEST_INVALID"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NotFoundError is error for the case of object not found
|
||||||
|
func NotFoundError(err error) *Error {
|
||||||
|
return New(err).WithCode(NotFoundCode).WithMessage("resource not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConflictError is error for the case of object conflict
|
||||||
|
func ConflictError(err error) *Error {
|
||||||
|
return New(err).WithCode(ConflictCode).WithMessage("resource conflict")
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeniedError is error for the case of denied
|
||||||
|
func DeniedError(err error) *Error {
|
||||||
|
return New(err).WithCode(DENIED).WithMessage("denied")
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnauthorizedError is error for the case of unauthorized accessing
|
||||||
|
func UnauthorizedError(err error) *Error {
|
||||||
|
return New(err).WithCode(UnAuthorizedCode).WithMessage("unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// BadRequestError is error for the case of bad request
|
||||||
|
func BadRequestError(err error) *Error {
|
||||||
|
return New(err).WithCode(BadRequestCode).WithMessage("bad request")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForbiddenError is error for the case of forbidden
|
||||||
|
func ForbiddenError(err error) *Error {
|
||||||
|
return New(err).WithCode(ForbiddenCode).WithMessage("forbidden")
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreconditionFailedError is error for the case of precondition failed
|
||||||
|
func PreconditionFailedError(err error) *Error {
|
||||||
|
return New(err).WithCode(PreconditionCode).WithMessage("precondition failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnknownError ...
|
||||||
|
func UnknownError(err error) *Error {
|
||||||
|
return New(err).WithCode(GeneralCode).WithMessage("unknown")
|
||||||
|
}
|
@ -35,9 +35,10 @@ type Error struct {
|
|||||||
Cause error `json:"-"`
|
Cause error `json:"-"`
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
|
Stack *stack `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error returns a human readable error.
|
// Error returns a human readable error, error.Error() will not contains the track information. Needs it? just call error.StackTrace()
|
||||||
func (e *Error) Error() string {
|
func (e *Error) Error() string {
|
||||||
var parts []string
|
var parts []string
|
||||||
|
|
||||||
@ -58,6 +59,11 @@ func (e *Error) Error() string {
|
|||||||
return strings.Join(parts, ", ")
|
return strings.Join(parts, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StackTrace ...
|
||||||
|
func (e *Error) StackTrace() string {
|
||||||
|
return e.Stack.frames().format()
|
||||||
|
}
|
||||||
|
|
||||||
// WithMessage ...
|
// WithMessage ...
|
||||||
func (e *Error) WithMessage(format string, v ...interface{}) *Error {
|
func (e *Error) WithMessage(format string, v ...interface{}) *Error {
|
||||||
e.Message = fmt.Sprintf(format, v...)
|
e.Message = fmt.Sprintf(format, v...)
|
||||||
@ -120,31 +126,6 @@ func NewErrs(err error) Errors {
|
|||||||
return Errors{err}
|
return Errors{err}
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
// NotFoundCode is code for the error of no object found
|
|
||||||
NotFoundCode = "NOT_FOUND"
|
|
||||||
// ConflictCode ...
|
|
||||||
ConflictCode = "CONFLICT"
|
|
||||||
// UnAuthorizedCode ...
|
|
||||||
UnAuthorizedCode = "UNAUTHORIZED"
|
|
||||||
// BadRequestCode ...
|
|
||||||
BadRequestCode = "BAD_REQUEST"
|
|
||||||
// ForbiddenCode ...
|
|
||||||
ForbiddenCode = "FORBIDDEN"
|
|
||||||
// PreconditionCode ...
|
|
||||||
PreconditionCode = "PRECONDITION"
|
|
||||||
// GeneralCode ...
|
|
||||||
GeneralCode = "UNKNOWN"
|
|
||||||
// DENIED it's used by middleware(readonly, vul and content trust) and returned to docker client to index the request is denied.
|
|
||||||
DENIED = "DENIED"
|
|
||||||
// PROJECTPOLICYVIOLATION ...
|
|
||||||
PROJECTPOLICYVIOLATION = "PROJECTPOLICYVIOLATION"
|
|
||||||
// ViolateForeignKeyConstraintCode is the error code for violating foreign key constraint error
|
|
||||||
ViolateForeignKeyConstraintCode = "VIOLATE_FOREIGN_KEY_CONSTRAINT"
|
|
||||||
// DIGESTINVALID ...
|
|
||||||
DIGESTINVALID = "DIGEST_INVALID"
|
|
||||||
)
|
|
||||||
|
|
||||||
// New ...
|
// New ...
|
||||||
func New(in interface{}) *Error {
|
func New(in interface{}) *Error {
|
||||||
var err error
|
var err error
|
||||||
@ -159,6 +140,7 @@ func New(in interface{}) *Error {
|
|||||||
return &Error{
|
return &Error{
|
||||||
Cause: err,
|
Cause: err,
|
||||||
Message: err.Error(),
|
Message: err.Error(),
|
||||||
|
Stack: newStack(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,6 +152,7 @@ func Wrap(err error, message string) *Error {
|
|||||||
e := &Error{
|
e := &Error{
|
||||||
Cause: err,
|
Cause: err,
|
||||||
Message: message,
|
Message: message,
|
||||||
|
Stack: newStack(),
|
||||||
}
|
}
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
@ -181,6 +164,7 @@ func Wrapf(err error, format string, args ...interface{}) *Error {
|
|||||||
}
|
}
|
||||||
e := &Error{
|
e := &Error{
|
||||||
Cause: err,
|
Cause: err,
|
||||||
|
Stack: newStack(),
|
||||||
}
|
}
|
||||||
return e.WithMessage(format, args...)
|
return e.WithMessage(format, args...)
|
||||||
}
|
}
|
||||||
@ -192,46 +176,6 @@ func Errorf(format string, args ...interface{}) *Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotFoundError is error for the case of object not found
|
|
||||||
func NotFoundError(err error) *Error {
|
|
||||||
return New(err).WithCode(NotFoundCode).WithMessage("resource not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConflictError is error for the case of object conflict
|
|
||||||
func ConflictError(err error) *Error {
|
|
||||||
return New(err).WithCode(ConflictCode).WithMessage("resource conflict")
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeniedError is error for the case of denied
|
|
||||||
func DeniedError(err error) *Error {
|
|
||||||
return New(err).WithCode(DENIED).WithMessage("denied")
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnauthorizedError is error for the case of unauthorized accessing
|
|
||||||
func UnauthorizedError(err error) *Error {
|
|
||||||
return New(err).WithCode(UnAuthorizedCode).WithMessage("unauthorized")
|
|
||||||
}
|
|
||||||
|
|
||||||
// BadRequestError is error for the case of bad request
|
|
||||||
func BadRequestError(err error) *Error {
|
|
||||||
return New(err).WithCode(BadRequestCode).WithMessage("bad request")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ForbiddenError is error for the case of forbidden
|
|
||||||
func ForbiddenError(err error) *Error {
|
|
||||||
return New(err).WithCode(ForbiddenCode).WithMessage("forbidden")
|
|
||||||
}
|
|
||||||
|
|
||||||
// PreconditionFailedError is error for the case of precondition failed
|
|
||||||
func PreconditionFailedError(err error) *Error {
|
|
||||||
return New(err).WithCode(PreconditionCode).WithMessage("precondition failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnknownError ...
|
|
||||||
func UnknownError(err error) *Error {
|
|
||||||
return New(err).WithCode(GeneralCode).WithMessage("unknown")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cause gets the root error
|
// Cause gets the root error
|
||||||
func Cause(err error) error {
|
func Cause(err error) error {
|
||||||
for err != nil {
|
for err != nil {
|
||||||
|
@ -44,6 +44,31 @@ func TestErrCode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
caller1Stack = "errors.caller1"
|
||||||
|
caller2Stack = "errors.caller2"
|
||||||
|
caller3Stack = "errors.caller3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func caller1() error {
|
||||||
|
err := caller2()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func caller2() error {
|
||||||
|
err := caller3()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func caller3() error {
|
||||||
|
err := caller4()
|
||||||
|
return New(err).WithMessage("it's caller 3.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func caller4() error {
|
||||||
|
return errors.New("it's caller 4")
|
||||||
|
}
|
||||||
|
|
||||||
type ErrorTestSuite struct {
|
type ErrorTestSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
}
|
}
|
||||||
@ -55,6 +80,15 @@ func (suite *ErrorTestSuite) TestNewCompatibleWithStdlib() {
|
|||||||
suite.Equal(err2.Error(), err1.Error())
|
suite.Equal(err2.Error(), err1.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *ErrorTestSuite) TestStackTrace() {
|
||||||
|
err := caller1()
|
||||||
|
suite.Contains(err.(*Error).StackTrace(), caller1Stack)
|
||||||
|
suite.Contains(err.(*Error).StackTrace(), caller2Stack)
|
||||||
|
suite.Contains(err.(*Error).StackTrace(), caller3Stack)
|
||||||
|
suite.Contains(err.Error(), "it's caller 3.")
|
||||||
|
suite.Contains(err.Error(), "it's caller 4")
|
||||||
|
}
|
||||||
|
|
||||||
func TestErrorTestSuite(t *testing.T) {
|
func TestErrorTestSuite(t *testing.T) {
|
||||||
suite.Run(t, &ErrorTestSuite{})
|
suite.Run(t, &ErrorTestSuite{})
|
||||||
}
|
}
|
||||||
|
48
src/lib/errors/stack.go
Normal file
48
src/lib/errors/stack.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxDeepth = 50
|
||||||
|
|
||||||
|
type stack []uintptr
|
||||||
|
|
||||||
|
func (s *stack) frames() StackFrames {
|
||||||
|
var stackFrames StackFrames
|
||||||
|
frames := runtime.CallersFrames(*s)
|
||||||
|
for {
|
||||||
|
frame, next := frames.Next()
|
||||||
|
// filter out runtime
|
||||||
|
if !strings.Contains(frame.File, "runtime/") {
|
||||||
|
stackFrames = append(stackFrames, frame)
|
||||||
|
}
|
||||||
|
if !next {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stackFrames
|
||||||
|
}
|
||||||
|
|
||||||
|
// newStack ...
|
||||||
|
func newStack() *stack {
|
||||||
|
var pcs [maxDeepth]uintptr
|
||||||
|
n := runtime.Callers(3, pcs[:])
|
||||||
|
var st stack = pcs[0:n]
|
||||||
|
return &st
|
||||||
|
}
|
||||||
|
|
||||||
|
// StackFrames ...
|
||||||
|
// ToDo we can define an Harbor frame to customize trace message, but it depends on requirement
|
||||||
|
type StackFrames []runtime.Frame
|
||||||
|
|
||||||
|
// Output: <File>:<Line>, <Method>
|
||||||
|
func (frames StackFrames) format() string {
|
||||||
|
var msg string
|
||||||
|
for _, frame := range frames {
|
||||||
|
msg = msg + fmt.Sprintf("\n%v:%v, %v", frame.File, frame.Line, frame.Function)
|
||||||
|
}
|
||||||
|
return msg
|
||||||
|
}
|
30
src/lib/errors/stack_test.go
Normal file
30
src/lib/errors/stack_test.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stackTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *stackTestSuite) SetupTest() {}
|
||||||
|
|
||||||
|
func (c *stackTestSuite) TestFrame() {
|
||||||
|
stack := newStack()
|
||||||
|
frames := stack.frames()
|
||||||
|
c.Equal(len(frames), 4)
|
||||||
|
fmt.Println(frames.format())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *stackTestSuite) TestFormat() {
|
||||||
|
stack := newStack()
|
||||||
|
frames := stack.frames()
|
||||||
|
c.Contains(frames[len(frames)-1].Function, "testing.tRunner")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStackTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, &stackTestSuite{})
|
||||||
|
}
|
@ -330,6 +330,7 @@ func line(callDepth int) string {
|
|||||||
_, file, line, ok := runtime.Caller(callDepth)
|
_, file, line, ok := runtime.Caller(callDepth)
|
||||||
if !ok {
|
if !ok {
|
||||||
file = "???"
|
file = "???"
|
||||||
|
|
||||||
line = 0
|
line = 0
|
||||||
}
|
}
|
||||||
l := strings.SplitN(file, srcSeparator, 2)
|
l := strings.SplitN(file, srcSeparator, 2)
|
||||||
|
@ -47,10 +47,11 @@ var (
|
|||||||
// And the error is logged as well
|
// And the error is logged as well
|
||||||
func SendError(w http.ResponseWriter, err error) {
|
func SendError(w http.ResponseWriter, err error) {
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
statusCode, errPayload := apiError(err)
|
statusCode, errPayload, stackTrace := apiError(err)
|
||||||
// the error detail is logged only, and will not be sent to the client to avoid leaking server information
|
// the error detail is logged only, and will not be sent to the client to avoid leaking server information
|
||||||
if statusCode >= http.StatusInternalServerError {
|
if statusCode >= http.StatusInternalServerError {
|
||||||
log.Error(errPayload)
|
log.Error(errPayload)
|
||||||
|
log.Debug(stackTrace)
|
||||||
err = errors.New(nil).WithCode(errors.GeneralCode).WithMessage("internal server error")
|
err = errors.New(nil).WithCode(errors.GeneralCode).WithMessage("internal server error")
|
||||||
errPayload = errors.NewErrs(err).Error()
|
errPayload = errors.NewErrs(err).Error()
|
||||||
} else {
|
} else {
|
||||||
@ -63,7 +64,7 @@ func SendError(w http.ResponseWriter, err error) {
|
|||||||
|
|
||||||
// generates the HTTP status code based on the specified error,
|
// generates the HTTP status code based on the specified error,
|
||||||
// envelops the error into an error array as the payload and return them
|
// envelops the error into an error array as the payload and return them
|
||||||
func apiError(err error) (statusCode int, errPayload string) {
|
func apiError(err error) (statusCode int, errPayload, stackTrace string) {
|
||||||
code := 0
|
code := 0
|
||||||
var openAPIErr openapi.Error
|
var openAPIErr openapi.Error
|
||||||
if errors.As(err, &openAPIErr) {
|
if errors.As(err, &openAPIErr) {
|
||||||
@ -85,7 +86,11 @@ func apiError(err error) (statusCode int, errPayload string) {
|
|||||||
if code == 0 {
|
if code == 0 {
|
||||||
code = http.StatusInternalServerError
|
code = http.StatusInternalServerError
|
||||||
}
|
}
|
||||||
return code, errors.NewErrs(err).Error()
|
fullStack := ""
|
||||||
|
if _, ok := err.(*errors.Error); ok {
|
||||||
|
fullStack = err.(*errors.Error).StackTrace()
|
||||||
|
}
|
||||||
|
return code, errors.NewErrs(err).Error(), fullStack
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ middleware.Responder = &ErrResponder{}
|
var _ middleware.Responder = &ErrResponder{}
|
||||||
|
@ -18,6 +18,7 @@ import (
|
|||||||
openapi "github.com/go-openapi/errors"
|
openapi "github.com/go-openapi/errors"
|
||||||
commonhttp "github.com/goharbor/harbor/src/common/http"
|
commonhttp "github.com/goharbor/harbor/src/common/http"
|
||||||
"github.com/goharbor/harbor/src/lib/errors"
|
"github.com/goharbor/harbor/src/lib/errors"
|
||||||
|
pkg_errors "github.com/pkg/errors"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@ -51,32 +52,33 @@ func TestAPIError(t *testing.T) {
|
|||||||
var err error
|
var err error
|
||||||
// open API error: github.com/go-openapi/errors.Error
|
// open API error: github.com/go-openapi/errors.Error
|
||||||
err = openapi.New(400, "bad request")
|
err = openapi.New(400, "bad request")
|
||||||
statusCode, payload := apiError(err)
|
statusCode, payload, stacktrace := apiError(err)
|
||||||
assert.Equal(t, http.StatusBadRequest, statusCode)
|
assert.Equal(t, http.StatusBadRequest, statusCode)
|
||||||
assert.Equal(t, `{"errors":[{"code":"BAD_REQUEST","message":"bad request"}]}`, payload)
|
assert.Equal(t, `{"errors":[{"code":"BAD_REQUEST","message":"bad request"}]}`, payload)
|
||||||
|
assert.Contains(t, stacktrace, `error.apiError`)
|
||||||
|
|
||||||
// legacy error
|
// legacy error
|
||||||
err = &commonhttp.Error{
|
err = &commonhttp.Error{
|
||||||
Code: http.StatusNotFound,
|
Code: http.StatusNotFound,
|
||||||
Message: "not found",
|
Message: "not found",
|
||||||
}
|
}
|
||||||
statusCode, payload = apiError(err)
|
statusCode, payload, stacktrace = apiError(err)
|
||||||
assert.Equal(t, http.StatusNotFound, statusCode)
|
assert.Equal(t, http.StatusNotFound, statusCode)
|
||||||
assert.Equal(t, `{"errors":[{"code":"NOT_FOUND","message":"not found"}]}`, payload)
|
assert.Equal(t, `{"errors":[{"code":"NOT_FOUND","message":"not found"}]}`, payload)
|
||||||
|
assert.Contains(t, stacktrace, `error.apiError`)
|
||||||
|
|
||||||
// errors.Error
|
// errors.Error
|
||||||
err = &errors.Error{
|
err = errors.New(nil).WithCode(errors.NotFoundCode).WithMessage("resource not found")
|
||||||
Cause: nil,
|
statusCode, payload, stacktrace = apiError(err)
|
||||||
Code: errors.NotFoundCode,
|
|
||||||
Message: "resource not found",
|
|
||||||
}
|
|
||||||
statusCode, payload = apiError(err)
|
|
||||||
assert.Equal(t, http.StatusNotFound, statusCode)
|
assert.Equal(t, http.StatusNotFound, statusCode)
|
||||||
assert.Equal(t, `{"errors":[{"code":"NOT_FOUND","message":"resource not found"}]}`, payload)
|
assert.Equal(t, `{"errors":[{"code":"NOT_FOUND","message":"resource not found"}]}`, payload)
|
||||||
|
assert.Contains(t, stacktrace, `error.TestAPIError`)
|
||||||
|
|
||||||
// common error
|
// common error, common error has no stacktrace
|
||||||
e := errors.New("customized error")
|
e := pkg_errors.New("customized error")
|
||||||
statusCode, payload = apiError(e)
|
statusCode, payload, stacktrace = apiError(e)
|
||||||
assert.Equal(t, http.StatusInternalServerError, statusCode)
|
assert.Equal(t, http.StatusInternalServerError, statusCode)
|
||||||
assert.Equal(t, `{"errors":[{"code":"UNKNOWN","message":"customized error"}]}`, payload)
|
assert.Equal(t, `{"errors":[{"code":"UNKNOWN","message":"customized error"}]}`, payload)
|
||||||
|
assert.Contains(t, stacktrace, ``)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user