Documentation ¶
Overview ¶
Package errutil provides utilities for working with errors in Grafana.
Idiomatic errors in Grafana provides a combination of static and dynamic information that is useful to developers, system administrators and end users alike.
Grafana itself can use the static information to infer the general category of error, retryability, log levels, and similar. A developer can combine static and dynamic information from logs to determine what went wrong and where even when access to the runtime environment is impossible. Server admins can use the information from the logs to monitor the health of their Grafana instance. End users will receive an appropriate amount of information to be able to correctly determine the best course of action when receiving an error.
It is also important that implementing errors idiomatically comes naturally to experienced and beginner Go developers alike and is compatible with standard library features such as the ones in the errors package. To achieve this, Grafana's errors are divided into the Base and Error types, where the Base contains static information about a category of errors that may occur within a service and Error contains the combination of static and dynamic information for a particular instance of an error.
A Base would typically be provided as a package-level variable for a service using the NewBase constructor with a CoreStatus and a unique static message ID that identifies the structure of the public message attached to the specific error.
var errNotFound = errutil.NewBase(errutil.StatusNotFound, "service.notFound")
This Base can now be used to construct a regular Go error with the Base.Errorf method using the same structure as fmt.Errorf:
return errNotFound.Errorf("looked for thing with ID %d, but it wasn't there: %w", id, err)
By default, the end user will be sent the static message ID and a message which is the string representation of the CoreStatus. It is possible to override the message sent to the end user by using the WithPublicMessage functional option when creating a new Base
var errNotFound = errutil.NewBase(errutil.StatusNotFound "service.notFound", WithPublicMessage("The thing is missing."))
If a dynamic message is needed, the Template type extends Base with a Go template using text/template, refer to the documentation related to the Template type for usage examples. It is also possible, but discouraged, to manually edit the fields of an Error.
Example ¶
package main import ( "context" "errors" "fmt" "path" "strings" "github.com/grafana/grafana/pkg/util/errutil" ) var ( // define the set of errors which should be presented using the // same error message for the frontend statically within the // package. errAbsPath = errutil.BadRequest("shorturl.absolutePath") errInvalidPath = errutil.BadRequest("shorturl.invalidPath") errUnexpected = errutil.Internal("shorturl.unexpected") ) func main() { var e errutil.Error _, err := CreateShortURL("abc/../def") errors.As(err, &e) fmt.Println(e.Reason.Status().HTTPStatus(), e.MessageID) fmt.Println(e.Error()) } // CreateShortURL runs a few validations and returns // 'https://example.org/s/tretton' if they all pass. It's not a very // useful function, but it shows errors in a semi-realistic function. func CreateShortURL(longURL string) (string, error) { if path.IsAbs(longURL) { return "", errAbsPath.Errorf("unexpected absolute path") } if strings.Contains(longURL, "../") { return "", errInvalidPath.Errorf("path mustn't contain '..': '%s'", longURL) } if strings.Contains(longURL, "@") { return "", errInvalidPath.Errorf("cannot shorten email addresses") } shortURL, err := createShortURL(context.Background(), longURL) if err != nil { return "", errUnexpected.Errorf("failed to create short URL: %w", err) } return shortURL, nil } func createShortURL(_ context.Context, _ string) (string, error) { return "https://example.org/s/tretton", nil }
Output: 400 shorturl.invalidPath [shorturl.invalidPath] path mustn't contain '..': 'abc/../def'
Index ¶
- Constants
- func HasUnifiedLogging(ctx context.Context) bool
- func SetUnifiedLogging(ctx context.Context) context.Context
- type Base
- func BadGateway(msgID string, opts ...BaseOpt) Base
- func BadRequest(msgID string, opts ...BaseOpt) Base
- func ClientClosedRequest(msgID string, opts ...BaseOpt) Base
- func Conflict(msgID string, opts ...BaseOpt) Base
- func Forbidden(msgID string, opts ...BaseOpt) Base
- func GatewayTimeout(msgID string, opts ...BaseOpt) Base
- func Internal(msgID string, opts ...BaseOpt) Base
- func NewBase(reason StatusReason, msgID string, opts ...BaseOpt) Base
- func NotFound(msgID string, opts ...BaseOpt) Base
- func NotImplemented(msgID string, opts ...BaseOpt) Base
- func Timeout(msgID string, opts ...BaseOpt) Base
- func TooManyRequests(msgID string, opts ...BaseOpt) Base
- func Unauthorized(msgID string, opts ...BaseOpt) Base
- func UnprocessableEntity(msgID string, opts ...BaseOpt) Base
- func ValidationFailed(msgID string, opts ...BaseOpt) Base
- func (b Base) Error() string
- func (b Base) Errorf(format string, args ...any) Error
- func (b Base) Is(err error) bool
- func (b Base) MustTemplate(pattern string, opts ...TemplateOpt) Template
- func (b Base) Status() StatusReason
- func (b Base) Template(pattern string, opts ...TemplateOpt) (Template, error)
- type BaseOpt
- type CoreStatus
- type Error
- type LogInterface
- type LogLevel
- type PluginStatus
- type ProxyStatus
- type PublicError
- type Source
- type StatusReason
- type Template
- type TemplateData
- type TemplateOpt
Examples ¶
Constants ¶
const HTTPStatusClientClosedRequest = 499
HTTPStatusClientClosedRequest A non-standard status code introduced by nginx for the case when a client closes the connection while nginx is processing the request. See https://httpstatus.in/499/ for more information.
Variables ¶
This section is empty.
Functions ¶
func HasUnifiedLogging ¶
Types ¶
type Base ¶
type Base struct {
// contains filtered or unexported fields
}
Base represents the static information about a specific error. Always use NewBase to create new instances of Base.
func BadGateway ¶
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 BadRequest ¶
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 ClientClosedRequest ¶
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 Conflict ¶
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 Forbidden ¶
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 GatewayTimeout ¶
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 Internal ¶
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 NewBase ¶
func NewBase(reason StatusReason, msgID string, opts ...BaseOpt) Base
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 NotFound ¶
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 NotImplemented ¶
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 TooManyRequests ¶
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 Unauthorized ¶
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 UnprocessableEntity ¶
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 ValidationFailed ¶
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 (Base) Error ¶
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 (Base) Errorf ¶
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 (Base) Is ¶
Is validates that an Error has the same reason and messageID as the Base.
Implements the interface used by errors.Is.
func (Base) MustTemplate ¶
func (b Base) MustTemplate(pattern string, opts ...TemplateOpt) Template
MustTemplate panics if the template for Template cannot be compiled.
Only useful for global or package level initialization of Template.
func (Base) Status ¶
func (b Base) Status() StatusReason
type BaseOpt ¶
func WithDownstream ¶
func WithDownstream() BaseOpt
WithDownstream sets the source as SourceDownstream that will be used for errors based on this Base.
Used as a functional option to NewBase.
func WithLogLevel ¶
WithLogLevel sets a custom log level for all errors instantiated from this Base.
Used as a functional option to NewBase.
type CoreStatus ¶
type CoreStatus metav1.StatusReason
const ( // StatusUnknown implies an error that should be updated to contain // an accurate status code, as none has been provided. // HTTP status code 500. StatusUnknown CoreStatus = "" // client's authentication, either because it has not been provided // or is invalid for the operation. // HTTP status code 401. StatusUnauthorized CoreStatus = CoreStatus(metav1.StatusReasonUnauthorized) // StatusForbidden means that the server refuses to perform the // requested action for the authenticated uer. // HTTP status code 403. StatusForbidden CoreStatus = CoreStatus(metav1.StatusReasonForbidden) // StatusNotFound means that the server does not have any // corresponding document to return to the request. // HTTP status code 404. StatusNotFound CoreStatus = CoreStatus(metav1.StatusReasonNotFound) // StatusUnprocessableEntity means that the server understands the request, // the content type and the syntax but it was unable to process the // contained instructions. // HTTP status code 422. StatusUnprocessableEntity CoreStatus = "Unprocessable Entity" // StatusConflict means that the server cannot fulfill the request // there is a conflict in the current state of a resource // HTTP status code 409. StatusConflict CoreStatus = CoreStatus(metav1.StatusReasonConflict) // StatusTooManyRequests means that the client is rate limited // by the server and should back-off before trying again. // HTTP status code 429. StatusTooManyRequests CoreStatus = CoreStatus(metav1.StatusReasonTooManyRequests) // StatusBadRequest means that the server was unable to parse the // parameters or payload for the request. // HTTP status code 400. StatusBadRequest CoreStatus = CoreStatus(metav1.StatusReasonBadRequest) // StatusClientClosedRequest means that a client closes the connection // while the server is processing the request. // // This is a non-standard HTTP status code introduced by nginx, see // https://httpstatus.in/499/ for more information. // HTTP status code 499. StatusClientClosedRequest CoreStatus = "Client closed request" // StatusValidationFailed means that the server was able to parse // the payload for the request but it failed one or more validation // checks. // HTTP status code 400. StatusValidationFailed CoreStatus = "Validation failed" // StatusInternal means that the server acknowledges that there's // an error, but that there is nothing the client can do to fix it. // HTTP status code 500. StatusInternal CoreStatus = CoreStatus(metav1.StatusReasonInternalError) // StatusTimeout means that the server did not complete the request // within the required time and aborted the action. // HTTP status code 504. StatusTimeout CoreStatus = CoreStatus(metav1.StatusReasonTimeout) // StatusNotImplemented means that the server does not support the // requested action. Typically used during development of new // features. // HTTP status code 501. StatusNotImplemented CoreStatus = "Not implemented" // StatusBadGateway means that the server, while acting as a proxy, // received an invalid response from the downstream server. // HTTP status code 502. StatusBadGateway CoreStatus = "Bad gateway" // StatusGatewayTimeout means that the server, while acting as a proxy, // did not receive a timely response from a downstream server it needed // to access in order to complete the request. // HTTP status code 504. StatusGatewayTimeout CoreStatus = "Gateway timeout" )
func (CoreStatus) HTTPStatus ¶
func (s CoreStatus) HTTPStatus() int
HTTPStatus converts the CoreStatus to an HTTP status code.
func (CoreStatus) LogLevel ¶
func (s CoreStatus) LogLevel() LogLevel
LogLevel returns the default LogLevel for the CoreStatus.
func (CoreStatus) Status ¶
func (s CoreStatus) Status() CoreStatus
Status implements the StatusReason interface.
func (CoreStatus) String ¶
func (s CoreStatus) String() string
type Error ¶
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 }
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 .
func (Error) Is ¶
Is checks whether an error is derived from the error passed as an argument.
Implements the interface used by errors.Is.
func (Error) MarshalJSON ¶
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 (Error) Public ¶
func (e Error) Public() PublicError
Public returns a subset of the error with non-sensitive information that may be relayed to the caller.
type LogInterface ¶
type LogInterface interface { Debug(msg string, ctx ...any) Info(msg string, ctx ...any) Warn(msg string, ctx ...any) Error(msg string, ctx ...any) }
LogInterface is a subset of github.com/grafana/grafana/pkg/infra/log.Logger to avoid having to depend on other packages in the module so that there's no risk of circular dependencies.
type PluginStatus ¶
type PluginStatus CoreStatus
PluginStatus implies that an error originated from a plugin.
func (PluginStatus) Status ¶
func (s PluginStatus) Status() CoreStatus
Status implements the StatusReason interface.
type ProxyStatus ¶
type ProxyStatus CoreStatus
ProxyStatus implies that an error originated from the data source proxy.
func (ProxyStatus) Status ¶
func (s ProxyStatus) Status() CoreStatus
Status implements the StatusReason interface.
type PublicError ¶
type PublicError struct { StatusCode int `json:"statusCode"` MessageID string `json:"messageId"` Message string `json:"message,omitempty"` Extra map[string]any `json:"extra,omitempty"` }
PublicError is derived from Error and only contains information available to the end user.
func (PublicError) Error ¶
func (p PublicError) Error() string
Error implements the error interface.
type Source ¶
type Source string
Source identifies from where an error originates.
func (Source) IsDownstream ¶
IsDownstream checks if Source is SourceDownstream.
type StatusReason ¶
type StatusReason interface {
Status() CoreStatus
}
StatusReason allows for wrapping of CoreStatus.
type Template ¶
type Template struct { Base Base // contains filtered or unexported fields }
Template is an extended Base for when using templating to construct error messages.
Example ¶
package main import ( "errors" "fmt" "github.com/grafana/grafana/pkg/util/errutil" ) func main() { // Initialization, this is typically done on a package or global // level. var tmpl = errutil.Internal("template.sampleError").MustTemplate("[{{ .Public.user }}] got error: {{ .Error }}") // Construct an error based on the template. err := tmpl.Build(errutil.TemplateData{ Public: map[string]any{ "user": "grot the bot", }, Error: errors.New("oh noes"), }) fmt.Println(err.Error()) }
Output: [template.sampleError] [grot the bot] got error: oh noes
Example (Public) ¶
package main import ( "errors" "fmt" "github.com/grafana/grafana/pkg/util/errutil" ) func main() { // Initialization, this is typically done on a package or global // level. var tmpl = errutil.Internal("template.sampleError").MustTemplate( "[{{ .Public.user }}] got error: {{ .Error }}", errutil.WithPublic("Oh, no, error for {{ .Public.user }}"), ) // Construct an error based on the template. //nolint:errorlint err := tmpl.Build(errutil.TemplateData{ Public: map[string]any{ "user": "grot the bot", }, Error: errors.New("oh noes"), }).(errutil.Error) fmt.Println(err.Error()) fmt.Println(err.PublicMessage) }
Output: [template.sampleError] [grot the bot] got error: oh noes Oh, no, error for grot the bot
func (Template) Build ¶
func (t Template) Build(data TemplateData) error
Build returns a new Error based on the base Template and the provided TemplateData, wrapping the error in TemplateData.Error.
Build can fail and return an error that is not of type Error.
type TemplateData ¶
TemplateData contains data for constructing an Error based on a Template.
type TemplateOpt ¶
func WithPublic ¶
func WithPublic(pattern string) TemplateOpt
WithPublic provides templating for the user facing error message based on only the fields available in TemplateData.Public.
Used as a functional option to Base.Template.
func WithPublicFromLog ¶
func WithPublicFromLog() TemplateOpt
WithPublicFromLog copies over the template for the log message to be used for the user facing error message. TemplateData.Error and TemplateData.Private will not be populated when rendering the public message.
Used as a functional option to Base.Template.