diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go index 7bd3e1b97e..6ed9728e4c 100644 --- a/pkg/errors/errors.go +++ b/pkg/errors/errors.go @@ -4,6 +4,10 @@ import ( "fmt" ) +const ( + codeUnknown string = "unknown" +) + // base is the fundamental struct that implements the error interface. // The order of the struct is 'TCMEUA'. type base struct { @@ -100,7 +104,7 @@ func Unwrapb(cause error) (typ, string, string, error, string, []string) { return base.t, base.c, base.m, base.e, base.u, base.a } - return TypeInternal, "", cause.Error(), cause, "", []string{} + return TypeInternal, codeUnknown, cause.Error(), cause, "", []string{} } // Ast checks if the provided error matches the specified custom error type. diff --git a/pkg/http/render/render.go b/pkg/http/render/render.go new file mode 100644 index 0000000000..405bb76ed1 --- /dev/null +++ b/pkg/http/render/render.go @@ -0,0 +1,83 @@ +package render + +import ( + "net/http" + + jsoniter "github.com/json-iterator/go" + "go.signoz.io/signoz/pkg/errors" +) + +var json = jsoniter.ConfigCompatibleWithStandardLibrary + +type response struct { + Status string `json:"status"` + Data interface{} `json:"data,omitempty"` + Error *responseerror `json:"error,omitempty"` +} + +type responseerror struct { + Code string `json:"code"` + Message string `json:"message"` + Url string `json:"url,omitempty"` + Errors []responseerroradditional `json:"errors,omitempty"` +} + +type responseerroradditional struct { + Message string `json:"message"` +} + +func Success(rw http.ResponseWriter, httpCode int, data interface{}) { + body, err := json.Marshal(&response{Status: StatusSuccess.s, Data: data}) + if err != nil { + Error(rw, err) + return + } + + if httpCode == 0 { + httpCode = http.StatusOK + } + + rw.WriteHeader(httpCode) + _, _ = rw.Write(body) +} + +func Error(rw http.ResponseWriter, cause error) { + // See if this is an instance of the base error or not + t, c, m, _, u, a := errors.Unwrapb(cause) + + // Derive the http code from the error type + httpCode := http.StatusInternalServerError + switch t { + case errors.TypeInvalidInput: + httpCode = http.StatusBadRequest + case errors.TypeNotFound: + httpCode = http.StatusNotFound + case errors.TypeAlreadyExists: + httpCode = http.StatusConflict + case errors.TypeUnauthenticated: + httpCode = http.StatusUnauthorized + } + + rea := make([]responseerroradditional, len(a)) + for k, v := range a { + rea[k] = responseerroradditional{v} + } + + body, err := json.Marshal(&response{ + Status: StatusError.s, + Error: &responseerror{ + Code: c, + Url: u, + Message: m, + Errors: rea, + }, + }) + if err != nil { + // this should never be the case + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + rw.WriteHeader(httpCode) + _, _ = rw.Write(body) +} diff --git a/pkg/http/render/render_test.go b/pkg/http/render/render_test.go new file mode 100644 index 0000000000..79943157f3 --- /dev/null +++ b/pkg/http/render/render_test.go @@ -0,0 +1,116 @@ +package render + +import ( + "context" + "fmt" + "io" + "net" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.signoz.io/signoz/pkg/errors" +) + +func TestSuccess(t *testing.T) { + listener, err := net.Listen("tcp", "localhost:0") + require.NoError(t, err) + + data := map[string]any{ + "int64": int64(9), + "string": "string", + "bool": true, + } + + marshalled, err := json.Marshal(data) + require.NoError(t, err) + + expected := []byte(fmt.Sprintf(`{"status":"success","data":%s}`, string(marshalled))) + + server := &http.Server{ + Handler: http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + Success(rw, http.StatusAccepted, data) + }), + } + + go func() { + _ = server.Serve(listener) + }() + + defer func() { + _ = server.Shutdown(context.Background()) + }() + + req, err := http.NewRequest("GET", "http://"+listener.Addr().String(), nil) + require.NoError(t, err) + + res, err := http.DefaultClient.Do(req) + require.NoError(t, err) + + actual, err := io.ReadAll(res.Body) + require.NoError(t, err) + + assert.Equal(t, http.StatusAccepted, res.StatusCode) + assert.Equal(t, expected, actual) +} + +func TestError(t *testing.T) { + listener, err := net.Listen("tcp", "localhost:0") + require.NoError(t, err) + + testCases := map[string]struct { + name string + statusCode int + err error + expected []byte + }{ + "/already_exists": { + name: "AlreadyExists", + statusCode: http.StatusConflict, + err: errors.New(errors.TypeAlreadyExists, "already_exists", "already exists").WithUrl("https://already_exists"), + expected: []byte(`{"status":"error","error":{"code":"already_exists","message":"already exists","url":"https://already_exists"}}`), + }, + "/unauthenticated": { + name: "Unauthenticated", + statusCode: http.StatusUnauthorized, + err: errors.New(errors.TypeUnauthenticated, "not_allowed", "not allowed").WithUrl("https://unauthenticated").WithAdditional("a1", "a2"), + expected: []byte(`{"status":"error","error":{"code":"not_allowed","message":"not allowed","url":"https://unauthenticated","errors":[{"message":"a1"},{"message":"a2"}]}}`), + }, + } + + server := &http.Server{ + Handler: http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + tc, ok := testCases[req.URL.Path] + if ok { + Error(rw, tc.err) + return + } + }), + } + + go func() { + _ = server.Serve(listener) + }() + + defer func() { + _ = server.Shutdown(context.Background()) + }() + + for path, tc := range testCases { + t.Run("", func(t *testing.T) { + req, err := http.NewRequest("GET", "http://"+listener.Addr().String()+path, nil) + require.NoError(t, err) + + res, err := http.DefaultClient.Do(req) + require.NoError(t, err) + + actual, err := io.ReadAll(res.Body) + require.NoError(t, err) + + assert.Equal(t, tc.statusCode, res.StatusCode) + assert.Equal(t, tc.expected, actual) + }) + } + +} diff --git a/pkg/http/render/status.go b/pkg/http/render/status.go new file mode 100644 index 0000000000..dc4f8720ff --- /dev/null +++ b/pkg/http/render/status.go @@ -0,0 +1,9 @@ +package render + +var ( + StatusSuccess status = status{"success"} + StatusError = status{"error"} +) + +// Defines custom error types +type status struct{ s string } diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index 5064cc359b..d37bb1eeea 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -238,6 +238,7 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) { return aH, nil } +// todo(remove): Implemented at render package (go.signoz.io/signoz/pkg/http/render) with the new error structure type structuredResponse struct { Data interface{} `json:"data"` Total int `json:"total"` @@ -246,11 +247,13 @@ type structuredResponse struct { Errors []structuredError `json:"errors"` } +// todo(remove): Implemented at render package (go.signoz.io/signoz/pkg/http/render) with the new error structure type structuredError struct { Code int `json:"code,omitempty"` Msg string `json:"msg"` } +// todo(remove): Implemented at render package (go.signoz.io/signoz/pkg/http/render) with the new error structure type ApiResponse struct { Status status `json:"status"` Data interface{} `json:"data,omitempty"` @@ -258,6 +261,7 @@ type ApiResponse struct { Error string `json:"error,omitempty"` } +// todo(remove): Implemented at render package (go.signoz.io/signoz/pkg/http/render) with the new error structure func RespondError(w http.ResponseWriter, apiErr model.BaseApiError, data interface{}) { json := jsoniter.ConfigCompatibleWithStandardLibrary b, err := json.Marshal(&ApiResponse{ @@ -301,6 +305,7 @@ func RespondError(w http.ResponseWriter, apiErr model.BaseApiError, data interfa } } +// todo(remove): Implemented at render package (go.signoz.io/signoz/pkg/http/render) with the new error structure func writeHttpResponse(w http.ResponseWriter, data interface{}) { json := jsoniter.ConfigCompatibleWithStandardLibrary b, err := json.Marshal(&ApiResponse{ @@ -351,6 +356,7 @@ func (aH *APIHandler) RegisterQueryRangeV4Routes(router *mux.Router, am *AuthMid subRouter.HandleFunc("/metric/metric_metadata", am.ViewAccess(aH.getMetricMetadata)).Methods(http.MethodGet) } +// todo(remove): Implemented at render package (go.signoz.io/signoz/pkg/http/render) with the new error structure func (aH *APIHandler) Respond(w http.ResponseWriter, data interface{}) { writeHttpResponse(w, data) }