mirror of
https://github.com/grafana/grafana.git
synced 2024-11-23 09:26:43 -06:00
90631360eb
Ref #68480 Co-authored-by: Giuseppe Guerra <giuseppe.guerra@grafana.com>
414 lines
13 KiB
Go
414 lines
13 KiB
Go
package errutil
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
// Base represents the static information about a specific error.
|
|
// Always use [NewBase] to create new instances of Base.
|
|
type Base struct {
|
|
// Because Base is typically instantiated as a package or global
|
|
// variable, having private members reduces the probability of a
|
|
// bug messing with the error base.
|
|
reason StatusReason
|
|
messageID string
|
|
publicMessage string
|
|
logLevel LogLevel
|
|
source Source
|
|
}
|
|
|
|
// NewBase initializes a [Base] that is used to construct [Error].
|
|
// The reason is used to determine the status code that should be
|
|
// returned for the error, and the msgID is passed to the caller
|
|
// to serve as the base for user facing error messages.
|
|
//
|
|
// msgID should be structured as component.errorBrief, for example
|
|
//
|
|
// login.failedAuthentication
|
|
// dashboards.validationError
|
|
// dashboards.uidAlreadyExists
|
|
func NewBase(reason StatusReason, msgID string, opts ...BaseOpt) Base {
|
|
b := Base{
|
|
reason: reason,
|
|
messageID: msgID,
|
|
logLevel: reason.Status().LogLevel(),
|
|
source: SourceServer,
|
|
}
|
|
|
|
for _, opt := range opts {
|
|
b = opt(b)
|
|
}
|
|
|
|
return b
|
|
}
|
|
|
|
// NotFound initializes a new [Base] error with reason StatusNotFound
|
|
// that is used to construct [Error]. The msgID is passed to the caller
|
|
// to serve as the base for user facing error messages.
|
|
//
|
|
// msgID should be structured as component.errorBrief, for example
|
|
//
|
|
// folder.notFound
|
|
// plugin.notRegistered
|
|
func NotFound(msgID string, opts ...BaseOpt) Base {
|
|
return NewBase(StatusNotFound, msgID, opts...)
|
|
}
|
|
|
|
// BadRequest initializes a new [Base] error with reason StatusBadRequest
|
|
// that is used to construct [Error]. The msgID is passed to the caller
|
|
// to serve as the base for user facing error messages.
|
|
//
|
|
// msgID should be structured as component.errorBrief, for example
|
|
//
|
|
// query.invalidDatasourceId
|
|
// sse.dataQueryError
|
|
func BadRequest(msgID string, opts ...BaseOpt) Base {
|
|
return NewBase(StatusBadRequest, msgID, opts...)
|
|
}
|
|
|
|
// ValidationFailed initializes a new [Base] error with reason StatusValidationFailed
|
|
// that is used to construct [Error]. The msgID is passed to the caller
|
|
// to serve as the base for user facing error messages.
|
|
//
|
|
// msgID should be structured as component.errorBrief, for example
|
|
//
|
|
// datasource.nameInvalid
|
|
// datasource.urlInvalid
|
|
// serviceaccounts.errInvalidInput
|
|
func ValidationFailed(msgID string, opts ...BaseOpt) Base {
|
|
return NewBase(StatusValidationFailed, msgID, opts...)
|
|
}
|
|
|
|
// Internal initializes a new [Base] error with reason StatusInternal
|
|
// that is used to construct [Error]. The msgID is passed to the caller
|
|
// to serve as the base for user facing error messages.
|
|
//
|
|
// msgID should be structured as component.errorBrief, for example
|
|
//
|
|
// sqleng.connectionError
|
|
// plugin.downstreamError
|
|
func Internal(msgID string, opts ...BaseOpt) Base {
|
|
return NewBase(StatusInternal, msgID, opts...)
|
|
}
|
|
|
|
// Timeout initializes a new [Base] error with reason StatusTimeout.
|
|
//
|
|
// area.timeout
|
|
func Timeout(msgID string, opts ...BaseOpt) Base {
|
|
return NewBase(StatusTimeout, msgID, opts...)
|
|
}
|
|
|
|
// Unauthorized initializes a new [Base] error with reason StatusUnauthorized
|
|
// that is used to construct [Error]. The msgID is passed to the caller
|
|
// to serve as the base for user facing error messages.
|
|
//
|
|
// msgID should be structured as component.errorBrief, for example
|
|
//
|
|
// auth.unauthorized
|
|
func Unauthorized(msgID string, opts ...BaseOpt) Base {
|
|
return NewBase(StatusUnauthorized, msgID, opts...)
|
|
}
|
|
|
|
// Forbidden initializes a new [Base] error with reason StatusForbidden
|
|
// that is used to construct [Error]. The msgID is passed to the caller
|
|
// to serve as the base for user facing error messages.
|
|
//
|
|
// msgID should be structured as component.errorBrief, for example
|
|
//
|
|
// quota.disabled
|
|
// user.sync.forbidden
|
|
func Forbidden(msgID string, opts ...BaseOpt) Base {
|
|
return NewBase(StatusForbidden, msgID, opts...)
|
|
}
|
|
|
|
// TooManyRequests initializes a new [Base] error with reason StatusTooManyRequests
|
|
// that is used to construct [Error]. The msgID is passed to the caller
|
|
// to serve as the base for user facing error messages.
|
|
//
|
|
// msgID should be structured as component.errorBrief, for example
|
|
//
|
|
// area.tooManyRequests
|
|
func TooManyRequests(msgID string, opts ...BaseOpt) Base {
|
|
return NewBase(StatusTooManyRequests, msgID, opts...)
|
|
}
|
|
|
|
// ClientClosedRequest initializes a new [Base] error with reason StatusClientClosedRequest
|
|
// that is used to construct [Error]. The msgID is passed to the caller
|
|
// to serve as the base for user facing error messages.
|
|
//
|
|
// msgID should be structured as component.errorBrief, for example
|
|
//
|
|
// plugin.requestCanceled
|
|
func ClientClosedRequest(msgID string, opts ...BaseOpt) Base {
|
|
return NewBase(StatusClientClosedRequest, msgID, opts...)
|
|
}
|
|
|
|
// NotImplemented initializes a new [Base] error with reason StatusNotImplemented
|
|
// that is used to construct [Error]. The msgID is passed to the caller
|
|
// to serve as the base for user facing error messages.
|
|
//
|
|
// msgID should be structured as component.errorBrief, for example
|
|
//
|
|
// plugin.notImplemented
|
|
// auth.identity.unsupported
|
|
func NotImplemented(msgID string, opts ...BaseOpt) Base {
|
|
return NewBase(StatusNotImplemented, msgID, opts...)
|
|
}
|
|
|
|
// BadGateway initializes a new [Base] error with reason StatusBadGateway
|
|
// and source SourceDownstream that is used to construct [Error]. The msgID
|
|
// is passed to the caller to serve as the base for user facing error messages.
|
|
//
|
|
// msgID should be structured as component.errorBrief, for example
|
|
//
|
|
// area.downstreamError
|
|
func BadGateway(msgID string, opts ...BaseOpt) Base {
|
|
newOpts := []BaseOpt{WithDownstream()}
|
|
newOpts = append(newOpts, opts...)
|
|
return NewBase(StatusBadGateway, msgID, newOpts...)
|
|
}
|
|
|
|
// GatewayTimeout initializes a new [Base] error with reason StatusGatewayTimeout
|
|
// and source SourceDownstream that is used to construct [Error]. The msgID
|
|
// is passed to the caller to serve as the base for user facing error messages.
|
|
//
|
|
// msgID should be structured as component.errorBrief, for example
|
|
//
|
|
// area.downstreamTimeout
|
|
func GatewayTimeout(msgID string, opts ...BaseOpt) Base {
|
|
newOpts := []BaseOpt{WithDownstream()}
|
|
newOpts = append(newOpts, opts...)
|
|
return NewBase(StatusGatewayTimeout, msgID, newOpts...)
|
|
}
|
|
|
|
type BaseOpt func(Base) Base
|
|
|
|
// WithLogLevel sets a custom log level for all errors instantiated from
|
|
// this [Base].
|
|
//
|
|
// Used as a functional option to [NewBase].
|
|
func WithLogLevel(lvl LogLevel) BaseOpt {
|
|
return func(b Base) Base {
|
|
b.logLevel = lvl
|
|
return b
|
|
}
|
|
}
|
|
|
|
// WithPublicMessage sets the default public message that will be used
|
|
// for errors based on this [Base].
|
|
//
|
|
// Used as a functional option to [NewBase].
|
|
func WithPublicMessage(message string) BaseOpt {
|
|
return func(b Base) Base {
|
|
b.publicMessage = message
|
|
return b
|
|
}
|
|
}
|
|
|
|
// WithDownstream sets the source as SourceDownstream that will be used
|
|
// for errors based on this [Base].
|
|
//
|
|
// Used as a functional option to [NewBase].
|
|
func WithDownstream() BaseOpt {
|
|
return func(b Base) Base {
|
|
b.source = SourceDownstream
|
|
return b
|
|
}
|
|
}
|
|
|
|
// Errorf creates a new [Error] with Reason and MessageID from [Base],
|
|
// and Message and Underlying will be populated using the rules of
|
|
// [fmt.Errorf].
|
|
func (b Base) Errorf(format string, args ...any) Error {
|
|
err := fmt.Errorf(format, args...)
|
|
|
|
return Error{
|
|
Reason: b.reason,
|
|
LogMessage: err.Error(),
|
|
PublicMessage: b.publicMessage,
|
|
MessageID: b.messageID,
|
|
Underlying: errors.Unwrap(err),
|
|
LogLevel: b.logLevel,
|
|
Source: b.source,
|
|
}
|
|
}
|
|
|
|
// Error makes Base implement the error type. Relying on this is
|
|
// discouraged, as the Error type can carry additional information
|
|
// that's valuable when debugging.
|
|
func (b Base) Error() string {
|
|
return b.Errorf("").Error()
|
|
}
|
|
|
|
func (b Base) Status() StatusReason {
|
|
if b.reason == nil {
|
|
return StatusUnknown
|
|
}
|
|
return b.reason.Status()
|
|
}
|
|
|
|
// Is validates that an [Error] has the same reason and messageID as the
|
|
// Base.
|
|
//
|
|
// Implements the interface used by [errors.Is].
|
|
func (b Base) Is(err error) bool {
|
|
// The linter complains that it wants to use errors.As because it
|
|
// handles unwrapping, we don't want to do that here since we want
|
|
// to validate the equality between the two objects.
|
|
// errors.Is handles the unwrapping, should you want it.
|
|
//nolint:errorlint
|
|
base, isBase := err.(Base)
|
|
//nolint:errorlint
|
|
gfErr, isGrafanaError := err.(Error)
|
|
|
|
switch {
|
|
case isGrafanaError:
|
|
return b.reason == gfErr.Reason && b.messageID == gfErr.MessageID
|
|
case isBase:
|
|
return b.reason == base.reason && b.messageID == base.messageID
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Error is the error type for errors within Grafana, extending
|
|
// the Go error type with Grafana specific metadata to reduce
|
|
// boilerplate error handling for status codes and internationalization
|
|
// support.
|
|
//
|
|
// Use [Base.Errorf] or [Template.Build] to construct errors:
|
|
//
|
|
// // package-level
|
|
// var errMonthlyQuota = NewBase(errutil.StatusTooManyRequests, "service.monthlyQuotaReached")
|
|
// // in function
|
|
// err := errMonthlyQuota.Errorf("user '%s' reached their monthly quota for service", userUID)
|
|
//
|
|
// or
|
|
//
|
|
// // package-level
|
|
// var errRateLimited = NewBase(errutil.StatusTooManyRequests, "service.backoff").MustTemplate(
|
|
// "quota reached for user {{ .Private.user }}, rate limited until {{ .Public.time }}",
|
|
// errutil.WithPublic("Too many requests, try again after {{ .Public.time }}"),
|
|
// )
|
|
// // in function
|
|
// err := errRateLimited.Build(TemplateData{
|
|
// Private: map[string]interface{ "user": userUID },
|
|
// Public: map[string]interface{ "time": rateLimitUntil },
|
|
// })
|
|
//
|
|
// Error implements Unwrap and Is to natively support Go 1.13 style
|
|
// errors as described in https://go.dev/blog/go1.13-errors .
|
|
type Error struct {
|
|
// Reason provides the Grafana abstracted reason which can be turned
|
|
// into an upstream status code depending on the protocol. This
|
|
// allows us to use the same errors across HTTP, gRPC, and other
|
|
// protocols.
|
|
Reason StatusReason
|
|
// A MessageID together with PublicPayload should suffice to
|
|
// create the PublicMessage. This lets a localization aware client
|
|
// construct messages based on structured data.
|
|
MessageID string
|
|
// LogMessage will be displayed in the server logs or wherever
|
|
// [Error.Error] is called.
|
|
LogMessage string
|
|
// Underlying is the wrapped error returned by [Error.Unwrap].
|
|
Underlying error
|
|
// PublicMessage is constructed from the template uniquely
|
|
// identified by MessageID and the values in PublicPayload (if any)
|
|
// to provide the end-user with information that they can use to
|
|
// resolve the issue.
|
|
PublicMessage string
|
|
// PublicPayload provides fields for passing structured data to
|
|
// construct localized error messages in the client.
|
|
PublicPayload map[string]any
|
|
// LogLevel provides a suggested level of logging for the error.
|
|
LogLevel LogLevel
|
|
// Source identifies from where the error originates.
|
|
Source Source
|
|
}
|
|
|
|
// MarshalJSON returns an error, we do not want raw [Error]s being
|
|
// marshaled into JSON.
|
|
//
|
|
// Use [Error.Public] to convert the Error into a [PublicError] which
|
|
// can safely be marshaled into JSON. This is not done automatically,
|
|
// as that conversion is lossy.
|
|
func (e Error) MarshalJSON() ([]byte, error) {
|
|
return nil, fmt.Errorf("errutil.Error cannot be directly marshaled into JSON")
|
|
}
|
|
|
|
// Error implements the error interface.
|
|
func (e Error) Error() string {
|
|
return fmt.Sprintf("[%s] %s", e.MessageID, e.LogMessage)
|
|
}
|
|
|
|
// Unwrap is used by errors.As to iterate over the sequence of
|
|
// underlying errors until a matching type is found.
|
|
func (e Error) Unwrap() error {
|
|
return e.Underlying
|
|
}
|
|
|
|
// Is checks whether an error is derived from the error passed as an
|
|
// argument.
|
|
//
|
|
// Implements the interface used by [errors.Is].
|
|
func (e Error) Is(other error) bool {
|
|
// The linter complains that it wants to use errors.As because it
|
|
// handles unwrapping, we don't want to do that here since we want
|
|
// to validate the equality between the two objects.
|
|
// errors.Is handles the unwrapping, should you want it.
|
|
//nolint:errorlint
|
|
o, isGrafanaError := other.(Error)
|
|
//nolint:errorlint
|
|
base, isBase := other.(Base)
|
|
//nolint:errorlint
|
|
templateErr, isTemplateErr := other.(Template)
|
|
|
|
switch {
|
|
case isGrafanaError:
|
|
return o.Reason == e.Reason && o.MessageID == e.MessageID && o.Error() == e.Error()
|
|
case isBase:
|
|
return base.Is(e)
|
|
case isTemplateErr:
|
|
return templateErr.Base.Is(e)
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// PublicError is derived from Error and only contains information
|
|
// available to the end user.
|
|
type PublicError struct {
|
|
StatusCode int `json:"statusCode"`
|
|
MessageID string `json:"messageId"`
|
|
Message string `json:"message,omitempty"`
|
|
Extra map[string]any `json:"extra,omitempty"`
|
|
}
|
|
|
|
// Public returns a subset of the error with non-sensitive information
|
|
// that may be relayed to the caller.
|
|
func (e Error) Public() PublicError {
|
|
message := e.PublicMessage
|
|
if message == "" {
|
|
if e.Reason == StatusUnknown {
|
|
// The unknown status is equal to the empty string.
|
|
message = string(StatusInternal)
|
|
} else {
|
|
message = string(e.Reason.Status())
|
|
}
|
|
}
|
|
|
|
return PublicError{
|
|
StatusCode: e.Reason.Status().HTTPStatus(),
|
|
MessageID: e.MessageID,
|
|
Message: message,
|
|
Extra: e.PublicPayload,
|
|
}
|
|
}
|
|
|
|
// Error implements the error interface.
|
|
func (p PublicError) Error() string {
|
|
return fmt.Sprintf("[%s] %s", p.MessageID, p.Message)
|
|
}
|