mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Chore: Move identity and errutil to apimachinery module (#89116)
This commit is contained in:
109
pkg/util/errhttp/writer.go
Normal file
109
pkg/util/errhttp/writer.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package errhttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/errutil"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
)
|
||||
|
||||
var ErrNonGrafanaError = errutil.Internal("core.MalformedError")
|
||||
var defaultLogger = log.New("requestErrors")
|
||||
|
||||
// ErrorOptions is a container for functional options passed to [Write].
|
||||
type ErrorOptions struct {
|
||||
fallback *errutil.Error
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
// Write writes an error to the provided [http.ResponseWriter] with the
|
||||
// appropriate HTTP status and JSON payload from [errutil.Error].
|
||||
// Write also logs the provided error to either the "request-errors"
|
||||
// logger, or the logger provided as a functional option using
|
||||
// [WithLogger].
|
||||
// When passing errors that are not [errors.As] compatible with
|
||||
// [errutil.Error], [ErrNonGrafanaError] will be used to create a
|
||||
// generic 500 Internal Server Error payload by default, this is
|
||||
// overrideable by providing [WithFallback] for a custom fallback
|
||||
// error.
|
||||
func Write(ctx context.Context, err error, w http.ResponseWriter, opts ...func(ErrorOptions) ErrorOptions) {
|
||||
opt := ErrorOptions{}
|
||||
for _, o := range opts {
|
||||
opt = o(opt)
|
||||
}
|
||||
|
||||
var gErr errutil.Error
|
||||
if !errors.As(err, &gErr) {
|
||||
gErr = fallbackOrInternalError(err, opt)
|
||||
}
|
||||
|
||||
logError(ctx, gErr, opt)
|
||||
|
||||
var rsp any
|
||||
pub := gErr.Public()
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.WriteHeader(pub.StatusCode)
|
||||
rsp = pub
|
||||
|
||||
// When running in k8s, this will return a v1 status
|
||||
// Typically, k8s handlers should directly support error negotiation, however
|
||||
// when implementing handlers directly this will maintain compatibility with client-go
|
||||
_, ok := request.RequestInfoFrom(ctx)
|
||||
if ok {
|
||||
rsp = gErr.Status()
|
||||
}
|
||||
|
||||
err = json.NewEncoder(w).Encode(rsp)
|
||||
if err != nil {
|
||||
defaultLogger.FromContext(ctx).Error("error while writing error", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// WithFallback sets the default error returned to the user if the error
|
||||
// sent to [Write] is not an [errutil.Error].
|
||||
func WithFallback(opt ErrorOptions, fallback errutil.Error) ErrorOptions {
|
||||
opt.fallback = &fallback
|
||||
return opt
|
||||
}
|
||||
|
||||
// WithLogger sets the logger that [Write] should write log output on.
|
||||
func WithLogger(opt ErrorOptions, logger log.Logger) ErrorOptions {
|
||||
opt.logger = logger
|
||||
return opt
|
||||
}
|
||||
|
||||
func logError(ctx context.Context, e errutil.Error, opt ErrorOptions) {
|
||||
var logger log.Logger = defaultLogger
|
||||
if opt.logger != nil {
|
||||
logger = opt.logger
|
||||
}
|
||||
|
||||
kv := []any{
|
||||
"messageID", e.MessageID,
|
||||
"error", e.LogMessage,
|
||||
}
|
||||
if e.Underlying != nil {
|
||||
kv = append(kv, "underlying", e.Underlying)
|
||||
}
|
||||
|
||||
e.LogLevel.LogFunc(logger.FromContext(ctx))(
|
||||
"Request error",
|
||||
kv...,
|
||||
)
|
||||
}
|
||||
|
||||
func fallbackOrInternalError(err error, opt ErrorOptions) errutil.Error {
|
||||
if opt.fallback != nil {
|
||||
fErr := *opt.fallback
|
||||
fErr.Underlying = err
|
||||
return fErr
|
||||
}
|
||||
|
||||
return ErrNonGrafanaError.Errorf("unexpected error type [%s]: %w", reflect.TypeOf(err), err)
|
||||
}
|
||||
50
pkg/util/errhttp/writer_test.go
Normal file
50
pkg/util/errhttp/writer_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package errhttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/errutil"
|
||||
)
|
||||
|
||||
func TestWrite(t *testing.T) {
|
||||
// Error without k8s context
|
||||
recorder := doError(t, context.Background())
|
||||
assert.Equal(t, http.StatusGatewayTimeout, recorder.Code)
|
||||
assert.JSONEq(t, `{"message": "Timeout", "messageId": "test.thisIsExpected", "statusCode": 504}`, recorder.Body.String())
|
||||
|
||||
// Another request, but within the k8s framework
|
||||
recorder = doError(t, request.WithRequestInfo(context.Background(), &request.RequestInfo{
|
||||
APIGroup: "TestGroup",
|
||||
}))
|
||||
assert.Equal(t, http.StatusGatewayTimeout, recorder.Code)
|
||||
assert.JSONEq(t, `{
|
||||
"status": "Failure",
|
||||
"reason": "Timeout",
|
||||
"metadata": {},
|
||||
"message": "Timeout",
|
||||
"details": { "uid": "test.thisIsExpected" },
|
||||
"code": 504
|
||||
}`, recorder.Body.String())
|
||||
}
|
||||
|
||||
func doError(t *testing.T, ctx context.Context) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
|
||||
const msgID = "test.thisIsExpected"
|
||||
base := errutil.Timeout(msgID)
|
||||
handler := func(writer http.ResponseWriter, _ *http.Request) {
|
||||
Write(ctx, base.Errorf("got expected error"), writer)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "http://localhost:3000/fake", nil)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
handler(recorder, req)
|
||||
return recorder
|
||||
}
|
||||
Reference in New Issue
Block a user