grafana/pkg/util/errutil/errors.go
Marcus Efraimsson 90631360eb
Instrumentation: Handle context.Canceled (#75867)
Ref #68480

Co-authored-by: Giuseppe Guerra <giuseppe.guerra@grafana.com>
2023-10-10 12:28:39 +02:00

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)
}