errutil

package
v0.0.0-kmdagger1 Latest Latest
Warning

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

Go to latest
Published: Jun 8, 2023 License: AGPL-3.0 Imports: 6 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.NewBase(errutil.StatusBadRequest, "shorturl.absolutePath")
	errInvalidPath = errutil.NewBase(errutil.StatusBadRequest, "shorturl.invalidPath")
	errUnexpected  = errutil.NewBase(errutil.StatusInternal, "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

This section is empty.

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 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 (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 ...interface{}) 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 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 string
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 = "Unauthorized"
	// StatusForbidden means that the server refuses to perform the
	// requested action for the authenticated uer.
	// HTTP status code 403.
	StatusForbidden CoreStatus = "Forbidden"
	// StatusNotFound means that the server does not have any
	// corresponding document to return to the request.
	// HTTP status code 404.
	StatusNotFound CoreStatus = "Not found"
	// StatusTooManyRequests means that the client is rate limited
	// by the server and should back-off before trying again.
	// HTTP status code 429.
	StatusTooManyRequests CoreStatus = "Too many requests"
	// StatusBadRequest means that the server was unable to parse the
	// parameters or payload for the request.
	// HTTP status code 400.
	StatusBadRequest CoreStatus = "Bad 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 = "Internal server error"
	// StatusTimeout means that the server did not complete the request
	// within the required time and aborted the action.
	// HTTP status code 504.
	StatusTimeout CoreStatus = "Timeout"
	// 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"
)

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]interface{}
	// LogLevel provides a suggested level of logging for the error.
	LogLevel LogLevel
}

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) 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 ...interface{})
	Info(msg string, ctx ...interface{})
	Warn(msg string, ctx ...interface{})
	Error(msg string, ctx ...interface{})
}

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 ...interface{})

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]interface{} `json:"extra,omitempty"`
}

PublicError is derived from Error and only contains information available to the end user.

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.NewBase(errutil.StatusInternal, "template.sampleError").MustTemplate("[{{ .Public.user }}] got error: {{ .Error }}")

	// Construct an error based on the template.
	err := tmpl.Build(errutil.TemplateData{
		Public: map[string]interface{}{
			"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.
		NewBase(errutil.StatusInternal, "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]interface{}{
			"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]interface{}
	Public  map[string]interface{}
	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