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