The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/util/errutil/errors.go

435 lines
14 KiB

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...)
}
// UnprocessableContent initializes a new [Base] error with reason StatusUnprocessableEntity
// 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.checksumMismatch
func UnprocessableEntity(msgID string, opts ...BaseOpt) Base {
return NewBase(StatusUnprocessableEntity, msgID, opts...)
}
// Conflict initializes a new [Base] error with reason StatusConflict
// 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.alreadyExists
func Conflict(msgID string, opts ...BaseOpt) Base {
return NewBase(StatusConflict, 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)
}