diff --git a/pkg/util/errutil/doc.go b/pkg/util/errutil/doc.go index 884191be88c..23624bdc619 100644 --- a/pkg/util/errutil/doc.go +++ b/pkg/util/errutil/doc.go @@ -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 diff --git a/pkg/util/errutil/errors.go b/pkg/util/errutil/errors.go index 6f20a90034c..1238427c200 100644 --- a/pkg/util/errutil/errors.go +++ b/pkg/util/errutil/errors.go @@ -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 StatusReason - MessageID string - LogMessage string - Underlying error + // 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 LogLevel + // 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 diff --git a/pkg/util/errutil/errors_example_test.go b/pkg/util/errutil/errors_example_test.go index fb85f9b2967..da06df3196c 100644 --- a/pkg/util/errutil/errors_example_test.go +++ b/pkg/util/errutil/errors_example_test.go @@ -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 diff --git a/pkg/util/errutil/errors_test.go b/pkg/util/errutil/errors_test.go index 3db114c2581..ac3292d5b91 100644 --- a/pkg/util/errutil/errors_test.go +++ b/pkg/util/errutil/errors_test.go @@ -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 diff --git a/pkg/util/errutil/template.go b/pkg/util/errutil/template.go index 77d297bce52..86b97c40ac1 100644 --- a/pkg/util/errutil/template.go +++ b/pkg/util/errutil/template.go @@ -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 { diff --git a/pkg/util/errutil/template_test.go b/pkg/util/errutil/template_test.go index 7a732db1a2d..4eaa3ec0285 100644 --- a/pkg/util/errutil/template_test.go +++ b/pkg/util/errutil/template_test.go @@ -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 }