mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Errutil: Update documentation for Go 1.19 (#55807)
This commit is contained in:
parent
c2d3a31772
commit
22756913ba
@ -17,20 +17,20 @@
|
||||
// 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
|
||||
// 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 general structure of the public
|
||||
// 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.not-found")
|
||||
// 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:
|
||||
// [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)
|
||||
//
|
||||
@ -39,11 +39,11 @@
|
||||
// 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.not-found", WithPublicMessage("The thing is missing."))
|
||||
// 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 from the standard library, refer to
|
||||
// the documentation related to the Template type for usage examples.
|
||||
// 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.
|
||||
package errutil
|
||||
|
@ -6,7 +6,7 @@ import (
|
||||
)
|
||||
|
||||
// Base represents the static information about a specific error.
|
||||
// Always use NewBase to create new instances of Base.
|
||||
// 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
|
||||
@ -17,16 +17,16 @@ type Base struct {
|
||||
logLevel LogLevel
|
||||
}
|
||||
|
||||
// NewBase initializes a Base that is used to construct Error:s.
|
||||
// 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.error-brief, for example
|
||||
// msgID should be structured as component.errorBrief, for example
|
||||
//
|
||||
// login.failed-authentication
|
||||
// dashboards.validation-error
|
||||
// dashboards.uid-already-exists
|
||||
// login.failedAuthentication
|
||||
// dashboards.validationError
|
||||
// dashboards.uidAlreadyExists
|
||||
func NewBase(reason StatusReason, msgID string, opts ...BaseOpt) Base {
|
||||
b := Base{
|
||||
reason: reason,
|
||||
@ -44,9 +44,9 @@ func NewBase(reason StatusReason, msgID string, opts ...BaseOpt) Base {
|
||||
type BaseOpt func(Base) Base
|
||||
|
||||
// WithLogLevel sets a custom log level for all errors instantiated from
|
||||
// this Base.
|
||||
// this [Base].
|
||||
//
|
||||
// Used as a functional option to NewBase.
|
||||
// Used as a functional option to [NewBase].
|
||||
func WithLogLevel(lvl LogLevel) BaseOpt {
|
||||
return func(b Base) Base {
|
||||
b.logLevel = lvl
|
||||
@ -55,9 +55,9 @@ func WithLogLevel(lvl LogLevel) BaseOpt {
|
||||
}
|
||||
|
||||
// WithPublicMessage sets the default public message that will be used
|
||||
// for errors based on this Base.
|
||||
// for errors based on this [Base].
|
||||
//
|
||||
// Used as a functional option to NewBase.
|
||||
// Used as a functional option to [NewBase].
|
||||
func WithPublicMessage(message string) BaseOpt {
|
||||
return func(b Base) Base {
|
||||
b.publicMessage = message
|
||||
@ -65,9 +65,9 @@ func WithPublicMessage(message string) BaseOpt {
|
||||
}
|
||||
}
|
||||
|
||||
// Errorf creates a new Error with the Reason and MessageID from
|
||||
// Base, and Message and Underlying will be populated using
|
||||
// the rules of fmt.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 (b Base) Errorf(format string, args ...interface{}) Error {
|
||||
err := fmt.Errorf(format, args...)
|
||||
|
||||
@ -95,8 +95,10 @@ func (b Base) Status() StatusReason {
|
||||
return b.reason.Status()
|
||||
}
|
||||
|
||||
// Is validates that an Error has the same reason and messageID as the
|
||||
// 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
|
||||
@ -122,24 +124,61 @@ func (b Base) Is(err error) bool {
|
||||
// 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]interface{}
|
||||
// LogLevel provides a suggested level of logging for the error.
|
||||
LogLevel LogLevel
|
||||
}
|
||||
|
||||
// MarshalJSON returns an error, we do not want raw Error:s being
|
||||
// MarshalJSON returns an error, we do not want raw [Error]s being
|
||||
// marshaled into JSON.
|
||||
//
|
||||
// Use Public to convert the Error into a PublicError which can be
|
||||
// marshaled. This is not done automatically, as that conversion is
|
||||
// lossy.
|
||||
// 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")
|
||||
}
|
||||
@ -155,8 +194,10 @@ func (e Error) Unwrap() error {
|
||||
return e.Underlying
|
||||
}
|
||||
|
||||
// Is is used by errors.Is to allow for custom definitions of equality
|
||||
// between two errors.
|
||||
// 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
|
||||
|
@ -15,8 +15,8 @@ var (
|
||||
// same error message for the frontend statically within the
|
||||
// package.
|
||||
|
||||
errAbsPath = errutil.NewBase(errutil.StatusBadRequest, "shorturl.absolute-path")
|
||||
errInvalidPath = errutil.NewBase(errutil.StatusBadRequest, "shorturl.invalid-path")
|
||||
errAbsPath = errutil.NewBase(errutil.StatusBadRequest, "shorturl.absolutePath")
|
||||
errInvalidPath = errutil.NewBase(errutil.StatusBadRequest, "shorturl.invalidPath")
|
||||
errUnexpected = errutil.NewBase(errutil.StatusInternal, "shorturl.unexpected")
|
||||
)
|
||||
|
||||
@ -29,8 +29,8 @@ func Example() {
|
||||
fmt.Println(e.Error())
|
||||
|
||||
// Output:
|
||||
// 400 shorturl.invalid-path
|
||||
// [shorturl.invalid-path] path mustn't contain '..': 'abc/../def'
|
||||
// 400 shorturl.invalidPath
|
||||
// [shorturl.invalidPath] path mustn't contain '..': 'abc/../def'
|
||||
}
|
||||
|
||||
// CreateShortURL runs a few validations and returns
|
||||
|
@ -10,8 +10,8 @@ import (
|
||||
)
|
||||
|
||||
func TestBase_Is(t *testing.T) {
|
||||
baseNotFound := NewBase(StatusNotFound, "test:not-found")
|
||||
baseInternal := NewBase(StatusInternal, "test:internal")
|
||||
baseNotFound := NewBase(StatusNotFound, "test.notFound")
|
||||
baseInternal := NewBase(StatusInternal, "test.internal")
|
||||
|
||||
tests := []struct {
|
||||
Base Base
|
||||
|
@ -50,7 +50,7 @@ type TemplateOpt func(Template) (Template, error)
|
||||
|
||||
// MustTemplate panics if the template for Template cannot be compiled.
|
||||
//
|
||||
// Only useful for global or package level initialization of Template:s.
|
||||
// Only useful for global or package level initialization of [Template].
|
||||
func (b Base) MustTemplate(pattern string, opts ...TemplateOpt) Template {
|
||||
res, err := b.Template(pattern, opts...)
|
||||
if err != nil {
|
||||
@ -63,7 +63,7 @@ func (b Base) MustTemplate(pattern string, opts ...TemplateOpt) Template {
|
||||
// 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.
|
||||
// Used as a functional option to [Base.Template].
|
||||
func WithPublic(pattern string) TemplateOpt {
|
||||
return func(t Template) (Template, error) {
|
||||
var err error
|
||||
@ -77,7 +77,7 @@ func WithPublic(pattern string) TemplateOpt {
|
||||
// TemplateData.Error and TemplateData.Private will not be populated
|
||||
// when rendering the public message.
|
||||
//
|
||||
// Used as a functional option to Base.Template.
|
||||
// Used as a functional option to [Base.Template].
|
||||
func WithPublicFromLog() TemplateOpt {
|
||||
return func(t Template) (Template, error) {
|
||||
t.publicTemplate = t.logTemplate
|
||||
@ -85,8 +85,8 @@ func WithPublicFromLog() TemplateOpt {
|
||||
}
|
||||
}
|
||||
|
||||
// Build returns a new Error based on the base Template and the provided
|
||||
// TemplateData, wrapping the error in TemplateData.Error if set.
|
||||
// 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 (t Template) Build(data TemplateData) error {
|
||||
|
@ -5,12 +5,13 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
)
|
||||
|
||||
func TestTemplate(t *testing.T) {
|
||||
tmpl := errutil.NewBase(errutil.StatusInternal, "template.sample-error").MustTemplate("[{{ .Public.user }}] got error: {{ .Error }}")
|
||||
tmpl := errutil.NewBase(errutil.StatusInternal, "template.sampleError").MustTemplate("[{{ .Public.user }}] got error: {{ .Error }}")
|
||||
err := tmpl.Build(errutil.TemplateData{
|
||||
Public: map[string]interface{}{
|
||||
"user": "grot the bot",
|
||||
@ -30,7 +31,7 @@ func TestTemplate(t *testing.T) {
|
||||
func ExampleTemplate() {
|
||||
// Initialization, this is typically done on a package or global
|
||||
// level.
|
||||
var tmpl = errutil.NewBase(errutil.StatusInternal, "template.sample-error").MustTemplate("[{{ .Public.user }}] got error: {{ .Error }}")
|
||||
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{
|
||||
@ -43,14 +44,14 @@ func ExampleTemplate() {
|
||||
fmt.Println(err.Error())
|
||||
|
||||
// Output:
|
||||
// [template.sample-error] [grot the bot] got error: oh noes
|
||||
// [template.sampleError] [grot the bot] got error: oh noes
|
||||
}
|
||||
|
||||
func ExampleTemplate_public() {
|
||||
// Initialization, this is typically done on a package or global
|
||||
// level.
|
||||
var tmpl = errutil.
|
||||
NewBase(errutil.StatusInternal, "template.sample-error").
|
||||
NewBase(errutil.StatusInternal, "template.sampleError").
|
||||
MustTemplate(
|
||||
"[{{ .Public.user }}] got error: {{ .Error }}",
|
||||
errutil.WithPublic("Oh, no, error for {{ .Public.user }}"),
|
||||
@ -69,6 +70,6 @@ func ExampleTemplate_public() {
|
||||
fmt.Println(err.PublicMessage)
|
||||
|
||||
// Output:
|
||||
// [template.sample-error] [grot the bot] got error: oh noes
|
||||
// [template.sampleError] [grot the bot] got error: oh noes
|
||||
// Oh, no, error for grot the bot
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user