errutil

package
v11.1.4-modfix Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 20, 2024 License: AGPL-3.0 Imports: 10 Imported by: 0

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

Examples

Constants

View Source
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

func HasUnifiedLogging(ctx context.Context) bool

func SetUnifiedLogging

func SetUnifiedLogging(ctx context.Context) context.Context

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

func BadGateway(msgID string, opts ...BaseOpt) Base

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

func BadRequest(msgID string, opts ...BaseOpt) Base

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

func ClientClosedRequest(msgID string, opts ...BaseOpt) Base

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

func Conflict(msgID string, opts ...BaseOpt) Base

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

func Forbidden(msgID string, opts ...BaseOpt) Base

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

func GatewayTimeout(msgID string, opts ...BaseOpt) Base

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

func Internal(msgID string, opts ...BaseOpt) Base

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

func NotFound(msgID string, opts ...BaseOpt) Base

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

func NotImplemented(msgID string, opts ...BaseOpt) Base

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 Timeout

func Timeout(msgID string, opts ...BaseOpt) Base

Timeout initializes a new Base error with reason StatusTimeout.

area.timeout

func TooManyRequests

func TooManyRequests(msgID string, opts ...BaseOpt) Base

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

func Unauthorized(msgID string, opts ...BaseOpt) Base

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

func UnprocessableEntity(msgID string, opts ...BaseOpt) Base

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

func ValidationFailed(msgID string, opts ...BaseOpt) Base

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

func (b Base) Error() string

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

func (b Base) Errorf(format string, args ...any) Error

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

func (b Base) Is(err error) bool

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

func (Base) Template

func (b Base) Template(pattern string, opts ...TemplateOpt) (Template, error)

Template provides templating for converting Base to Error. This is useful where the public payload is populated with fields that should be present in the internal error representation.

type BaseOpt

type BaseOpt func(Base) Base

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

func WithLogLevel(lvl LogLevel) BaseOpt

WithLogLevel sets a custom log level for all errors instantiated from this Base.

Used as a functional option to NewBase.

func WithPublicMessage

func WithPublicMessage(message string) BaseOpt

WithPublicMessage sets the default public message that will be used for errors based on 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 = ""
	// StatusUnauthorized means that the server does not recognize the
	// 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) Error

func (e Error) Error() string

Error implements the error interface.

func (Error) Is

func (e Error) Is(other error) bool

Is checks whether an error is derived from the error passed as an argument.

Implements the interface used by errors.Is.

func (Error) MarshalJSON

func (e Error) MarshalJSON() ([]byte, error)

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.

func (Error) Status

func (e Error) Status() metav1.Status

When the error is rendered by an apiserver, this format is used

func (Error) Unwrap

func (e Error) Unwrap() error

Unwrap is used by errors.As to iterate over the sequence of underlying errors until a matching type is found.

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 LogLevel

type LogLevel string
const (
	LevelUnknown LogLevel = ""
	LevelNever   LogLevel = "never"
	LevelDebug   LogLevel = "debug"
	LevelInfo    LogLevel = "info"
	LevelWarn    LogLevel = "warn"
	LevelError   LogLevel = "error"
)

func (LogLevel) HighestOf

func (l LogLevel) HighestOf(other LogLevel) LogLevel

func (LogLevel) LogFunc

func (l LogLevel) LogFunc(logger LogInterface) func(msg string, ctx ...any)

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.

const (
	// SourceServer implies error originates from within the server, i.e. this application.
	SourceServer Source = "server"

	// SourceDownstream implies error originates from response error while server acting
	// as a proxy, i.e. from a downstream service.
	SourceDownstream Source = "downstream"
)

func (Source) IsDownstream

func (s Source) IsDownstream() bool

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.

func (Template) Error

func (t Template) Error() string

type TemplateData

type TemplateData struct {
	Private map[string]any
	Public  map[string]any
	Error   error
}

TemplateData contains data for constructing an Error based on a Template.

type TemplateOpt

type TemplateOpt func(Template) (Template, error)

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.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL