diff --git a/.gitignore b/.gitignore index 5a24fe6c..ccc2e483 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ main_service.go *.key coverage.txt example/ +cmd/candi/test/ .vscode .idea .DS_Store diff --git a/cmd/candi/template_delivery_rest.go b/cmd/candi/template_delivery_rest.go index 13025864..60778a67 100644 --- a/cmd/candi/template_delivery_rest.go +++ b/cmd/candi/template_delivery_rest.go @@ -11,8 +11,6 @@ import ( "net/http"{{if and .MongoDeps (not .SQLDeps)}}{{else}} "strconv"{{end}} - "github.com/labstack/echo" - "{{$.PackagePrefix}}/internal/modules/{{cleanPathModule .ModuleName}}/domain" "{{.PackagePrefix}}/pkg/shared/usecase" @@ -41,109 +39,119 @@ func NewRestHandler(uc usecase.Usecase, deps dependency.Dependency) *RestHandler // Mount handler with root "/" // handling version in here -func (h *RestHandler) Mount(root *echo.Group) { - v1Root := root.Group(candihelper.V1) - - {{camel .ModuleName}} := v1Root.Group("/{{kebab .ModuleName}}", restserver.EchoWrapMiddleware(h.mw.HTTPBearerAuth)) - {{camel .ModuleName}}.GET("", h.getAll{{upper (camel .ModuleName)}}, restserver.EchoWrapMiddleware(h.mw.HTTPPermissionACL("getAll{{upper (camel .ModuleName)}}"))) - {{camel .ModuleName}}.GET("/:id", h.getDetail{{upper (camel .ModuleName)}}ByID, restserver.EchoWrapMiddleware(h.mw.HTTPPermissionACL("getDetail{{upper (camel .ModuleName)}}"))) - {{camel .ModuleName}}.POST("", h.create{{upper (camel .ModuleName)}}, restserver.EchoWrapMiddleware(h.mw.HTTPPermissionACL("create{{upper (camel .ModuleName)}}"))) - {{camel .ModuleName}}.PUT("/:id", h.update{{upper (camel .ModuleName)}}, restserver.EchoWrapMiddleware(h.mw.HTTPPermissionACL("update{{upper (camel .ModuleName)}}"))) - {{camel .ModuleName}}.DELETE("/:id", h.delete{{upper (camel .ModuleName)}}, restserver.EchoWrapMiddleware(h.mw.HTTPPermissionACL("delete{{upper (camel .ModuleName)}}"))) +func (h *RestHandler) Mount(root interfaces.RESTRouter) { + v1{{upper (camel .ModuleName)}} := root.Group(candihelper.V1+"/{{kebab .ModuleName}}", h.mw.HTTPBearerAuth) + + v1{{upper (camel .ModuleName)}}.GET("/", h.getAll{{upper (camel .ModuleName)}}, h.mw.HTTPPermissionACL("getAll{{upper (camel .ModuleName)}}")) + v1{{upper (camel .ModuleName)}}.GET("/:id", h.getDetail{{upper (camel .ModuleName)}}ByID, h.mw.HTTPPermissionACL("getDetail{{upper (camel .ModuleName)}}")) + v1{{upper (camel .ModuleName)}}.POST("/", h.create{{upper (camel .ModuleName)}}, h.mw.HTTPPermissionACL("create{{upper (camel .ModuleName)}}")) + v1{{upper (camel .ModuleName)}}.PUT("/:id", h.update{{upper (camel .ModuleName)}}, h.mw.HTTPPermissionACL("update{{upper (camel .ModuleName)}}")) + v1{{upper (camel .ModuleName)}}.DELETE("/:id", h.delete{{upper (camel .ModuleName)}}, h.mw.HTTPPermissionACL("delete{{upper (camel .ModuleName)}}")) } -func (h *RestHandler) getAll{{upper (camel .ModuleName)}}(c echo.Context) error { - trace, ctx := tracer.StartTraceWithContext(c.Request().Context(), "{{upper (camel .ModuleName)}}DeliveryREST:GetAll{{upper (camel .ModuleName)}}") +func (h *RestHandler) getAll{{upper (camel .ModuleName)}}(rw http.ResponseWriter, req *http.Request) { + trace, ctx := tracer.StartTraceWithContext(req.Context(), "{{upper (camel .ModuleName)}}DeliveryREST:GetAll{{upper (camel .ModuleName)}}") defer trace.Finish() tokenClaim := candishared.ParseTokenClaimFromContext(ctx) // must using HTTPBearerAuth in middleware for this handler var filter domain.Filter{{upper (camel .ModuleName)}} - if err := candihelper.ParseFromQueryParam(c.Request().URL.Query(), &filter); err != nil { - return wrapper.NewHTTPResponse(http.StatusBadRequest, "Failed parse filter", err).JSON(c.Response()) + if err := candihelper.ParseFromQueryParam(req.URL.Query(), &filter); err != nil { + wrapper.NewHTTPResponse(http.StatusBadRequest, "Failed parse filter", err).JSON(rw) + return } if err := h.validator.ValidateDocument("{{cleanPathModule .ModuleName}}/get_all", filter); err != nil { - return wrapper.NewHTTPResponse(http.StatusBadRequest, "Failed validate filter", err).JSON(c.Response()) + wrapper.NewHTTPResponse(http.StatusBadRequest, "Failed validate filter", err).JSON(rw) + return } data, meta, err := h.uc.{{upper (camel .ModuleName)}}().GetAll{{upper (camel .ModuleName)}}(ctx, &filter) if err != nil { - return wrapper.NewHTTPResponse(http.StatusBadRequest, err.Error()).JSON(c.Response()) + wrapper.NewHTTPResponse(http.StatusBadRequest, err.Error()).JSON(rw) + return } message := "Success, with your user id (" + tokenClaim.Subject + ") and role (" + tokenClaim.Role + ")" - return wrapper.NewHTTPResponse(http.StatusOK, message, meta, data).JSON(c.Response()) + wrapper.NewHTTPResponse(http.StatusOK, message, meta, data).JSON(rw) } -func (h *RestHandler) getDetail{{upper (camel .ModuleName)}}ByID(c echo.Context) error { - trace, ctx := tracer.StartTraceWithContext(c.Request().Context(), "{{upper (camel .ModuleName)}}DeliveryREST:GetDetail{{upper (camel .ModuleName)}}ByID") +func (h *RestHandler) getDetail{{upper (camel .ModuleName)}}ByID(rw http.ResponseWriter, req *http.Request) { + trace, ctx := tracer.StartTraceWithContext(req.Context(), "{{upper (camel .ModuleName)}}DeliveryREST:GetDetail{{upper (camel .ModuleName)}}ByID") defer trace.Finish() - {{if and .MongoDeps (not .SQLDeps)}}id := c.Param("id"){{else}}id, _ := strconv.Atoi(c.Param("id")){{end}} + {{if and .MongoDeps (not .SQLDeps)}}id := restserver.URLParam(req, "id"){{else}}id, _ := strconv.Atoi(restserver.URLParam(req, "id")){{end}} data, err := h.uc.{{upper (camel .ModuleName)}}().GetDetail{{upper (camel .ModuleName)}}(ctx, id) if err != nil { - return wrapper.NewHTTPResponse(http.StatusBadRequest, err.Error()).JSON(c.Response()) + wrapper.NewHTTPResponse(http.StatusBadRequest, err.Error()).JSON(rw) + return } - return wrapper.NewHTTPResponse(http.StatusOK, "Success", data).JSON(c.Response()) + wrapper.NewHTTPResponse(http.StatusOK, "Success", data).JSON(rw) } -func (h *RestHandler) create{{upper (camel .ModuleName)}}(c echo.Context) error { - trace, ctx := tracer.StartTraceWithContext(c.Request().Context(), "{{upper (camel .ModuleName)}}DeliveryREST:Create{{upper (camel .ModuleName)}}") +func (h *RestHandler) create{{upper (camel .ModuleName)}}(rw http.ResponseWriter, req *http.Request) { + trace, ctx := tracer.StartTraceWithContext(req.Context(), "{{upper (camel .ModuleName)}}DeliveryREST:Create{{upper (camel .ModuleName)}}") defer trace.Finish() - body, _ := io.ReadAll(c.Request().Body) + body, _ := io.ReadAll(req.Body) if err := h.validator.ValidateDocument("{{cleanPathModule .ModuleName}}/save", body); err != nil { - return wrapper.NewHTTPResponse(http.StatusBadRequest, "Failed validate payload", err).JSON(c.Response()) + wrapper.NewHTTPResponse(http.StatusBadRequest, "Failed validate payload", err).JSON(rw) + return } var payload domain.Request{{upper (camel .ModuleName)}} if err := json.Unmarshal(body, &payload); err != nil { - return wrapper.NewHTTPResponse(http.StatusBadRequest, err.Error()).JSON(c.Response()) + wrapper.NewHTTPResponse(http.StatusBadRequest, err.Error()).JSON(rw) + return } res, err := h.uc.{{upper (camel .ModuleName)}}().Create{{upper (camel .ModuleName)}}(ctx, &payload) if err != nil { - return wrapper.NewHTTPResponse(http.StatusBadRequest, err.Error()).JSON(c.Response()) + wrapper.NewHTTPResponse(http.StatusBadRequest, err.Error()).JSON(rw) + return } - return wrapper.NewHTTPResponse(http.StatusCreated, "Success", res).JSON(c.Response()) + wrapper.NewHTTPResponse(http.StatusCreated, "Success", res).JSON(rw) } -func (h *RestHandler) update{{upper (camel .ModuleName)}}(c echo.Context) error { - trace, ctx := tracer.StartTraceWithContext(c.Request().Context(), "{{upper (camel .ModuleName)}}DeliveryREST:Update{{upper (camel .ModuleName)}}") +func (h *RestHandler) update{{upper (camel .ModuleName)}}(rw http.ResponseWriter, req *http.Request) { + trace, ctx := tracer.StartTraceWithContext(req.Context(), "{{upper (camel .ModuleName)}}DeliveryREST:Update{{upper (camel .ModuleName)}}") defer trace.Finish() - body, _ := io.ReadAll(c.Request().Body) + body, _ := io.ReadAll(req.Body) if err := h.validator.ValidateDocument("{{cleanPathModule .ModuleName}}/save", body); err != nil { - return wrapper.NewHTTPResponse(http.StatusBadRequest, "Failed validate payload", err).JSON(c.Response()) + wrapper.NewHTTPResponse(http.StatusBadRequest, "Failed validate payload", err).JSON(rw) + return } var payload domain.Request{{upper (camel .ModuleName)}} if err := json.Unmarshal(body, &payload); err != nil { - return wrapper.NewHTTPResponse(http.StatusBadRequest, err.Error()).JSON(c.Response()) + wrapper.NewHTTPResponse(http.StatusBadRequest, err.Error()).JSON(rw) + return } - {{if and .MongoDeps (not .SQLDeps)}}payload.ID = c.Param("id"){{else}}payload.ID, _ = strconv.Atoi(c.Param("id")){{end}} + {{if and .MongoDeps (not .SQLDeps)}}payload.ID = restserver.URLParam(req, "id"){{else}}payload.ID, _ = strconv.Atoi(restserver.URLParam(req, "id")){{end}} err := h.uc.{{upper (camel .ModuleName)}}().Update{{upper (camel .ModuleName)}}(ctx, &payload) if err != nil { - return wrapper.NewHTTPResponse(http.StatusBadRequest, err.Error()).JSON(c.Response()) + wrapper.NewHTTPResponse(http.StatusBadRequest, err.Error()).JSON(rw) + return } - return wrapper.NewHTTPResponse(http.StatusOK, "Success").JSON(c.Response()) + wrapper.NewHTTPResponse(http.StatusOK, "Success").JSON(rw) } -func (h *RestHandler) delete{{upper (camel .ModuleName)}}(c echo.Context) error { - trace, ctx := tracer.StartTraceWithContext(c.Request().Context(), "{{upper (camel .ModuleName)}}DeliveryREST:Delete{{upper (camel .ModuleName)}}") +func (h *RestHandler) delete{{upper (camel .ModuleName)}}(rw http.ResponseWriter, req *http.Request) { + trace, ctx := tracer.StartTraceWithContext(req.Context(), "{{upper (camel .ModuleName)}}DeliveryREST:Delete{{upper (camel .ModuleName)}}") defer trace.Finish() - {{if and .MongoDeps (not .SQLDeps)}}id := c.Param("id"){{else}}id, _ := strconv.Atoi(c.Param("id")){{end}} + {{if and .MongoDeps (not .SQLDeps)}}id := restserver.URLParam(req, "id"){{else}}id, _ := strconv.Atoi(restserver.URLParam(req, "id")){{end}} if err := h.uc.{{upper (camel .ModuleName)}}().Delete{{upper (camel .ModuleName)}}(ctx, id); err != nil { - return wrapper.NewHTTPResponse(http.StatusBadRequest, err.Error()).JSON(c.Response()) + wrapper.NewHTTPResponse(http.StatusBadRequest, err.Error()).JSON(rw) + return } - return wrapper.NewHTTPResponse(http.StatusOK, "Success").JSON(c.Response()) + wrapper.NewHTTPResponse(http.StatusOK, "Success").JSON(rw) } ` @@ -162,10 +170,10 @@ import ( mockusecase "{{$.PackagePrefix}}/pkg/mocks/modules/{{cleanPathModule .ModuleName}}/usecase" mocksharedusecase "{{$.PackagePrefix}}/pkg/mocks/shared/usecase" + "{{.LibraryName}}/candihelper" "{{.LibraryName}}/candishared" mockdeps "{{.LibraryName}}/mocks/codebase/factory/dependency" mockinterfaces "{{.LibraryName}}/mocks/codebase/interfaces" - "github.com/labstack/echo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -192,8 +200,13 @@ func TestNewRestHandler(t *testing.T) { handler := NewRestHandler(nil, mockDeps) assert.NotNil(t, handler) - e := echo.New() - handler.Mount(e.Group("/")) + mockRoute := &mockinterfaces.RESTRouter{} + mockRoute.On("Group", mock.Anything, mock.Anything).Return(mockRoute) + mockRoute.On("GET", mock.Anything, mock.Anything, mock.Anything) + mockRoute.On("POST", mock.Anything, mock.Anything, mock.Anything) + mockRoute.On("PUT", mock.Anything, mock.Anything, mock.Anything) + mockRoute.On("DELETE", mock.Anything, mock.Anything, mock.Anything) + handler.Mount(mockRoute) } func TestRestHandler_getAll{{upper (camel .ModuleName)}}(t *testing.T) { @@ -224,11 +237,9 @@ func TestRestHandler_getAll{{upper (camel .ModuleName)}}(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/"+tt.reqBody, strings.NewReader(tt.reqBody)) req = req.WithContext(candishared.SetToContext(req.Context(), candishared.ContextKeyTokenClaim, &candishared.TokenClaim{})) - req.Header.Add(echo.HeaderContentType, echo.MIMEApplicationJSON) + req.Header.Add(candihelper.HeaderContentType, candihelper.HeaderMIMEApplicationJSON) res := httptest.NewRecorder() - echoContext := echo.New().NewContext(req, res) - err := handler.getAll{{upper (camel .ModuleName)}}(echoContext) - assert.NoError(t, err) + handler.getAll{{upper (camel .ModuleName)}}(res, req) assert.Equal(t, tt.wantRespCode, res.Code) }) } @@ -258,11 +269,9 @@ func TestRestHandler_getDetail{{upper (camel .ModuleName)}}ByID(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(tt.reqBody)) req = req.WithContext(candishared.SetToContext(req.Context(), candishared.ContextKeyTokenClaim, &candishared.TokenClaim{})) - req.Header.Add(echo.HeaderContentType, echo.MIMEApplicationJSON) + req.Header.Add(candihelper.HeaderContentType, candihelper.HeaderMIMEApplicationJSON) res := httptest.NewRecorder() - echoContext := echo.New().NewContext(req, res) - err := handler.getDetail{{upper (camel .ModuleName)}}ByID(echoContext) - assert.NoError(t, err) + handler.getDetail{{upper (camel .ModuleName)}}ByID(res, req) assert.Equal(t, tt.wantRespCode, res.Code) }) } @@ -284,7 +293,7 @@ func TestRestHandler_create{{upper (camel .ModuleName)}}(t *testing.T) { t.Run(tt.name, func(t *testing.T) { {{camel .ModuleName}}Usecase := &mockusecase.{{upper (camel .ModuleName)}}Usecase{} - {{camel .ModuleName}}Usecase.On("Create{{upper (camel .ModuleName)}}", mock.Anything, mock.Anything).Return(tt.wantUsecaseError) + {{camel .ModuleName}}Usecase.On("Create{{upper (camel .ModuleName)}}", mock.Anything, mock.Anything).Return(domain.Response{{upper (camel .ModuleName)}}{}, tt.wantUsecaseError) mockValidator := &mockinterfaces.Validator{} mockValidator.On("ValidateDocument", mock.Anything, mock.Anything).Return(tt.wantValidateError) @@ -294,13 +303,9 @@ func TestRestHandler_create{{upper (camel .ModuleName)}}(t *testing.T) { handler := RestHandler{uc: uc, validator: mockValidator} req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(tt.reqBody)) - req.Header.Add(echo.HeaderContentType, echo.MIMEApplicationJSON) + req.Header.Add(candihelper.HeaderContentType, candihelper.HeaderMIMEApplicationJSON) res := httptest.NewRecorder() - echoContext := echo.New().NewContext(req, res) - echoContext.SetParamNames("id") - echoContext.SetParamValues("001") - err := handler.create{{upper (camel .ModuleName)}}(echoContext) - assert.NoError(t, err) + handler.create{{upper (camel .ModuleName)}}(res, req) assert.Equal(t, tt.wantRespCode, res.Code) }) } @@ -336,11 +341,9 @@ func TestRestHandler_update{{upper (camel .ModuleName)}}(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(tt.reqBody)) req = req.WithContext(candishared.SetToContext(req.Context(), candishared.ContextKeyTokenClaim, &candishared.TokenClaim{})) - req.Header.Add(echo.HeaderContentType, echo.MIMEApplicationJSON) + req.Header.Add(candihelper.HeaderContentType, candihelper.HeaderMIMEApplicationJSON) res := httptest.NewRecorder() - echoContext := echo.New().NewContext(req, res) - err := handler.update{{upper (camel .ModuleName)}}(echoContext) - assert.NoError(t, err) + handler.update{{upper (camel .ModuleName)}}(res, req) assert.Equal(t, tt.wantRespCode, res.Code) }) } @@ -369,11 +372,9 @@ func TestRestHandler_delete{{upper (camel .ModuleName)}}(t *testing.T) { handler := RestHandler{uc: uc, validator: mockValidator} req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(tt.reqBody)) - req.Header.Add(echo.HeaderContentType, echo.MIMEApplicationJSON) + req.Header.Add(candihelper.HeaderContentType, candihelper.HeaderMIMEApplicationJSON) res := httptest.NewRecorder() - echoContext := echo.New().NewContext(req, res) - err := handler.delete{{upper (camel .ModuleName)}}(echoContext) - assert.NoError(t, err) + handler.delete{{upper (camel .ModuleName)}}(res, req) assert.Equal(t, tt.wantRespCode, res.Code) }) } diff --git a/codebase/app/graphql_server/graphql_handler.go b/codebase/app/graphql_server/graphql_handler.go index 018f58de..c14f49db 100644 --- a/codebase/app/graphql_server/graphql_handler.go +++ b/codebase/app/graphql_server/graphql_handler.go @@ -31,7 +31,6 @@ type Handler interface { // ConstructHandlerFromService for create public graphql handler (maybe inject to rest handler) func ConstructHandlerFromService(service factory.ServiceFactory, opt Option) Handler { - // create dynamic struct queryResolverValues := make(map[string]interface{}) mutationResolverValues := make(map[string]interface{}) @@ -79,33 +78,31 @@ func ConstructHandlerFromService(service factory.ServiceFactory, opt Option) Han } schema := graphql.MustParseSchema(string(gqlSchema), &opt.rootResolver, schemaOpts...) - logger.LogYellow(fmt.Sprintf("[GraphQL] endpoint\t\t\t: http://127.0.0.1:%d%s%s", opt.httpPort, opt.rootPath, rootGraphQLPath)) - logger.LogYellow(fmt.Sprintf("[GraphQL] playground\t\t\t: http://127.0.0.1:%d%s%s", opt.httpPort, opt.rootPath, rootGraphQLPlayground)) - logger.LogYellow(fmt.Sprintf("[GraphQL] playground (with explorer)\t: http://127.0.0.1:%d%s%s?graphiql=true", opt.httpPort, opt.rootPath, rootGraphQLPlayground)) - logger.LogYellow(fmt.Sprintf("[GraphQL] voyager\t\t\t: http://127.0.0.1:%d%s%s", opt.httpPort, opt.rootPath, rootGraphQLVoyager)) + logger.LogYellow(fmt.Sprintf("[GraphQL] endpoint\t\t\t: http://127.0.0.1:%d%s", opt.httpPort, opt.RootPath)) + logger.LogYellow(fmt.Sprintf("[GraphQL] playground\t\t\t: http://127.0.0.1:%d%s/playground", opt.httpPort, opt.RootPath)) + logger.LogYellow(fmt.Sprintf("[GraphQL] playground (with explorer)\t: http://127.0.0.1:%d%s/playground?graphiql=true", opt.httpPort, opt.RootPath)) + logger.LogYellow(fmt.Sprintf("[GraphQL] voyager\t\t\t: http://127.0.0.1:%d%s/voyager", opt.httpPort, opt.RootPath)) return &handlerImpl{ - disableIntrospection: opt.DisableIntrospection, - schema: schema, + schema: schema, + option: opt, } } type handlerImpl struct { - disableIntrospection bool - schema *graphql.Schema + schema *graphql.Schema + option Option } -func NewHandler(disableIntrospection bool, schema *graphql.Schema) Handler { +func NewHandler(schema *graphql.Schema, opt Option) Handler { return &handlerImpl{ - disableIntrospection: disableIntrospection, - schema: schema, + schema: schema, + option: opt, } } func (s *handlerImpl) ServeGraphQL() http.HandlerFunc { - return ws.NewHandlerFunc(s.schema, http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - var params struct { Query string `json:"query"` OperationName string `json:"operationName"` @@ -136,7 +133,7 @@ func (s *handlerImpl) ServeGraphQL() http.HandlerFunc { } func (s *handlerImpl) ServePlayground(resp http.ResponseWriter, req *http.Request) { - if s.disableIntrospection { + if s.option.DisableIntrospection { http.Error(resp, "Forbidden", http.StatusForbidden) return } @@ -156,7 +153,7 @@ func (s *handlerImpl) ServePlayground(resp http.ResponseWriter, req *http.Reques @@ -188,8 +185,8 @@ func (s *handlerImpl) ServePlayground(resp http.ResponseWriter, req *http.Reques root.classList.add('playgroundIn'); const wsProto = location.protocol == 'https:' ? 'wss:' : 'ws:' GraphQLPlayground.init(root, { - endpoint: location.protocol + '//' + location.host + '/graphql', - subscriptionsEndpoint: wsProto + '//' + location.host + '/graphql', + endpoint: location.protocol + '//' + location.host + '` + s.option.RootPath + `', + subscriptionsEndpoint: wsProto + '//' + location.host + '` + s.option.RootPath + `', settings: { 'request.credentials': 'same-origin' } @@ -201,7 +198,7 @@ func (s *handlerImpl) ServePlayground(resp http.ResponseWriter, req *http.Reques } func (s *handlerImpl) ServeVoyager(resp http.ResponseWriter, req *http.Request) { - if s.disableIntrospection { + if s.option.DisableIntrospection { http.Error(resp, "Forbidden", http.StatusForbidden) return } @@ -251,7 +248,7 @@ func (s *handlerImpl) ServeVoyager(resp http.ResponseWriter, req *http.Request) function introspectionProvider(introspectionQuery) { // This example expects a GraphQL server at the path /graphql. // Change this to point wherever you host your GraphQL server. - return fetch(location.protocol + '//' + location.host + '/graphql', { + return fetch(location.protocol + '//' + location.host + '` + s.option.RootPath + `', { method: 'post', headers: { 'Accept': 'application/json', diff --git a/codebase/app/graphql_server/graphql_server.go b/codebase/app/graphql_server/graphql_server.go index 75d12b29..956a7a88 100644 --- a/codebase/app/graphql_server/graphql_server.go +++ b/codebase/app/graphql_server/graphql_server.go @@ -37,9 +37,9 @@ func NewServer(service factory.ServiceFactory, opts ...OptionFunc) factory.AppSe mux := http.NewServeMux() mux.Handle("/", server.opt.rootHandler) mux.Handle("/memstats", service.GetDependency().GetMiddleware().HTTPBasicAuth(http.HandlerFunc(wrapper.HTTPHandlerMemstats))) - mux.HandleFunc(server.opt.rootPath+rootGraphQLPath, httpHandler.ServeGraphQL()) - mux.HandleFunc(server.opt.rootPath+rootGraphQLPlayground, httpHandler.ServePlayground) - mux.HandleFunc(server.opt.rootPath+rootGraphQLVoyager, httpHandler.ServeVoyager) + mux.HandleFunc(server.opt.RootPath, httpHandler.ServeGraphQL()) + mux.HandleFunc(server.opt.RootPath+"/playground", httpHandler.ServePlayground) + mux.HandleFunc(server.opt.RootPath+"/voyager", httpHandler.ServeVoyager) httpEngine.Addr = fmt.Sprintf(":%d", server.opt.httpPort) httpEngine.Handler = mux diff --git a/codebase/app/graphql_server/option.go b/codebase/app/graphql_server/option.go index c8519831..777d71cc 100644 --- a/codebase/app/graphql_server/option.go +++ b/codebase/app/graphql_server/option.go @@ -8,19 +8,13 @@ import ( "github.com/soheilhy/cmux" ) -const ( - rootGraphQLPath = "/graphql" - rootGraphQLPlayground = "/graphql/playground" - rootGraphQLVoyager = "/graphql/voyager" -) - type ( // Option gql server Option struct { DisableIntrospection bool + RootPath string httpPort uint16 - rootPath string debugMode bool jaegerMaxPacketSize int rootHandler http.Handler @@ -36,7 +30,7 @@ type ( func getDefaultOption() Option { return Option{ httpPort: 8000, - rootPath: "", + RootPath: "/graphql", debugMode: true, rootHandler: http.HandlerFunc(wrapper.HTTPHandlerDefaultRoot), } @@ -52,7 +46,7 @@ func SetHTTPPort(port uint16) OptionFunc { // SetRootPath option func func SetRootPath(rootPath string) OptionFunc { return func(o *Option) { - o.rootPath = rootPath + o.RootPath = rootPath } } diff --git a/codebase/app/rest_server/custom_http_error.go b/codebase/app/rest_server/custom_http_error.go deleted file mode 100644 index 1f046e4f..00000000 --- a/codebase/app/rest_server/custom_http_error.go +++ /dev/null @@ -1,26 +0,0 @@ -package restserver - -import ( - "fmt" - "net/http" - - "github.com/golangid/candi/wrapper" - "github.com/labstack/echo" -) - -// CustomHTTPErrorHandler custom echo http error -func CustomHTTPErrorHandler(err error, c echo.Context) { - var message string - code := http.StatusInternalServerError - if err != nil { - message = err.Error() - } - - if he, ok := err.(*echo.HTTPError); ok { - code = he.Code - if code == http.StatusNotFound { - message = fmt.Sprintf(`Resource "%s %s" not found`, c.Request().Method, c.Request().URL.Path) - } - } - wrapper.NewHTTPResponse(code, message).JSON(c.Response()) -} diff --git a/codebase/app/rest_server/custom_http_error_test.go b/codebase/app/rest_server/custom_http_error_test.go deleted file mode 100644 index 12edd048..00000000 --- a/codebase/app/rest_server/custom_http_error_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package restserver - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/labstack/echo" - "github.com/stretchr/testify/assert" -) - -func TestCustomHTTPErrorHandler(t *testing.T) { - e := echo.New() - url := "/testing" - req, err := http.NewRequest(echo.GET, url, nil) - assert.NoError(t, err) - - rec := httptest.NewRecorder() - c := e.NewContext(req, rec) - - err = &echo.HTTPError{ - Code: http.StatusNotFound, Message: "Not Found", - } - CustomHTTPErrorHandler(err, c) -} diff --git a/codebase/app/rest_server/middleware.go b/codebase/app/rest_server/middleware.go index 3492c016..b2d7aa73 100644 --- a/codebase/app/rest_server/middleware.go +++ b/codebase/app/rest_server/middleware.go @@ -1,127 +1,16 @@ package restserver -import ( - "bytes" - "io" - "net/http" - "strconv" - "sync" - "time" +import "net/http" - "github.com/golangid/candi/logger" - "github.com/labstack/echo" -) - -// EchoWrapMiddleware wraps `func(http.Handler) http.Handler` into `echo.MiddlewareFunc` -func EchoWrapMiddleware(m func(http.Handler) http.Handler) echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) (err error) { - - m(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - c.SetRequest(r) - c.Response().Writer = w - err = next(c) - })).ServeHTTP(c.Response().Writer, c.Request()) - - return - } +// WithChainingMiddlewares chaining middlewares +func WithChainingMiddlewares(handlerFunc http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) http.HandlerFunc { + if len(middlewares) == 0 { + return handlerFunc } -} - -// EchoLoggerMiddleware middleware -func EchoLoggerMiddleware(isActive bool, writer io.Writer) echo.MiddlewareFunc { - bPool := &sync.Pool{ - New: func() interface{} { - return bytes.NewBuffer(make([]byte, 256)) - }, - } - - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - - start := time.Now() - - errNext := next(c) - if errNext != nil { - c.Error(errNext) - } - - if !isActive { - return nil - } - - req := c.Request() - res := c.Response() - - stop := time.Now() - buf := bPool.Get().(*bytes.Buffer) - buf.Reset() - defer bPool.Put(buf) - - buf.WriteString(`{"time":"`) - buf.WriteString(time.Now().Format(time.RFC3339Nano)) - - buf.WriteString(`","id":"`) - id := req.Header.Get(echo.HeaderXRequestID) - if id == "" { - id = res.Header().Get(echo.HeaderXRequestID) - } - buf.WriteString(id) - - buf.WriteString(`","remote_ip":"`) - buf.WriteString(c.RealIP()) - - buf.WriteString(`","host":"`) - buf.WriteString(req.Host) - - buf.WriteString(`","method":"`) - buf.WriteString(req.Method) - - buf.WriteString(`","uri":"`) - buf.WriteString(req.RequestURI) - - buf.WriteString(`","user_agent":"`) - buf.WriteString(req.UserAgent()) - - buf.WriteString(`","status":`) - n := res.Status - s := logger.GreenColor(n) - switch { - case n >= 500: - s = logger.RedColor(n) - case n >= 400: - s = logger.YellowColor(n) - case n >= 300: - s = logger.CyanColor(n) - } - buf.WriteString(s) - - buf.WriteString(`,"error":"`) - if errNext != nil { - buf.WriteString(logger.RedColor(errNext.Error())) - } - - buf.WriteString(`","latency":`) - l := stop.Sub(start) - buf.WriteString(strconv.FormatInt(int64(l), 10)) - - buf.WriteString(`,"latency_human":"`) - buf.WriteString(stop.Sub(start).String()) - - buf.WriteString(`","bytes_in":`) - cl := req.Header.Get(echo.HeaderContentLength) - if cl == "" { - cl = "0" - } - buf.WriteString(cl) - - buf.WriteString(`,"bytes_out":`) - buf.WriteString(strconv.FormatInt(res.Size, 10)) - - buf.WriteString("}\n") - io.Copy(writer, buf) - return nil - } + wrapped := handlerFunc + for i := len(middlewares) - 1; i >= 0; i-- { + wrapped = middlewares[i](http.HandlerFunc(wrapped)).ServeHTTP } + return wrapped } diff --git a/codebase/app/rest_server/option.go b/codebase/app/rest_server/option.go index 5da01d9f..2dad7e82 100644 --- a/codebase/app/rest_server/option.go +++ b/codebase/app/rest_server/option.go @@ -3,26 +3,25 @@ package restserver import ( "net/http" "os" + "strings" graphqlserver "github.com/golangid/candi/codebase/app/graphql_server" "github.com/golangid/candi/config/env" "github.com/golangid/candi/wrapper" - "github.com/labstack/echo" "github.com/soheilhy/cmux" ) type ( option struct { - rootMiddlewares []echo.MiddlewareFunc - rootHandler http.Handler - errorHandler echo.HTTPErrorHandler + rootMiddlewares []func(http.Handler) http.Handler + rootHandler http.HandlerFunc + errorHandler http.HandlerFunc httpPort uint16 rootPath string debugMode bool includeGraphQL bool jaegerMaxPacketSize int sharedListener cmux.CMux - engineOption func(e *echo.Echo) graphqlOption graphqlserver.Option } @@ -33,21 +32,20 @@ type ( func getDefaultOption() option { return option{ httpPort: 8000, - rootPath: "", + rootPath: "/", debugMode: true, - rootMiddlewares: []echo.MiddlewareFunc{ - echo.WrapMiddleware(wrapper.HTTPMiddlewareCORS( + rootMiddlewares: []func(http.Handler) http.Handler{ + wrapper.HTTPMiddlewareCORS( env.BaseEnv().CORSAllowMethods, env.BaseEnv().CORSAllowHeaders, env.BaseEnv().CORSAllowOrigins, nil, env.BaseEnv().CORSAllowCredential, - )), - EchoWrapMiddleware(wrapper.HTTPMiddlewareTracer(wrapper.HTTPMiddlewareTracerConfig{ + ), + wrapper.HTTPMiddlewareTracer(wrapper.HTTPMiddlewareTracerConfig{ MaxLogSize: env.BaseEnv().JaegerMaxPacketSize, - ExcludePath: map[string]struct{}{"/": {}, "/graphql": {}}, - })), - EchoLoggerMiddleware(env.BaseEnv().DebugMode, os.Stdout), + ExcludePath: map[string]struct{}{"/": {}, "/graphql": {}, "/favicon.ico": {}}, + }), + wrapper.HTTPMiddlewareLog(env.BaseEnv().DebugMode, os.Stdout), }, - rootHandler: http.HandlerFunc(wrapper.HTTPHandlerDefaultRoot), - errorHandler: CustomHTTPErrorHandler, + rootHandler: http.HandlerFunc(wrapper.HTTPHandlerDefaultRoot), } } @@ -61,12 +59,15 @@ func SetHTTPPort(port uint16) OptionFunc { // SetRootPath option func func SetRootPath(rootPath string) OptionFunc { return func(o *option) { + if !strings.HasPrefix(rootPath, "/") { + rootPath = "/" + strings.Trim(rootPath, "/") + } o.rootPath = rootPath } } // SetRootHTTPHandler option func -func SetRootHTTPHandler(rootHandler http.Handler) OptionFunc { +func SetRootHTTPHandler(rootHandler http.HandlerFunc) OptionFunc { return func(o *option) { o.rootHandler = rootHandler } @@ -101,36 +102,23 @@ func SetJaegerMaxPacketSize(max int) OptionFunc { } // SetRootMiddlewares option func -func SetRootMiddlewares(middlewares ...echo.MiddlewareFunc) OptionFunc { +func SetRootMiddlewares(middlewares ...func(http.Handler) http.Handler) OptionFunc { return func(o *option) { o.rootMiddlewares = middlewares } } // AddRootMiddlewares option func, overide root middleware -func AddRootMiddlewares(middlewares ...echo.MiddlewareFunc) OptionFunc { +func AddRootMiddlewares(middlewares ...func(http.Handler) http.Handler) OptionFunc { return func(o *option) { o.rootMiddlewares = append(o.rootMiddlewares, middlewares...) } } -// SetErrorHandler option func -func SetErrorHandler(errorHandler echo.HTTPErrorHandler) OptionFunc { - return func(o *option) { - o.errorHandler = errorHandler - } -} - -// SetEchoEngineOption option func -func SetEchoEngineOption(echoFunc func(e *echo.Echo)) OptionFunc { - return func(o *option) { - o.engineOption = echoFunc - } -} - // AddGraphQLOption option func func AddGraphQLOption(opts ...graphqlserver.OptionFunc) OptionFunc { return func(o *option) { + o.graphqlOption.RootPath = "/graphql" for _, opt := range opts { opt(&o.graphqlOption) } diff --git a/codebase/app/rest_server/rest_server.go b/codebase/app/rest_server/rest_server.go index 192836ac..415b01a4 100644 --- a/codebase/app/rest_server/rest_server.go +++ b/codebase/app/rest_server/rest_server.go @@ -6,96 +6,90 @@ import ( "log" "net" "net/http" - "sort" "strings" - "github.com/labstack/echo" - "github.com/soheilhy/cmux" - + "github.com/go-chi/chi/v5" "github.com/golangid/candi/candihelper" graphqlserver "github.com/golangid/candi/codebase/app/graphql_server" "github.com/golangid/candi/codebase/factory" "github.com/golangid/candi/codebase/factory/types" "github.com/golangid/candi/logger" "github.com/golangid/candi/wrapper" + "github.com/soheilhy/cmux" ) type restServer struct { - serverEngine *echo.Echo - service factory.ServiceFactory - listener net.Listener - opt option + opt option + httpEngine *http.Server + listener net.Listener } // NewServer create new REST server func NewServer(service factory.ServiceFactory, opts ...OptionFunc) factory.AppServerFactory { server := &restServer{ - serverEngine: echo.New(), - service: service, - opt: getDefaultOption(), + httpEngine: new(http.Server), + opt: getDefaultOption(), } for _, opt := range opts { opt(&server.opt) } - if server.opt.engineOption != nil { - server.opt.engineOption(server.serverEngine) - } - - if server.opt.sharedListener != nil { - server.listener = server.opt.sharedListener.Match(cmux.HTTP1Fast(http.MethodPatch)) - } - - server.serverEngine.HTTPErrorHandler = server.opt.errorHandler - server.serverEngine.Use(server.opt.rootMiddlewares...) - - server.serverEngine.GET("/", echo.WrapHandler(server.opt.rootHandler)) - server.serverEngine.GET("/memstats", - echo.WrapHandler(http.HandlerFunc(wrapper.HTTPHandlerMemstats)), - echo.WrapMiddleware(service.GetDependency().GetMiddleware().HTTPBasicAuth), - ) + mux := chi.NewRouter() + mux.Use(server.opt.rootMiddlewares...) + mux.Get("/", server.opt.rootHandler) + mux.Route("/memstats", func(r chi.Router) { + r.Use(service.GetDependency().GetMiddleware().HTTPBasicAuth) + r.Get("/", http.HandlerFunc(wrapper.HTTPHandlerMemstats)) + }) + mux.NotFound(func(w http.ResponseWriter, r *http.Request) { + wrapper.NewHTTPResponse(http.StatusNotFound, fmt.Sprintf(`Resource "%s %s" not found`, r.Method, r.URL.Path)).JSON(w) + }) - restRootPath := server.serverEngine.Group(server.opt.rootPath) + rootPath := mux.Route(server.opt.rootPath, func(chi.Router) {}) + route := &routeWrapper{router: rootPath} for _, m := range service.GetModules() { if h := m.RESTHandler(); h != nil { - h.Mount(restRootPath) + h.Mount(route) } } - httpRoutes := server.serverEngine.Routes() - sort.Slice(httpRoutes, func(i, j int) bool { - return httpRoutes[i].Path < httpRoutes[j].Path - }) - for _, route := range httpRoutes { - if !candihelper.StringInSlice(route.Path, []string{"/", "/memstats"}) && !strings.Contains(route.Name, "(*Group)") { - logger.LogGreen(fmt.Sprintf("[REST-ROUTE] %-6s %-30s --> %s", route.Method, route.Path, route.Name)) + chi.Walk(mux, func(method, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error { + if !candihelper.StringInSlice(route, []string{"/", "/memstats/"}) { + logger.LogGreen(fmt.Sprintf("[REST-ROUTE] %-6s %-30s", method, strings.TrimSuffix(route, "/"))) } - } + return nil + }) // inject graphql handler to rest server if server.opt.includeGraphQL { - graphqlHandler := graphqlserver.ConstructHandlerFromService(service, server.opt.graphqlOption) - server.serverEngine.Any(server.opt.rootPath+"/graphql", echo.WrapHandler(graphqlHandler.ServeGraphQL())) - server.serverEngine.GET(server.opt.rootPath+"/graphql/playground", echo.WrapHandler(http.HandlerFunc(graphqlHandler.ServePlayground))) - server.serverEngine.GET(server.opt.rootPath+"/graphql/voyager", echo.WrapHandler(http.HandlerFunc(graphqlHandler.ServeVoyager))) + gqlOpt := server.opt.graphqlOption + gqlRootPath := gqlOpt.RootPath + gqlOpt.RootPath = strings.Trim(server.opt.rootPath, "/") + gqlRootPath + graphqlHandler := graphqlserver.ConstructHandlerFromService(service, gqlOpt) + + rootPath.HandleFunc(gqlRootPath, graphqlHandler.ServeGraphQL()) + rootPath.Get(gqlRootPath+"/playground", http.HandlerFunc(graphqlHandler.ServePlayground)) + rootPath.Get(gqlRootPath+"/voyager", http.HandlerFunc(graphqlHandler.ServeVoyager)) } - fmt.Printf("\x1b[34;1m⇨ HTTP server run at port [::]:%d\x1b[0m\n\n", server.opt.httpPort) + server.httpEngine.Addr = fmt.Sprintf(":%d", server.opt.httpPort) + server.httpEngine.Handler = mux - return server -} + fmt.Printf("\x1b[34;1m⇨ HTTP server run at port [::]%s\x1b[0m\n\n", server.httpEngine.Addr) -func (h *restServer) Serve() { + if server.opt.sharedListener != nil { + server.listener = server.opt.sharedListener.Match(cmux.HTTP1Fast(http.MethodPatch)) + } - h.serverEngine.HideBanner = true - h.serverEngine.HidePort = true + return server +} +func (s *restServer) Serve() { var err error - if h.listener != nil { - h.serverEngine.Listener = h.listener - err = h.serverEngine.Start("") + if s.listener != nil { + err = s.httpEngine.Serve(s.listener) } else { - err = h.serverEngine.Start(fmt.Sprintf(":%d", h.opt.httpPort)) + err = s.httpEngine.ListenAndServe() } switch err.(type) { @@ -104,15 +98,15 @@ func (h *restServer) Serve() { } } -func (h *restServer) Shutdown(ctx context.Context) { +func (s *restServer) Shutdown(ctx context.Context) { defer log.Println("\x1b[33;1mStopping HTTP server:\x1b[0m \x1b[32;1mSUCCESS\x1b[0m") - h.serverEngine.Shutdown(ctx) - if h.listener != nil { - h.listener.Close() + s.httpEngine.Shutdown(ctx) + if s.listener != nil { + s.listener.Close() } } -func (h *restServer) Name() string { +func (s *restServer) Name() string { return string(types.REST) } diff --git a/codebase/app/rest_server/route_wrapper.go b/codebase/app/rest_server/route_wrapper.go new file mode 100644 index 00000000..8a59f3f7 --- /dev/null +++ b/codebase/app/rest_server/route_wrapper.go @@ -0,0 +1,98 @@ +package restserver + +import ( + "net/http" + "strings" + + "github.com/go-chi/chi/v5" + "github.com/golangid/candi/codebase/interfaces" +) + +type routeWrapper struct { + router chi.Router +} + +func (r *routeWrapper) Use(middlewares ...func(http.Handler) http.Handler) { + r.router.Use(middlewares...) +} + +func (r *routeWrapper) Group(pattern string, middlewares ...func(http.Handler) http.Handler) interfaces.RESTRouter { + route := r.router.Route(transformURLParam(pattern), func(chi.Router) {}) + if len(middlewares) > 0 { + route.Use(middlewares...) + } + return &routeWrapper{router: route} +} + +func (r *routeWrapper) HandleFunc(pattern string, h http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) { + r.router.HandleFunc(transformURLParam(pattern), WithChainingMiddlewares(h, middlewares...)) +} + +func (r *routeWrapper) CONNECT(pattern string, h http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) { + r.router.Connect(transformURLParam(pattern), WithChainingMiddlewares(h, middlewares...)) +} + +func (r *routeWrapper) DELETE(pattern string, h http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) { + r.router.Delete(transformURLParam(pattern), WithChainingMiddlewares(h, middlewares...)) +} + +func (r *routeWrapper) GET(pattern string, h http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) { + r.router.Get(transformURLParam(pattern), WithChainingMiddlewares(h, middlewares...)) +} + +func (r *routeWrapper) HEAD(pattern string, h http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) { + r.router.Head(transformURLParam(pattern), WithChainingMiddlewares(h, middlewares...)) +} + +func (r *routeWrapper) OPTIONS(pattern string, h http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) { + r.router.Options(transformURLParam(pattern), WithChainingMiddlewares(h, middlewares...)) +} + +func (r *routeWrapper) PATCH(pattern string, h http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) { + r.router.Patch(transformURLParam(pattern), WithChainingMiddlewares(h, middlewares...)) +} + +func (r *routeWrapper) POST(pattern string, h http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) { + r.router.Post(transformURLParam(pattern), WithChainingMiddlewares(h, middlewares...)) +} + +func (r *routeWrapper) PUT(pattern string, h http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) { + r.router.Put(transformURLParam(pattern), WithChainingMiddlewares(h, middlewares...)) +} + +func (r *routeWrapper) TRACE(pattern string, h http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) { + r.router.Trace(transformURLParam(pattern), WithChainingMiddlewares(h, middlewares...)) +} + +func transformURLParam(pattern string) string { + if strings.ContainsRune(pattern, '{') { + return pattern + } + + if strings.ContainsRune(pattern, ':') { + found := false + var newPattern strings.Builder + for i, c := range pattern { + if c == ':' { + found = true + c = '{' + } + if found && c == '/' { + newPattern.WriteRune('}') + found = false + } + newPattern.WriteRune(c) + if found && i == len(pattern)-1 { + newPattern.WriteRune('}') + } + } + pattern = newPattern.String() + } + + return pattern +} + +// URLParam bridging get url param with chi +func URLParam(r *http.Request, key string) string { + return chi.URLParam(r, key) +} diff --git a/codebase/app/task_queue_worker/graphql_resolver.go b/codebase/app/task_queue_worker/graphql_resolver.go index ecaf1002..f68e5567 100644 --- a/codebase/app/task_queue_worker/graphql_resolver.go +++ b/codebase/app/task_queue_worker/graphql_resolver.go @@ -36,7 +36,7 @@ func (t *taskQueueWorker) serveGraphQLAPI() { mux.Handle("/job", t.opt.basicAuth(http.StripPrefix("/", http.FileServer(dashboard.Dashboard)))) mux.Handle("/expired", t.opt.basicAuth(http.StripPrefix("/", http.FileServer(dashboard.Dashboard)))) - gqlHandler := graphqlserver.NewHandler(false, schema) + gqlHandler := graphqlserver.NewHandler(schema, graphqlserver.Option{}) mux.HandleFunc("/graphql", gqlHandler.ServeGraphQL()) mux.HandleFunc("/playground", gqlHandler.ServePlayground) mux.HandleFunc("/voyager", gqlHandler.ServeVoyager) diff --git a/codebase/app/task_queue_worker/trigger_task.go b/codebase/app/task_queue_worker/trigger_task.go index d5c1d3af..bcf3f295 100644 --- a/codebase/app/task_queue_worker/trigger_task.go +++ b/codebase/app/task_queue_worker/trigger_task.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "log" - "runtime/debug" "strconv" "strings" "time" @@ -121,7 +120,8 @@ func (t *taskQueueWorker) execJob(ctx context.Context, runningTask *Task) { err = fmt.Errorf("panic: %v", r) job.Error = err.Error() job.Status = string(StatusFailure) - trace.Log("stacktrace", string(debug.Stack())) + + tracer.LogStackTraceWhenPanic(trace) } job.FinishedAt = time.Now() diff --git a/codebase/interfaces/handlers.go b/codebase/interfaces/handlers.go index 47186071..a8728cf4 100644 --- a/codebase/interfaces/handlers.go +++ b/codebase/interfaces/handlers.go @@ -2,13 +2,12 @@ package interfaces import ( "github.com/golangid/candi/codebase/factory/types" - "github.com/labstack/echo" "google.golang.org/grpc" ) // RESTHandler delivery factory for REST handler (default using echo rest framework) type RESTHandler interface { - Mount(group *echo.Group) + Mount(group RESTRouter) } // GRPCHandler delivery factory for GRPC handler diff --git a/codebase/interfaces/rest_router.go b/codebase/interfaces/rest_router.go new file mode 100644 index 00000000..4b37bbfc --- /dev/null +++ b/codebase/interfaces/rest_router.go @@ -0,0 +1,19 @@ +package interfaces + +import "net/http" + +// RESTRouter for REST routing abstraction +type RESTRouter interface { + Use(middlewares ...func(http.Handler) http.Handler) + Group(pattern string, middlewares ...func(http.Handler) http.Handler) RESTRouter + HandleFunc(pattern string, h http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) + CONNECT(pattern string, h http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) + DELETE(pattern string, h http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) + GET(pattern string, h http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) + HEAD(pattern string, h http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) + OPTIONS(pattern string, h http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) + PATCH(pattern string, h http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) + POST(pattern string, h http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) + PUT(pattern string, h http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) + TRACE(pattern string, h http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) +} diff --git a/go.mod b/go.mod index 9ba56ce8..e4918933 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.18 require ( github.com/Shopify/sarama v1.38.1 github.com/gertd/go-pluralize v0.2.1 + github.com/go-chi/chi/v5 v5.0.8 github.com/go-playground/locales v0.14.1 github.com/go-playground/universal-translator v0.18.1 github.com/go-playground/validator/v10 v10.11.2 @@ -16,7 +17,6 @@ require ( github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.5.0 github.com/joho/godotenv v1.5.1 - github.com/labstack/echo v3.3.10+incompatible github.com/lib/pq v1.10.9 github.com/opentracing/opentracing-go v1.2.0 github.com/soheilhy/cmux v0.1.5 @@ -47,10 +47,7 @@ require ( github.com/jcmturner/rpc/v2 v2.0.3 // indirect github.com/klauspost/compress v1.16.5 // indirect github.com/kr/text v0.2.0 // indirect - github.com/labstack/gommon v0.4.0 // indirect github.com/leodido/go-urn v1.2.1 // indirect - github.com/mattn/go-colorable v0.1.11 // indirect - github.com/mattn/go-isatty v0.0.18 // indirect github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect github.com/pierrec/lz4/v4 v4.1.17 // indirect github.com/pkg/errors v0.9.1 // indirect @@ -58,8 +55,6 @@ require ( github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect github.com/stretchr/objx v0.5.0 // indirect github.com/uber/jaeger-lib v2.4.1+incompatible // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasttemplate v1.2.2 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect diff --git a/go.sum b/go.sum index 3147af8d..14f297bb 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,8 @@ github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/ github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/gertd/go-pluralize v0.2.1 h1:M3uASbVjMnTsPb0PNqg+E/24Vwigyo/tvyMTtAlLgiA= github.com/gertd/go-pluralize v0.2.1/go.mod h1:rbYaKDbsXxmRfr8uygAEKhOWsjyrrqrkHVpZvoOp8zk= +github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= +github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -87,19 +89,10 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg= -github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= -github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= -github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs= -github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= -github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= @@ -136,11 +129,6 @@ github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaO github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= -github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= @@ -207,12 +195,8 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -251,7 +235,6 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/middleware/bearer.go b/middleware/bearer.go index 46cd68f6..a7e926c4 100644 --- a/middleware/bearer.go +++ b/middleware/bearer.go @@ -28,7 +28,6 @@ func (m *Middleware) Bearer(ctx context.Context, tokenString string) (*candishar // HTTPBearerAuth http jwt token middleware func (m *Middleware) HTTPBearerAuth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - claim, err := func(req *http.Request) (*candishared.TokenClaim, error) { trace := tracer.StartTrace(req.Context(), "Middleware:HTTPBearerAuth") defer trace.Finish() diff --git a/middleware/cache.go b/middleware/cache.go index 083cb070..4deba25f 100644 --- a/middleware/cache.go +++ b/middleware/cache.go @@ -63,7 +63,7 @@ func (m *Middleware) HTTPCache(next http.Handler) http.Handler { trace, ctx := tracer.StartTraceWithContext(req.Context(), "Middleware:HTTPCache") defer trace.Finish() - cacheKey := req.Method + ":" + req.URL.String() + cacheKey := req.Method + ":" + strings.TrimSuffix(req.URL.String(), "/") trace.SetTag("key", cacheKey) if cacheVal, err := m.cache.Get(ctx, cacheKey); err == nil { if ttl, err := m.cache.GetTTL(ctx, cacheKey); err == nil { diff --git a/mocks/broker/OptionFunc.go b/mocks/broker/OptionFunc.go deleted file mode 100644 index b3d0a3c9..00000000 --- a/mocks/broker/OptionFunc.go +++ /dev/null @@ -1,18 +0,0 @@ -// Code generated by mockery v2.8.0. DO NOT EDIT. - -package mocks - -import ( - broker "github.com/golangid/candi/broker" - mock "github.com/stretchr/testify/mock" -) - -// OptionFunc is an autogenerated mock type for the OptionFunc type -type OptionFunc struct { - mock.Mock -} - -// Execute provides a mock function with given fields: _a0 -func (_m *OptionFunc) Execute(_a0 *broker.Broker) { - _m.Called(_a0) -} diff --git a/mocks/codebase/factory/types/WorkerErrorHandler.go b/mocks/codebase/factory/types/WorkerErrorHandler.go deleted file mode 100644 index 863589dd..00000000 --- a/mocks/codebase/factory/types/WorkerErrorHandler.go +++ /dev/null @@ -1,20 +0,0 @@ -// Code generated by mockery v2.9.4. DO NOT EDIT. - -package mocks - -import ( - context "context" - - types "github.com/golangid/candi/codebase/factory/types" - mock "github.com/stretchr/testify/mock" -) - -// WorkerErrorHandler is an autogenerated mock type for the WorkerErrorHandler type -type WorkerErrorHandler struct { - mock.Mock -} - -// Execute provides a mock function with given fields: ctx, workerType, workerName, message, err -func (_m *WorkerErrorHandler) Execute(ctx context.Context, workerType types.Worker, workerName string, message []byte, err error) { - _m.Called(ctx, workerType, workerName, message, err) -} diff --git a/mocks/codebase/interfaces/RESTHandler.go b/mocks/codebase/interfaces/RESTHandler.go index fa243c02..1c40960e 100644 --- a/mocks/codebase/interfaces/RESTHandler.go +++ b/mocks/codebase/interfaces/RESTHandler.go @@ -3,8 +3,7 @@ package mocks import ( - echo "github.com/labstack/echo" - + interfaces "github.com/golangid/candi/codebase/interfaces" mock "github.com/stretchr/testify/mock" ) @@ -14,7 +13,7 @@ type RESTHandler struct { } // Mount provides a mock function with given fields: group -func (_m *RESTHandler) Mount(group *echo.Group) { +func (_m *RESTHandler) Mount(group interfaces.RESTRouter) { _m.Called(group) } diff --git a/mocks/codebase/interfaces/RESTRouter.go b/mocks/codebase/interfaces/RESTRouter.go new file mode 100644 index 00000000..2d507123 --- /dev/null +++ b/mocks/codebase/interfaces/RESTRouter.go @@ -0,0 +1,184 @@ +// Code generated by mockery v2.13.1. DO NOT EDIT. + +package mocks + +import ( + http "net/http" + + interfaces "github.com/golangid/candi/codebase/interfaces" + mock "github.com/stretchr/testify/mock" +) + +// RESTRouter is an autogenerated mock type for the RESTRouter type +type RESTRouter struct { + mock.Mock +} + +// CONNECT provides a mock function with given fields: pattern, h, middlewares +func (_m *RESTRouter) CONNECT(pattern string, h http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) { + _va := make([]interface{}, len(middlewares)) + for _i := range middlewares { + _va[_i] = middlewares[_i] + } + var _ca []interface{} + _ca = append(_ca, pattern, h) + _ca = append(_ca, _va...) + _m.Called(_ca...) +} + +// DELETE provides a mock function with given fields: pattern, h, middlewares +func (_m *RESTRouter) DELETE(pattern string, h http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) { + _va := make([]interface{}, len(middlewares)) + for _i := range middlewares { + _va[_i] = middlewares[_i] + } + var _ca []interface{} + _ca = append(_ca, pattern, h) + _ca = append(_ca, _va...) + _m.Called(_ca...) +} + +// GET provides a mock function with given fields: pattern, h, middlewares +func (_m *RESTRouter) GET(pattern string, h http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) { + _va := make([]interface{}, len(middlewares)) + for _i := range middlewares { + _va[_i] = middlewares[_i] + } + var _ca []interface{} + _ca = append(_ca, pattern, h) + _ca = append(_ca, _va...) + _m.Called(_ca...) +} + +// Group provides a mock function with given fields: pattern, middlewares +func (_m *RESTRouter) Group(pattern string, middlewares ...func(http.Handler) http.Handler) interfaces.RESTRouter { + _va := make([]interface{}, len(middlewares)) + for _i := range middlewares { + _va[_i] = middlewares[_i] + } + var _ca []interface{} + _ca = append(_ca, pattern) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 interfaces.RESTRouter + if rf, ok := ret.Get(0).(func(string, ...func(http.Handler) http.Handler) interfaces.RESTRouter); ok { + r0 = rf(pattern, middlewares...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(interfaces.RESTRouter) + } + } + + return r0 +} + +// HEAD provides a mock function with given fields: pattern, h, middlewares +func (_m *RESTRouter) HEAD(pattern string, h http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) { + _va := make([]interface{}, len(middlewares)) + for _i := range middlewares { + _va[_i] = middlewares[_i] + } + var _ca []interface{} + _ca = append(_ca, pattern, h) + _ca = append(_ca, _va...) + _m.Called(_ca...) +} + +// HandleFunc provides a mock function with given fields: pattern, h, middlewares +func (_m *RESTRouter) HandleFunc(pattern string, h http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) { + _va := make([]interface{}, len(middlewares)) + for _i := range middlewares { + _va[_i] = middlewares[_i] + } + var _ca []interface{} + _ca = append(_ca, pattern, h) + _ca = append(_ca, _va...) + _m.Called(_ca...) +} + +// OPTIONS provides a mock function with given fields: pattern, h, middlewares +func (_m *RESTRouter) OPTIONS(pattern string, h http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) { + _va := make([]interface{}, len(middlewares)) + for _i := range middlewares { + _va[_i] = middlewares[_i] + } + var _ca []interface{} + _ca = append(_ca, pattern, h) + _ca = append(_ca, _va...) + _m.Called(_ca...) +} + +// PATCH provides a mock function with given fields: pattern, h, middlewares +func (_m *RESTRouter) PATCH(pattern string, h http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) { + _va := make([]interface{}, len(middlewares)) + for _i := range middlewares { + _va[_i] = middlewares[_i] + } + var _ca []interface{} + _ca = append(_ca, pattern, h) + _ca = append(_ca, _va...) + _m.Called(_ca...) +} + +// POST provides a mock function with given fields: pattern, h, middlewares +func (_m *RESTRouter) POST(pattern string, h http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) { + _va := make([]interface{}, len(middlewares)) + for _i := range middlewares { + _va[_i] = middlewares[_i] + } + var _ca []interface{} + _ca = append(_ca, pattern, h) + _ca = append(_ca, _va...) + _m.Called(_ca...) +} + +// PUT provides a mock function with given fields: pattern, h, middlewares +func (_m *RESTRouter) PUT(pattern string, h http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) { + _va := make([]interface{}, len(middlewares)) + for _i := range middlewares { + _va[_i] = middlewares[_i] + } + var _ca []interface{} + _ca = append(_ca, pattern, h) + _ca = append(_ca, _va...) + _m.Called(_ca...) +} + +// TRACE provides a mock function with given fields: pattern, h, middlewares +func (_m *RESTRouter) TRACE(pattern string, h http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) { + _va := make([]interface{}, len(middlewares)) + for _i := range middlewares { + _va[_i] = middlewares[_i] + } + var _ca []interface{} + _ca = append(_ca, pattern, h) + _ca = append(_ca, _va...) + _m.Called(_ca...) +} + +// Use provides a mock function with given fields: middlewares +func (_m *RESTRouter) Use(middlewares ...func(http.Handler) http.Handler) { + _va := make([]interface{}, len(middlewares)) + for _i := range middlewares { + _va[_i] = middlewares[_i] + } + var _ca []interface{} + _ca = append(_ca, _va...) + _m.Called(_ca...) +} + +type mockConstructorTestingTNewRESTRouter interface { + mock.TestingT + Cleanup(func()) +} + +// NewRESTRouter creates a new instance of RESTRouter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewRESTRouter(t mockConstructorTestingTNewRESTRouter) *RESTRouter { + mock := &RESTRouter{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/tracer/tracer.go b/tracer/tracer.go index fae9cd02..d68532d2 100644 --- a/tracer/tracer.go +++ b/tracer/tracer.go @@ -2,6 +2,7 @@ package tracer import ( "context" + "runtime" "sync" "github.com/golangid/candi/candishared" @@ -72,6 +73,14 @@ func GetTraceURL(ctx context.Context) (u string) { return activeTracer.GetTraceURL(ctx) } +// LogStackTraceWhenPanic log stack trace in recover panic +func LogStackTraceWhenPanic(trace Tracer) { + const size = 2 << 10 + buf := make([]byte, size) + buf = buf[:runtime.Stack(buf, false)] + trace.Log("panic_trace", buf) +} + type noopTracer struct{ ctx context.Context } func (n noopTracer) Context() context.Context { return n.ctx } diff --git a/wrapper/http_handler.go b/wrapper/http_handler.go index 11f63a3b..b67c66d6 100644 --- a/wrapper/http_handler.go +++ b/wrapper/http_handler.go @@ -58,6 +58,10 @@ func HTTPMiddlewareTracer(cfg HTTPMiddlewareTracerConfig) func(http.Handler) htt trace, ctx := tracer.StartTraceFromHeader(req.Context(), operationName, header) defer func() { + if rec := recover(); rec != nil { + tracer.LogStackTraceWhenPanic(trace) + NewHTTPResponse(http.StatusInternalServerError, fmt.Sprintf("panic: %v", rec)).JSON(rw) + } trace.SetTag("trace_id", tracer.GetTraceID(ctx)) trace.Finish() logger.LogGreen("rest_server > trace_url: " + tracer.GetTraceURL(ctx)) @@ -163,6 +167,94 @@ func HTTPMiddlewareCORS( } } +// HTTPMiddlewareLog middleware +func HTTPMiddlewareLog(isActive bool, writer io.Writer) func(http.Handler) http.Handler { + bPool := &sync.Pool{ + New: func() interface{} { + return bytes.NewBuffer(make([]byte, 256)) + }, + } + + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + if !isActive { + next.ServeHTTP(res, req) + return + } + + start := time.Now() + + resBody := bytes.NewBuffer(make([]byte, 256)) + respWriter := NewWrapHTTPResponseWriter(resBody, res) + next.ServeHTTP(respWriter, req) + + stop := time.Now() + buf := bPool.Get().(*bytes.Buffer) + buf.Reset() + + buf.WriteString(`{"time":"`) + buf.WriteString(time.Now().Format(time.RFC3339Nano)) + + buf.WriteString(`","id":"`) + id := req.Header.Get("X-Request-ID") + if id == "" { + id = res.Header().Get("X-Request-ID") + } + buf.WriteString(id) + + buf.WriteString(`","remote_ip":"`) + buf.WriteString(req.Header.Get("X-Real-IP")) + + buf.WriteString(`","host":"`) + buf.WriteString(req.Host) + + buf.WriteString(`","method":"`) + buf.WriteString(req.Method) + + buf.WriteString(`","uri":"`) + buf.WriteString(req.RequestURI) + + buf.WriteString(`","user_agent":"`) + buf.WriteString(req.UserAgent()) + + buf.WriteString(`","status":`) + + s := logger.GreenColor(respWriter.statusCode) + switch { + case respWriter.statusCode >= 500: + s = logger.RedColor(respWriter.statusCode) + case respWriter.statusCode >= 400: + s = logger.YellowColor(respWriter.statusCode) + case respWriter.statusCode >= 300: + s = logger.CyanColor(respWriter.statusCode) + } + buf.WriteString(s) + + buf.WriteString(`","latency":`) + l := stop.Sub(start) + buf.WriteString(strconv.FormatInt(int64(l), 10)) + + buf.WriteString(`,"latency_human":"`) + buf.WriteString(stop.Sub(start).String()) + + buf.WriteString(`","bytes_in":`) + cl := req.Header.Get("Content-Length") + if cl == "" { + cl = "0" + } + buf.WriteString(cl) + + buf.WriteString(`,"bytes_out":`) + buf.WriteString(strconv.FormatInt(int64(respWriter.contentLength), 10)) + + buf.WriteString("}\n") + + io.Copy(writer, buf) + bPool.Put(buf) + }) + } +} + // HTTPHandlerDefaultRoot default root http handler func HTTPHandlerDefaultRoot(w http.ResponseWriter, r *http.Request) { now := time.Now() diff --git a/wrapper/http_response_test.go b/wrapper/http_response_test.go index 57688620..9316d765 100644 --- a/wrapper/http_response_test.go +++ b/wrapper/http_response_test.go @@ -10,7 +10,6 @@ import ( "github.com/golangid/candi/candihelper" "github.com/golangid/candi/candishared" - "github.com/labstack/echo" "github.com/stretchr/testify/assert" ) @@ -118,23 +117,13 @@ func TestNewHTTPResponse(t *testing.T) { } func TestHTTPResponse_JSON(t *testing.T) { - e := echo.New() - req, err := http.NewRequest(echo.GET, "/testing", nil) - assert.NoError(t, err) - rec := httptest.NewRecorder() - c := e.NewContext(req, rec) resp := NewHTTPResponse(200, "success") - assert.NoError(t, resp.JSON(c.Response())) + assert.NoError(t, resp.JSON(rec)) } func TestHTTPResponse_XML(t *testing.T) { - e := echo.New() - req, err := http.NewRequest(echo.GET, "/testing", nil) - assert.NoError(t, err) - rec := httptest.NewRecorder() - c := e.NewContext(req, rec) resp := NewHTTPResponse(200, "success") - assert.NoError(t, resp.XML(c.Response())) + assert.NoError(t, resp.XML(rec)) }