Chore: Move identity and errutil to apimachinery module (#89116)

This commit is contained in:
Ryan McKinley
2024-06-13 07:11:35 +03:00
committed by GitHub
parent 12a45fdeca
commit 99d8025829
298 changed files with 575 additions and 393 deletions
@@ -9,8 +9,8 @@ import (
"k8s.io/apiserver/pkg/endpoints/request"
"github.com/grafana/grafana/pkg/apimachinery/errutil"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/util/errutil"
)
var ErrNonGrafanaError = errutil.Internal("core.MalformedError")
@@ -9,7 +9,7 @@ import (
"github.com/stretchr/testify/assert"
"k8s.io/apiserver/pkg/endpoints/request"
"github.com/grafana/grafana/pkg/util/errutil"
"github.com/grafana/grafana/pkg/apimachinery/errutil"
)
func TestWrite(t *testing.T) {
-49
View File
@@ -1,49 +0,0 @@
// 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.
package errutil
-471
View File
@@ -1,471 +0,0 @@
package errutil
import (
"encoding/json"
"errors"
"fmt"
errorsK8s "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)
// Base represents the static information about a specific error.
// 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
// bug messing with the error base.
reason StatusReason
messageID string
publicMessage string
logLevel LogLevel
source Source
}
// 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 NewBase(reason StatusReason, msgID string, opts ...BaseOpt) Base {
b := Base{
reason: reason,
messageID: msgID,
logLevel: reason.Status().LogLevel(),
source: SourceServer,
}
for _, opt := range opts {
b = opt(b)
}
return b
}
// 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 NotFound(msgID string, opts ...BaseOpt) Base {
return NewBase(StatusNotFound, msgID, opts...)
}
// 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 UnprocessableEntity(msgID string, opts ...BaseOpt) Base {
return NewBase(StatusUnprocessableEntity, msgID, opts...)
}
// 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 Conflict(msgID string, opts ...BaseOpt) Base {
return NewBase(StatusConflict, msgID, opts...)
}
// 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 BadRequest(msgID string, opts ...BaseOpt) Base {
return NewBase(StatusBadRequest, msgID, opts...)
}
// 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 ValidationFailed(msgID string, opts ...BaseOpt) Base {
return NewBase(StatusValidationFailed, msgID, opts...)
}
// 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 Internal(msgID string, opts ...BaseOpt) Base {
return NewBase(StatusInternal, msgID, opts...)
}
// Timeout initializes a new [Base] error with reason StatusTimeout.
//
// area.timeout
func Timeout(msgID string, opts ...BaseOpt) Base {
return NewBase(StatusTimeout, msgID, opts...)
}
// 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 Unauthorized(msgID string, opts ...BaseOpt) Base {
return NewBase(StatusUnauthorized, msgID, opts...)
}
// 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 Forbidden(msgID string, opts ...BaseOpt) Base {
return NewBase(StatusForbidden, msgID, opts...)
}
// 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 TooManyRequests(msgID string, opts ...BaseOpt) Base {
return NewBase(StatusTooManyRequests, msgID, opts...)
}
// 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 ClientClosedRequest(msgID string, opts ...BaseOpt) Base {
return NewBase(StatusClientClosedRequest, msgID, opts...)
}
// 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 NotImplemented(msgID string, opts ...BaseOpt) Base {
return NewBase(StatusNotImplemented, msgID, opts...)
}
// 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 BadGateway(msgID string, opts ...BaseOpt) Base {
newOpts := []BaseOpt{WithDownstream()}
newOpts = append(newOpts, opts...)
return NewBase(StatusBadGateway, msgID, newOpts...)
}
// 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 GatewayTimeout(msgID string, opts ...BaseOpt) Base {
newOpts := []BaseOpt{WithDownstream()}
newOpts = append(newOpts, opts...)
return NewBase(StatusGatewayTimeout, msgID, newOpts...)
}
type BaseOpt func(Base) Base
// WithLogLevel sets a custom log level for all errors instantiated from
// this [Base].
//
// Used as a functional option to [NewBase].
func WithLogLevel(lvl LogLevel) BaseOpt {
return func(b Base) Base {
b.logLevel = lvl
return b
}
}
// WithPublicMessage sets the default public message that will be used
// for errors based on this [Base].
//
// Used as a functional option to [NewBase].
func WithPublicMessage(message string) BaseOpt {
return func(b Base) Base {
b.publicMessage = message
return b
}
}
// WithDownstream sets the source as SourceDownstream that will be used
// for errors based on this [Base].
//
// Used as a functional option to [NewBase].
func WithDownstream() BaseOpt {
return func(b Base) Base {
b.source = SourceDownstream
return b
}
}
// 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 ...any) Error {
err := fmt.Errorf(format, args...)
return Error{
Reason: b.reason,
LogMessage: err.Error(),
PublicMessage: b.publicMessage,
MessageID: b.messageID,
Underlying: errors.Unwrap(err),
LogLevel: b.logLevel,
Source: b.source,
}
}
// 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 (b Base) Error() string {
return b.Errorf("").Error()
}
func (b Base) Status() StatusReason {
if b.reason == nil {
return StatusUnknown
}
return b.reason.Status()
}
// 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
// to validate the equality between the two objects.
// errors.Is handles the unwrapping, should you want it.
//nolint:errorlint
base, isBase := err.(Base)
//nolint:errorlint
gfErr, isGrafanaError := err.(Error)
switch {
case isGrafanaError:
return b.reason == gfErr.Reason && b.messageID == gfErr.MessageID
case isBase:
return b.reason == base.reason && b.messageID == base.messageID
default:
return false
}
}
// Allow errorutil errors to be returned as informative k8s errors
var _ = errorsK8s.APIStatus(&Error{})
// 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 .
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
}
// 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 (e Error) MarshalJSON() ([]byte, error) {
return nil, fmt.Errorf("errutil.Error cannot be directly marshaled into JSON")
}
// Error implements the error interface.
func (e Error) Error() string {
return fmt.Sprintf("[%s] %s", e.MessageID, e.LogMessage)
}
// When the error is rendered by an apiserver, this format is used
func (e Error) Status() metav1.Status {
public := e.Public()
s := metav1.Status{
Status: metav1.StatusFailure,
Code: int32(public.StatusCode),
Reason: metav1.StatusReason(e.Reason.Status()), // almost true
Message: public.Message,
}
// Shove the extra data into details
if public.Extra != nil || public.MessageID != "" {
s.Details = &metav1.StatusDetails{
UID: types.UID(public.MessageID),
}
for k, v := range public.Extra {
v, err := json.Marshal(v)
if err != nil {
s.Details.Causes = append(s.Details.Causes, metav1.StatusCause{
Field: k,
Message: string(v),
})
}
}
}
return s
}
// Unwrap is used by errors.As to iterate over the sequence of
// underlying errors until a matching type is found.
func (e Error) Unwrap() error {
return e.Underlying
}
// 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
// to validate the equality between the two objects.
// errors.Is handles the unwrapping, should you want it.
//nolint:errorlint
o, isGrafanaError := other.(Error)
//nolint:errorlint
base, isBase := other.(Base)
//nolint:errorlint
templateErr, isTemplateErr := other.(Template)
switch {
case isGrafanaError:
return o.Reason == e.Reason && o.MessageID == e.MessageID && o.Error() == e.Error()
case isBase:
return base.Is(e)
case isTemplateErr:
return templateErr.Base.Is(e)
default:
return false
}
}
// PublicError is derived from Error and only contains information
// available to the end user.
type PublicError struct {
StatusCode int `json:"statusCode"`
MessageID string `json:"messageId"`
Message string `json:"message,omitempty"`
Extra map[string]any `json:"extra,omitempty"`
}
// Public returns a subset of the error with non-sensitive information
// that may be relayed to the caller.
func (e Error) Public() PublicError {
message := e.PublicMessage
if message == "" {
if e.Reason == StatusUnknown {
// The unknown status is equal to the empty string.
message = string(StatusInternal)
} else {
message = string(e.Reason.Status())
}
}
return PublicError{
StatusCode: e.Reason.Status().HTTPStatus(),
MessageID: e.MessageID,
Message: message,
Extra: e.PublicPayload,
}
}
// Error implements the error interface.
func (p PublicError) Error() string {
return fmt.Sprintf("[%s] %s", p.MessageID, p.Message)
}
-60
View File
@@ -1,60 +0,0 @@
package errutil_test
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 Example() {
var e errutil.Error
_, err := CreateShortURL("abc/../def")
errors.As(err, &e)
fmt.Println(e.Reason.Status().HTTPStatus(), e.MessageID)
fmt.Println(e.Error())
// Output:
// 400 shorturl.invalidPath
// [shorturl.invalidPath] path mustn't contain '..': 'abc/../def'
}
// 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
}
-74
View File
@@ -1,74 +0,0 @@
package errutil
import (
"errors"
"fmt"
"reflect"
"testing"
"github.com/stretchr/testify/assert"
)
func TestBase_Is(t *testing.T) {
baseNotFound := NotFound("test.notFound")
baseInternal := Internal("test.internal")
tests := []struct {
Base Base
Other error
Expect bool
ExpectUnwrapped bool
}{
{
Base: Base{},
Other: errors.New(""),
Expect: false,
},
{
Base: Base{},
Other: Base{},
Expect: true,
},
{
Base: Base{},
Other: Error{},
Expect: true,
},
{
Base: baseNotFound,
Other: baseNotFound,
Expect: true,
},
{
Base: baseNotFound,
Other: baseNotFound.Errorf("this is an error derived from baseNotFound, it is considered to be equal to baseNotFound"),
Expect: true,
},
{
Base: baseNotFound,
Other: baseInternal,
Expect: false,
},
{
Base: baseInternal,
Other: fmt.Errorf("wrapped, like a burrito: %w", baseInternal.Errorf("oh noes")),
Expect: false,
ExpectUnwrapped: true,
},
}
for _, tc := range tests {
t.Run(fmt.Sprintf(
"Base '%s' == '%s' of type %s = %v (%v unwrapped)",
tc.Base.Error(),
tc.Other.Error(),
reflect.TypeOf(tc.Other),
tc.Expect,
tc.Expect || tc.ExpectUnwrapped,
), func(t *testing.T) {
assert.Equal(t, tc.Expect, tc.Base.Is(tc.Other), "direct comparison")
assert.Equal(t, tc.Expect, errors.Is(tc.Base, tc.Other), "comparison using errors.Is with other as target")
assert.Equal(t, tc.Expect || tc.ExpectUnwrapped, errors.Is(tc.Other, tc.Base), "comparison using errors.Is with base as target, should unwrap other")
})
}
}
-72
View File
@@ -1,72 +0,0 @@
package errutil
import "context"
type LogLevel string
const (
LevelUnknown LogLevel = ""
LevelNever LogLevel = "never"
LevelDebug LogLevel = "debug"
LevelInfo LogLevel = "info"
LevelWarn LogLevel = "warn"
LevelError LogLevel = "error"
)
// 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 LogInterface interface {
Debug(msg string, ctx ...any)
Info(msg string, ctx ...any)
Warn(msg string, ctx ...any)
Error(msg string, ctx ...any)
}
func (l LogLevel) LogFunc(logger LogInterface) func(msg string, ctx ...any) {
switch l {
case LevelNever:
return func(_ string, _ ...any) {}
case LevelDebug:
return logger.Debug
case LevelInfo:
return logger.Info
case LevelWarn:
return logger.Warn
default: // LevelUnknown and LevelError.
return logger.Error
}
}
func (l LogLevel) HighestOf(other LogLevel) LogLevel {
if l.order() < other.order() {
return other
}
return l
}
func (l LogLevel) order() int {
switch l {
case LevelNever:
return 0
case LevelDebug:
return 1
case LevelInfo:
return 2
case LevelWarn:
return 3
default: // LevelUnknown and LevelError.
return 4
}
}
type useUnifiedLogging struct{}
func SetUnifiedLogging(ctx context.Context) context.Context {
return context.WithValue(ctx, useUnifiedLogging{}, true)
}
func HasUnifiedLogging(ctx context.Context) bool {
v, ok := ctx.Value(useUnifiedLogging{}).(bool)
return ok && v
}
-18
View File
@@ -1,18 +0,0 @@
package errutil
// Source identifies from where an error originates.
type Source string
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"
)
// IsDownstream checks if Source is SourceDownstream.
func (s Source) IsDownstream() bool {
return s == SourceDownstream
}
-178
View File
@@ -1,178 +0,0 @@
package errutil
import (
"net/http"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
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"
)
// 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.
const HTTPStatusClientClosedRequest = 499
// StatusReason allows for wrapping of CoreStatus.
type StatusReason interface {
Status() CoreStatus
}
type CoreStatus metav1.StatusReason
// Status implements the StatusReason interface.
func (s CoreStatus) Status() CoreStatus {
return s
}
// HTTPStatus converts the CoreStatus to an HTTP status code.
func (s CoreStatus) HTTPStatus() int {
switch s {
case StatusUnauthorized:
return http.StatusUnauthorized
case StatusForbidden:
return http.StatusForbidden
case StatusNotFound:
return http.StatusNotFound
case StatusTimeout, StatusGatewayTimeout:
return http.StatusGatewayTimeout
case StatusUnprocessableEntity:
return http.StatusUnprocessableEntity
case StatusConflict:
return http.StatusConflict
case StatusTooManyRequests:
return http.StatusTooManyRequests
case StatusBadRequest, StatusValidationFailed:
return http.StatusBadRequest
case StatusClientClosedRequest:
return HTTPStatusClientClosedRequest
case StatusNotImplemented:
return http.StatusNotImplemented
case StatusBadGateway:
return http.StatusBadGateway
case StatusUnknown, StatusInternal:
return http.StatusInternalServerError
default:
return http.StatusInternalServerError
}
}
// LogLevel returns the default LogLevel for the CoreStatus.
func (s CoreStatus) LogLevel() LogLevel {
switch s {
case StatusUnauthorized:
return LevelInfo
case StatusForbidden:
return LevelInfo
case StatusNotFound:
return LevelInfo
case StatusTimeout:
return LevelInfo
case StatusUnprocessableEntity:
return LevelInfo
case StatusConflict:
return LevelInfo
case StatusTooManyRequests:
return LevelInfo
case StatusBadRequest:
return LevelInfo
case StatusValidationFailed:
return LevelInfo
case StatusNotImplemented:
return LevelDebug
case StatusUnknown, StatusInternal:
return LevelError
default:
return LevelUnknown
}
}
func (s CoreStatus) String() string {
return string(s)
}
// ProxyStatus implies that an error originated from the data source
// proxy.
type ProxyStatus CoreStatus
// Status implements the StatusReason interface.
func (s ProxyStatus) Status() CoreStatus {
return CoreStatus(s)
}
// PluginStatus implies that an error originated from a plugin.
type PluginStatus CoreStatus
// Status implements the StatusReason interface.
func (s PluginStatus) Status() CoreStatus {
return CoreStatus(s)
}
-121
View File
@@ -1,121 +0,0 @@
package errutil
import (
"bytes"
"fmt"
"text/template"
)
// Template is an extended Base for when using templating to construct
// error messages.
type Template struct {
Base Base
logTemplate *template.Template
publicTemplate *template.Template
}
// TemplateData contains data for constructing an Error based on a
// Template.
type TemplateData struct {
Private map[string]any
Public map[string]any
Error 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.
func (b Base) Template(pattern string, opts ...TemplateOpt) (Template, error) {
tmpl, err := template.New(b.messageID + "~private").Parse(pattern)
if err != nil {
return Template{}, err
}
t := Template{
Base: b,
logTemplate: tmpl,
}
for _, o := range opts {
t, err = o(t)
if err != nil {
return Template{}, err
}
}
return t, nil
}
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].
func (b Base) MustTemplate(pattern string, opts ...TemplateOpt) Template {
res, err := b.Template(pattern, opts...)
if err != nil {
panic(err)
}
return res
}
// 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 WithPublic(pattern string) TemplateOpt {
return func(t Template) (Template, error) {
var err error
t.publicTemplate, err = template.New(t.Base.messageID + "~public").Parse(pattern)
return t, err
}
}
// 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].
func WithPublicFromLog() TemplateOpt {
return func(t Template) (Template, error) {
t.publicTemplate = t.logTemplate
return t, nil
}
}
// 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 {
if t.logTemplate == nil {
return fmt.Errorf("cannot initialize error using missing template")
}
buf := bytes.Buffer{}
err := t.logTemplate.Execute(&buf, data)
if err != nil {
return err
}
pubBuf := bytes.Buffer{}
if t.publicTemplate != nil {
err := t.publicTemplate.Execute(&pubBuf, TemplateData{Public: data.Public})
if err != nil {
return err
}
}
e := t.Base.Errorf("%s", buf.String())
e.PublicMessage = pubBuf.String()
e.PublicPayload = data.Public
e.Underlying = data.Error
return e
}
func (t Template) Error() string {
return t.Base.Error()
}
-73
View File
@@ -1,73 +0,0 @@
package errutil_test
import (
"errors"
"fmt"
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/util/errutil"
)
func TestTemplate(t *testing.T) {
tmpl := errutil.Internal("template.sampleError").MustTemplate("[{{ .Public.user }}] got error: {{ .Error }}")
err := tmpl.Build(errutil.TemplateData{
Public: map[string]any{
"user": "grot the bot",
},
Error: errors.New("oh noes"),
})
t.Run("Built error should return true when compared with templated error ", func(t *testing.T) {
require.True(t, errors.Is(err, tmpl))
})
t.Run("Built error should return true when compared with templated error base ", func(t *testing.T) {
require.True(t, errors.Is(err, tmpl.Base))
})
}
func ExampleTemplate() {
// 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
}
func ExampleTemplate_public() {
// 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
}
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"github.com/jmespath/go-jmespath"
"github.com/grafana/grafana/pkg/util/errutil"
"github.com/grafana/grafana/pkg/apimachinery/errutil"
)
// DynMap defines a dynamic map interface.
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"sort"
"strings"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/apimachinery/identity"
)
const (
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/services/user"
)