mirror of
https://github.com/grafana/grafana.git
synced 2024-11-24 09:50:29 -06:00
Errors: Introduce error type with Grafana specific metadata (#47504)
This commit is contained in:
parent
2d3cc26aa8
commit
264c2a9d1e
@ -3,12 +3,17 @@ package response
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
)
|
||||
|
||||
// Response is an HTTP response interface.
|
||||
@ -82,7 +87,13 @@ func (r *NormalResponse) WriteTo(ctx *models.ReqContext) {
|
||||
r.body = bytes.NewBuffer(b)
|
||||
}
|
||||
}
|
||||
ctx.Logger.Error(r.errMessage, "error", r.err, "remote_addr", ctx.RemoteAddr(), "traceID", traceID)
|
||||
|
||||
logger := ctx.Logger.Error
|
||||
var gfErr *errutil.Error
|
||||
if errors.As(r.err, &gfErr) {
|
||||
logger = gfErr.LogLevel.LogFunc(ctx.Logger)
|
||||
}
|
||||
logger(r.errMessage, "error", r.err, "remote_addr", ctx.RemoteAddr(), "traceID", traceID)
|
||||
}
|
||||
|
||||
header := ctx.Resp.Header()
|
||||
@ -214,6 +225,20 @@ func Error(status int, message string, err error) *NormalResponse {
|
||||
return resp
|
||||
}
|
||||
|
||||
// Err creates an error response based on an errutil.Error error.
|
||||
func Err(err error) *NormalResponse {
|
||||
grafanaErr := &errutil.Error{}
|
||||
if !errors.As(err, grafanaErr) {
|
||||
return Error(http.StatusInternalServerError, "", fmt.Errorf("unexpected error type [%s]: %w", reflect.TypeOf(err), err))
|
||||
}
|
||||
|
||||
resp := JSON(grafanaErr.Reason.Status().HTTPStatus(), grafanaErr.Public())
|
||||
resp.errMessage = string(grafanaErr.Reason.Status())
|
||||
resp.err = grafanaErr
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
// Empty creates an empty NormalResponse.
|
||||
func Empty(status int) *NormalResponse {
|
||||
return Respond(status, nil)
|
||||
|
44
pkg/util/errutil/doc.go
Normal file
44
pkg/util/errutil/doc.go
Normal file
@ -0,0 +1,44 @@
|
||||
// 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 general structure of the public
|
||||
// message attached to the specific error.
|
||||
// var errNotFound = errutil.NewBase(errutil.StatusNotFound, "service.not-found")
|
||||
// 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.not-found", 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.
|
||||
// It is also possible, but discouraged, to manually edit the fields of
|
||||
// an Error.
|
||||
package errutil
|
165
pkg/util/errutil/errors.go
Normal file
165
pkg/util/errutil/errors.go
Normal file
@ -0,0 +1,165 @@
|
||||
package errutil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// NewBase initializes a Base that is used to construct Error:s.
|
||||
// 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
|
||||
// login.failed-authentication
|
||||
// dashboards.validation-error
|
||||
// dashboards.uid-already-exists
|
||||
func NewBase(reason StatusReason, msgID string, opts ...BaseOpt) Base {
|
||||
b := Base{
|
||||
reason: reason,
|
||||
messageID: msgID,
|
||||
logLevel: reason.Status().LogLevel(),
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
b = opt(b)
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (b Base) Errorf(format string, args ...interface{}) 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,
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// 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
|
||||
PublicMessage string
|
||||
PublicPayload map[string]interface{}
|
||||
LogLevel LogLevel
|
||||
}
|
||||
|
||||
// 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.
|
||||
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)
|
||||
}
|
||||
|
||||
// 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 is used by errors.Is to allow for custom definitions of equality
|
||||
// between two errors.
|
||||
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, ok := other.(Error)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return o.Reason == e.Reason && o.MessageID == e.MessageID && o.Error() == e.Error()
|
||||
}
|
||||
|
||||
// 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]interface{} `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,
|
||||
}
|
||||
}
|
60
pkg/util/errutil/errors_example_test.go
Normal file
60
pkg/util/errutil/errors_example_test.go
Normal file
@ -0,0 +1,60 @@
|
||||
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.NewBase(errutil.StatusBadRequest, "shorturl.absolute-path")
|
||||
errInvalidPath = errutil.NewBase(errutil.StatusBadRequest, "shorturl.invalid-path")
|
||||
errUnexpected = errutil.NewBase(errutil.StatusInternal, "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.invalid-path
|
||||
// [shorturl.invalid-path] 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
|
||||
}
|
37
pkg/util/errutil/log.go
Normal file
37
pkg/util/errutil/log.go
Normal file
@ -0,0 +1,37 @@
|
||||
package errutil
|
||||
|
||||
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 ...interface{})
|
||||
Info(msg string, ctx ...interface{})
|
||||
Warn(msg string, ctx ...interface{})
|
||||
Error(msg string, ctx ...interface{})
|
||||
}
|
||||
|
||||
func (l LogLevel) LogFunc(logger LogInterface) func(msg string, ctx ...interface{}) {
|
||||
switch l {
|
||||
case LevelNever:
|
||||
return func(_ string, _ ...interface{}) {}
|
||||
case LevelDebug:
|
||||
return logger.Debug
|
||||
case LevelInfo:
|
||||
return logger.Info
|
||||
case LevelWarn:
|
||||
return logger.Warn
|
||||
default: // LevelUnknown and LevelError
|
||||
return logger.Error
|
||||
}
|
||||
}
|
128
pkg/util/errutil/status.go
Normal file
128
pkg/util/errutil/status.go
Normal file
@ -0,0 +1,128 @@
|
||||
package errutil
|
||||
|
||||
import "net/http"
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// StatusReason allows for wrapping of CoreStatus.
|
||||
type StatusReason interface {
|
||||
Status() CoreStatus
|
||||
}
|
||||
|
||||
type CoreStatus string
|
||||
|
||||
// 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:
|
||||
return http.StatusGatewayTimeout
|
||||
case StatusTooManyRequests:
|
||||
return http.StatusTooManyRequests
|
||||
case StatusBadRequest, StatusValidationFailed:
|
||||
return http.StatusBadRequest
|
||||
case StatusNotImplemented:
|
||||
return http.StatusNotImplemented
|
||||
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 LevelDebug
|
||||
case StatusTimeout:
|
||||
return LevelInfo
|
||||
case StatusTooManyRequests:
|
||||
return LevelInfo
|
||||
case StatusBadRequest:
|
||||
return LevelInfo
|
||||
case StatusValidationFailed:
|
||||
return LevelInfo
|
||||
case StatusNotImplemented:
|
||||
return LevelError
|
||||
case StatusUnknown, StatusInternal:
|
||||
return LevelError
|
||||
default:
|
||||
return LevelUnknown
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
117
pkg/util/errutil/template.go
Normal file
117
pkg/util/errutil/template.go
Normal file
@ -0,0 +1,117 @@
|
||||
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]interface{}
|
||||
Public map[string]interface{}
|
||||
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:s.
|
||||
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 if set.
|
||||
//
|
||||
// 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
|
||||
}
|
54
pkg/util/errutil/template_test.go
Normal file
54
pkg/util/errutil/template_test.go
Normal file
@ -0,0 +1,54 @@
|
||||
package errutil_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
)
|
||||
|
||||
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 }}")
|
||||
|
||||
// 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.sample-error] [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").
|
||||
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.sample-error] [grot the bot] got error: oh noes
|
||||
// Oh, no, error for grot the bot
|
||||
}
|
Loading…
Reference in New Issue
Block a user