diff --git a/.gitignore b/.gitignore index 4a8e0dbe..83b5512f 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,7 @@ .vscode .idea golangci-lint -bean +bean$ main internal/project/go.mod internal/project/go.sum diff --git a/internal/project/commands/root.go b/internal/project/commands/root.go index a0746555..19016b15 100644 --- a/internal/project/commands/root.go +++ b/internal/project/commands/root.go @@ -4,7 +4,10 @@ package commands import ( "log" "os" - "runtime/debug" + + /**#bean*/ + "demo/framework/internals/helpers" + /*#bean.replace("{{ .PkgPath }}/framework/internals/helpers")**/ "github.com/spf13/cobra" "github.com/spf13/viper" @@ -34,12 +37,7 @@ func init() { // Cobra also supports local flags, which will only run // when this action is called directly. rootCmd.CompletionOptions.DisableDefaultCmd = true - - if bi, ok := debug.ReadBuildInfo(); ok { - rootCmd.Version = bi.Main.Version - } else { - log.Fatalln("Failed to read build info") - } + rootCmd.Version = helpers.CurrVersion() viper.AddConfigPath(".") viper.SetConfigType("json") diff --git a/internal/project/commands/start.go b/internal/project/commands/start.go index f3f76ab6..ffce2948 100644 --- a/internal/project/commands/start.go +++ b/internal/project/commands/start.go @@ -6,6 +6,9 @@ import ( "demo/framework/bean" /*#bean.replace("{{ .PkgPath }}/framework/bean")**/ /**#bean*/ + berror "demo/framework/internals/error" + /*#bean.replace(berror "{{ .PkgPath }}/framework/internals/error")**/ + /**#bean*/ beanValidator "demo/framework/internals/validator" /*#bean.replace(beanValidator "{{ .PkgPath }}/framework/internals/validator")**/ /**#bean*/ @@ -35,15 +38,6 @@ var ( func init() { rootCmd.AddCommand(startCmd) - - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // startCmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: defaultHost := viper.GetString("http.host") defaultPort := viper.GetString("http.port") startCmd.Flags().StringVar(&host, "host", defaultHost, "host address") @@ -58,31 +52,50 @@ func start(cmd *cobra.Command, args []string) { // Create a bean object b := bean.New() - b.BeforeServe = func() { - // Init global middleware if you need - // middlerwares.Init() - - // Init DB dependency. - b.InitDB() - - // Init different routes. - routers.Init(b) - } - // Below is an example of how you can initialize your own validator. Just create a new directory // as `packages/validator` and create a validator package inside the directory. Then initialize your // own validation function here, such as; `validator.MyTestValidationFunction(c, vd)`. b.Validate = func(c echo.Context, vd *validator.Validate) { beanValidator.TestUserIdValidation(c, vd) - // Add your own validation function here. } - // Below is an example of how you can set custom error handler middleware - // bean can call `UseErrorHandlerMiddleware` multiple times - b.UseErrorHandlerMiddleware(func(e error, c echo.Context) (bool, error) { - return false, nil - }) + // Set custom middleware in here. + b.UseMiddlewares( + // Example: + // func(arg string) echo.MiddlewareFunc { + // return func(next echo.HandlerFunc) echo.HandlerFunc { + // return func(c echo.Context) error { + // c.Logger().Info(arg) + // return next(c) + // } + // } + // }("example"), + ) + + // Set custom error handler function here. + // Bean use a error function chain inside the default http error handler, + // so that it can easily add or remove the different kind of error handling. + b.UseErrorHandlerFuncs( + berror.ValidationErrorHanderFunc, + berror.APIErrorHanderFunc, + berror.EchoHTTPErrorHanderFunc, + // Set your custom error handler func here, for example: + // func(e error, c echo.Context) (bool, error) { + // return false, nil + // }, + ) + + b.BeforeServe = func() { + // Init DB dependency. + b.InitDB() + + // Init different routes. + routers.Init(b) + + // You can also replace the default error handler. + // b.Echo.HTTPErrorHandler = YourErrorHandler() + } b.ServeAt(host, port) } diff --git a/internal/project/env.json b/internal/project/env.json index 855f3b98..d0efb23a 100644 --- a/internal/project/env.json +++ b/internal/project/env.json @@ -14,11 +14,10 @@ "skipEndpoints": ["/ping", "/route/stats"] }, "http": { - "port": 8888, + "port": "8888", "host": "0.0.0.0", "bodyLimit": "1M", "isHttpsRedirect": false, - "uriLatencyIntervals": [5, 10, 15], "timeout": "24s", "keepAlive": true }, @@ -66,14 +65,14 @@ "redis": { "password": "64vc7632-62dc-482e-67fg-046c7faec493", "host": "127.0.0.1", - "port": 6379, + "port": "6379", "name": 3, "prefix": "{{ .PkgName }}_queue", "poolsize": 100, "maxidle": 2 }, "health": { - "port": 7777, + "port": "7777", "host": "0.0.0.0" } }, @@ -83,9 +82,9 @@ }, "sentry": { "on": true, + "debug": true, "dsn": "", "timeout": "5s", - "attachStacktrace": true, "tracesSampleRate": 1.0 }, "security": { diff --git a/internal/project/framework/bean/bean.go b/internal/project/framework/bean/bean.go index 81253a4f..1510b1f3 100644 --- a/internal/project/framework/bean/bean.go +++ b/internal/project/framework/bean/bean.go @@ -3,6 +3,7 @@ package bean import ( "net/http" + /**#bean*/ "demo/framework/kernel" /*#bean.replace("{{ .PkgPath }}/framework/kernel")**/ @@ -18,8 +19,12 @@ import ( /**#bean*/ validate "demo/framework/internals/validator" /*#bean.replace(validate "{{ .PkgPath }}/framework/internals/validator")**/ + /**#bean*/ + "demo/packages/options" + /*#bean.replace("{{ .PkgPath }}/packages/options")**/ "github.com/dgraph-io/badger/v3" + "github.com/getsentry/sentry-go" "github.com/go-playground/validator/v10" "github.com/go-redis/redis/v8" "github.com/labstack/echo/v4" @@ -46,12 +51,12 @@ type DBDeps struct { } type Bean struct { - DBConn *DBDeps - Echo *echo.Echo - Environment string - Validate func(c echo.Context, vd *validator.Validate) - BeforeServe func() - errorHandlerMiddlewares []berror.ErrorHandlerMiddleware + DBConn *DBDeps + Echo *echo.Echo + Environment string + Validate func(c echo.Context, vd *validator.Validate) + BeforeServe func() + errorHandlerFuncs []berror.ErrorHandlerFunc } func New() (b *Bean) { @@ -69,27 +74,16 @@ func New() (b *Bean) { } func (b *Bean) ServeAt(host, port string) { - // before bean bootstrap - if b.BeforeServe != nil { - b.BeforeServe() - } - - b.UseErrorHandlerMiddleware( - berror.ValidationErrorHanderMiddleware, - berror.APIErrorHanderMiddleware, - berror.HTTPErrorHanderMiddleware, - berror.DefaultErrorHanderMiddleware, - ) + projectName := viper.GetString("projectName") + env := viper.GetString("environment") + b.Echo.Logger.Info("Starting " + projectName + " at " + env + "...🚀") - b.Echo.HTTPErrorHandler = berror.ErrorHandlerChain(b.errorHandlerMiddlewares...) + b.UseErrorHandlerFuncs(berror.DefaultErrorHanderFunc) + b.Echo.HTTPErrorHandler = DefaultHTTPErrorHandler(b.errorHandlerFuncs...) // Initialize and bind the validator to echo instance validate.BindCustomValidator(b.Echo, b.Validate) - projectName := viper.GetString("name") - - b.Echo.Logger.Info(`Starting ` + projectName + ` server...🚀`) - s := http.Server{ Addr: host + ":" + port, Handler: b.Echo, @@ -99,17 +93,49 @@ func (b *Bean) ServeAt(host, port string) { // for it :) s.SetKeepAlivesEnabled(viper.GetBool("http.keepAlive")) + // before bean bootstrap + if b.BeforeServe != nil { + b.BeforeServe() + } + // Start the server if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed { b.Echo.Logger.Fatal(err) } } -func (b *Bean) UseErrorHandlerMiddleware(errorHandlerMiddleware ...berror.ErrorHandlerMiddleware) { - if b.errorHandlerMiddlewares == nil { - b.errorHandlerMiddlewares = []berror.ErrorHandlerMiddleware{} +func (b *Bean) UseMiddlewares(middlewares ...echo.MiddlewareFunc) { + b.Echo.Use(middlewares...) +} + +func (b *Bean) UseErrorHandlerFuncs(errHdlrFuncs ...berror.ErrorHandlerFunc) { + if b.errorHandlerFuncs == nil { + b.errorHandlerFuncs = []berror.ErrorHandlerFunc{} + } + b.errorHandlerFuncs = append(b.errorHandlerFuncs, errHdlrFuncs...) +} + +func DefaultHTTPErrorHandler(errHdlrFuncs ...berror.ErrorHandlerFunc) echo.HTTPErrorHandler { + return func(err error, c echo.Context) { + + if c.Response().Committed { + return + } + + for _, handle := range errHdlrFuncs { + handled, err := handle(err, c) + if err != nil { + if options.SentryOn { + sentry.CaptureException(err) + } else { + c.Logger().Error(err) + } + } + if handled { + break + } + } } - b.errorHandlerMiddlewares = append(b.errorHandlerMiddlewares, errorHandlerMiddleware...) } // InitDB initialize all the database dependencies and store it in global variable `global.DBConn`. diff --git a/internal/project/framework/bean/bean_test.go b/internal/project/framework/bean/bean_test.go new file mode 100644 index 00000000..f9ae4a79 --- /dev/null +++ b/internal/project/framework/bean/bean_test.go @@ -0,0 +1,107 @@ +/**#bean*/ +/*#bean.replace({{ .Copyright }})**/ +package bean + +import ( + "log" + "net/http" + "net/http/httptest" + "os" + "path" + "runtime" + "testing" + + "github.com/labstack/echo/v4" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" +) + +func init() { + _, filename, _, _ := runtime.Caller(0) + // The ".." may change depending on you folder structure + dir := path.Join(path.Dir(filename), "../../") + err := os.Chdir(dir) + if err != nil { + panic(err) + } + + viper.AddConfigPath(".") + viper.SetConfigType("json") + viper.SetConfigName("env") + + if err := viper.ReadInConfig(); err != nil { + log.Fatalln(err) + } +} + +func TestBean_UseErrorHandlerFuncs(t *testing.T) { + b := New() + assert.Empty(t, b.errorHandlerFuncs) + + b.UseErrorHandlerFuncs(func(err error, c echo.Context) (bool, error) { + return true, nil + }) + assert.Equal(t, 1, len(b.errorHandlerFuncs)) +} + +func TestDefaultHTTPErrorHandler(t *testing.T) { + b := New() + b.UseErrorHandlerFuncs( + func(err error, c echo.Context) (bool, error) { + he, ok := err.(*fakeError) + if !ok { + return false, nil + } + err = c.JSON(http.StatusBadRequest, map[string]interface{}{ + "errorCode": "fake code", + "errors": he.Error(), + }) + return ok, err + }, + func(_ error, c echo.Context) (bool, error) { + err := c.JSON(http.StatusInternalServerError, map[string]interface{}{ + "errorCode": "default code", + "errors": "default catched!", + }) + return true, err + }, + ) + b.Echo.HTTPErrorHandler = DefaultHTTPErrorHandler() + + b.Echo.Any("/fake", func(c echo.Context) error { + return newFakeError("fake error") + }) + b.Echo.Any("/default", func(c echo.Context) error { + return echo.NewHTTPError(http.StatusInternalServerError, "default error") + }) + + // With Debug=true plain response contains error message + code, body := request(http.MethodGet, "/fake", b.Echo) + assert.Equal(t, http.StatusBadRequest, code) + assert.Equal(t, `{"errorCode":"fake code","errors":"fake error"}`+"\n", body) + // and special handling for HTTPError + code, body = request(http.MethodGet, "/default", b.Echo) + assert.Equal(t, http.StatusInternalServerError, code) + assert.Equal(t, `{"errorCode":"default code","errors":"default catched!"}`+"\n", body) +} + +func request(method, path string, e *echo.Echo) (int, string) { + req := httptest.NewRequest(method, path, nil) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + return rec.Code, rec.Body.String() +} + +type fakeError struct { + Message string +} + +func (f *fakeError) Error() string { + return f.Message +} + +func newFakeError(msg string) error { + return &fakeError{ + Message: msg, + } +} diff --git a/internal/project/framework/internals/async/async.go b/internal/project/framework/internals/async/async.go index 451e4690..d0a75e13 100644 --- a/internal/project/framework/internals/async/async.go +++ b/internal/project/framework/internals/async/async.go @@ -4,9 +4,10 @@ package async import ( /**#bean*/ - "demo/framework/internals/sentry" - /*#bean.replace("{{ .PkgPath }}/framework/internals/sentry")**/ + "demo/packages/options" + /*#bean.replace("{{ .PkgPath }}/packages/options")**/ + "github.com/getsentry/sentry-go" "github.com/labstack/echo/v4" ) @@ -15,22 +16,26 @@ type Task func(c echo.Context) // `Execute` provides a safe way to execute a function asynchronously, recovering if they panic // and provides all error stack aiming to facilitate fail causes discovery. func Execute(fn Task, e *echo.Echo) { - go func() { - // Acquire a context from global echo instance and reset it to avoid race condition. - c := e.AcquireContext() - c.Reset(nil, nil) - + c := e.AcquireContext() // Acquire a context from echo. + c.Reset(nil, nil) // IMPORTANT: It must be reset before use. defer recoverPanic(c) fn(c) }() } -// Write the error to console or sentry when a goroutine of a task panics. +// Recover the panic and send the exception to sentry. func recoverPanic(c echo.Context) { - - if r := recover(); r != nil { - sentry.PushData(c, r, nil, false) + if err := recover(); err != nil { + // Create a new Hub by cloning the existing one. + if options.SentryOn { + localHub := sentry.CurrentHub().Clone() + localHub.ConfigureScope(func(scope *sentry.Scope) { + scope.SetTag("goroutine", "true") + }) + localHub.Recover(err) + } + c.Logger().Error(err) } // Release the acquired context. diff --git a/internal/project/framework/internals/error/api_error.go b/internal/project/framework/internals/error/api_error.go new file mode 100644 index 00000000..83349595 --- /dev/null +++ b/internal/project/framework/internals/error/api_error.go @@ -0,0 +1,40 @@ +/**#bean*/ /*#bean.replace({{ .Copyright }})**/ +package error + +import ( + /**#bean*/ + "demo/framework/internals/stacktrace" + /*#bean.replace("{{ .PkgPath }}/framework/internals/stacktrace")**/ + + "fmt" +) + +// APIError represents the error object of {{ .PkgPath }} API error. +type APIError struct { + HTTPStatusCode int + GlobalErrCode ErrorCode + Err error + *stacktrace.Stack +} + +// NewAPIError returns the proper error object from {{ .PkgPath }}. You must provide `error` interface as 3rd parameter. +func NewAPIError(HTTPStatusCode int, globalErrCode ErrorCode, err error) *APIError { + return &APIError{ + HTTPStatusCode: HTTPStatusCode, + GlobalErrCode: globalErrCode, + Err: err, + Stack: stacktrace.Callers(), + } +} + +// This function need to be call explicitly because the APIError embedded the *stacktrace.Stack which already implemented the Format() +// function and treat it as a formatter. Example: fmt.Println(e.String()) +func (e *APIError) String() string { + return fmt.Sprintf(`{"HTTPStatusCode":%d, "GlobalErrCode":%s, "Err":%s}`, e.HTTPStatusCode, e.GlobalErrCode, e.Err) +} + +// This function need to be call explicitly because the APIError embedded the *stacktrace.Stack which already implemented the Format() +// function and treat it as a formatter. Example: fmt.Println(e.String()) +func (e *APIError) Error() string { + return e.Err.Error() +} diff --git a/internal/project/framework/internals/error/error.go b/internal/project/framework/internals/error/error.go deleted file mode 100644 index 15d8402d..00000000 --- a/internal/project/framework/internals/error/error.go +++ /dev/null @@ -1,77 +0,0 @@ -/**#bean*/ /*#bean.replace({{ .Copyright }})**/ -package error - -import ( - "encoding/json" - "errors" - "fmt" - "io" - - /**#bean*/ - "demo/framework/internals/stacktrace" - /*#bean.replace("{{ .PkgPath }}/framework/internals/stacktrace")**/) - -var ( - ErrInternalServer = errors.New("internal server error") - ErrInvalidJsonResponse = errors.New("invalid JSON response") - ErrContextExtraction = errors.New("some data is missing in the context") - ErrParamMissing = errors.New("parameters are missing") - ErrUpstreamTimeout = errors.New("timeout from upstream server") - ErrTimeout = errors.New("timeout") -) - -// APIError represents the error object of bean API error. -type APIError struct { - HTTPStatusCode int - GlobalErrCode ErrorCode - Err error - *stacktrace.Stack -} - -// NewAPIError returns the proper error object from bean. You must provide `error` interface as 3rd parameter. -func NewAPIError(HTTPStatusCode int, globalErrCode ErrorCode, err error) *APIError { - - return &APIError{ - HTTPStatusCode: HTTPStatusCode, - GlobalErrCode: globalErrCode, - Err: err, - Stack: stacktrace.Callers(), - } -} - -// Format implements the `Formatter` interface -func (e *APIError) Format(s fmt.State, verb rune) { - - tmp := struct { - HTTPStatusCode int - GlobalErrCode string - Err string - }{ - e.HTTPStatusCode, - string(e.GlobalErrCode), - e.Error(), - } - - out, err := json.Marshal(tmp) - if err != nil { - panic(err) - } - - switch verb { - case 'v': - if s.Flag('+') { - io.WriteString(s, string(out)) - return - } - fallthrough - case 's': - io.WriteString(s, string(out)) - case 'q': - fmt.Fprintf(s, "%q", out) - } -} - -func (e *APIError) Error() string { - - return e.Err.Error() -} diff --git a/internal/project/framework/internals/error/api_error_codes.go b/internal/project/framework/internals/error/error_codes.go similarity index 56% rename from internal/project/framework/internals/error/api_error_codes.go rename to internal/project/framework/internals/error/error_codes.go index 5bdb4749..e2b3d440 100644 --- a/internal/project/framework/internals/error/api_error_codes.go +++ b/internal/project/framework/internals/error/error_codes.go @@ -1,14 +1,14 @@ /**#bean*/ /*#bean.replace({{ .Copyright }})**/ package error +import "errors" + type ErrorCode string const ( API_SUCCESS ErrorCode = "000000" -) -// API general error code -const ( + // API general error code PROBLEM_PARSING_JSON ErrorCode = "100001" UNAUTHORIZED_ACCESS ErrorCode = "100002" RESOURCE_NOT_FOUND ErrorCode = "100003" @@ -18,9 +18,16 @@ const ( TOO_MANY_REQUESTS ErrorCode = "100010" UNKNOWN_ERROR_CODE ErrorCode = "100098" TIMEOUT ErrorCode = "100099" -) -// API parameter error code -const ( + // API parameter error code API_DATA_VALIDATION_FAILED ErrorCode = "200001" ) + +var ( + ErrInternalServer = errors.New("internal server error") + ErrInvalidJsonResponse = errors.New("invalid JSON response") + ErrContextExtraction = errors.New("some data is missing in the context") + ErrParamMissing = errors.New("parameters are missing") + ErrUpstreamTimeout = errors.New("timeout from upstream server") + ErrTimeout = errors.New("timeout") +) diff --git a/internal/project/framework/internals/error/error_handler.go b/internal/project/framework/internals/error/error_handler.go index 986c1ce8..c29c7d8e 100644 --- a/internal/project/framework/internals/error/error_handler.go +++ b/internal/project/framework/internals/error/error_handler.go @@ -3,103 +3,103 @@ package error import ( "net/http" - "strings" - /**#bean*/ - "demo/framework/internals/sentry" - /*#bean.replace("{{ .PkgPath }}/framework/internals/sentry")**/ /**#bean*/ "demo/framework/internals/validator" /*#bean.replace("{{ .PkgPath }}/framework/internals/validator")**/ + "github.com/getsentry/sentry-go" "github.com/labstack/echo/v4" ) type errorResp struct { - ErrorCode ErrorCode `json:"errorCode"` - Errors []map[string]string `json:"errors"` - ErrorMsg interface{} `json:"errorMsg"` + ErrorCode ErrorCode `json:"errorCode"` + ErrorMsg interface{} `json:"errorMsg"` } -// HTTPErrorHandler is a middleware which handles all the error. -func HTTPErrorHandler(err error, c echo.Context) { +type ErrorHandlerFunc func(err error, c echo.Context) (bool, error) - if c.Response().Committed { - return +func ValidationErrorHanderFunc(e error, c echo.Context) (bool, error) { + he, ok := e.(*validator.ValidationError) + if !ok { + return false, nil } + err := c.JSON(http.StatusUnprocessableEntity, errorResp{ + ErrorCode: API_DATA_VALIDATION_FAILED, + ErrorMsg: he.ErrCollection(), + }) - switch he := err.(type) { + return ok, err +} - case *validator.ValidationError: +func APIErrorHanderFunc(e error, c echo.Context) (bool, error) { + he, ok := e.(*APIError) + if !ok { + return false, nil + } - err := c.JSON(http.StatusUnprocessableEntity, errorResp{ - ErrorCode: API_DATA_VALIDATION_FAILED, - Errors: he.ErrCollection(), - ErrorMsg: nil, - }) + if he.HTTPStatusCode >= 404 { + // Send error event to sentry. + sentry.CaptureException(he) + } - if err != nil { - sentry.PushData(c, err, nil, true) - } + err := c.JSON(he.HTTPStatusCode, errorResp{ + ErrorCode: he.GlobalErrCode, + ErrorMsg: he.Error(), + }) - return + return ok, err +} - case *APIError: +func EchoHTTPErrorHanderFunc(e error, c echo.Context) (bool, error) { + he, ok := e.(*echo.HTTPError) + if !ok { + return false, nil + } - if he.HTTPStatusCode >= 404 { - sentry.PushData(c, he, nil, true) - } + // Send error event to sentry. + sentry.CaptureException(he) - err := c.JSON(he.HTTPStatusCode, errorResp{ - ErrorCode: he.GlobalErrCode, - Errors: nil, - ErrorMsg: he.Error(), + // Return different response base on some defined error. + var err error + switch he.Code { + case http.StatusNotFound: + err = c.JSON(he.Code, errorResp{ + ErrorCode: RESOURCE_NOT_FOUND, + ErrorMsg: he.Message, }) - - if err != nil { - sentry.PushData(c, err, nil, true) - } - - return - - case *echo.HTTPError: - - // Just in case to capture this unused type error. - err := c.JSON(he.Code, errorResp{ + case http.StatusMethodNotAllowed: + err = c.JSON(he.Code, errorResp{ + ErrorCode: METHOD_NOT_ALLOWED, + ErrorMsg: he.Message, + }) + default: + err = c.JSON(he.Code, errorResp{ ErrorCode: UNKNOWN_ERROR_CODE, - Errors: nil, ErrorMsg: he.Message, }) + } - if err != nil { - sentry.PushData(c, err, nil, true) - } - - return + return ok, err +} - default: +func DefaultErrorHanderFunc(err error, c echo.Context) (bool, error) { + // Send error event to sentry. + sentry.CaptureException(err) - // Get Content-Type parameter from request header to identify the request content type. If the request is for - // html then we should display the error in html. - contentType := c.Request().Header.Get("Content-Type") - - if strings.ToLower(contentType) == "text/html" { - c.HTML(http.StatusInternalServerError, "Internal server error.") - return - } - - // All other panic errors. - // Sentry already captured the panic and send notification in sentry-recover middleware. - err := c.JSON(http.StatusInternalServerError, errorResp{ - ErrorCode: INTERNAL_SERVER_ERROR, - Errors: nil, - ErrorMsg: nil, // TODO: Put some generic message. - }) + // Get Content-Type parameter from request header to identify the request content type. If the request is for + // html then we should display the error in html. + if c.Request().Header.Get("Content-Type") == "text/html" { + err := c.HTML(http.StatusInternalServerError, "Internal server error.") + return true, err + } - if err != nil { - sentry.PushData(c, err, nil, true) - } + // All other uncaught errors. + // Sentry already captured the panic and send notification in sentry-recover middleware. + err = c.JSON(http.StatusInternalServerError, errorResp{ + ErrorCode: INTERNAL_SERVER_ERROR, + ErrorMsg: err.Error(), + }) - return - } + return true, err } diff --git a/internal/project/framework/internals/error/error_handler_test.go b/internal/project/framework/internals/error/error_handler_test.go new file mode 100644 index 00000000..a4ef43eb --- /dev/null +++ b/internal/project/framework/internals/error/error_handler_test.go @@ -0,0 +1,103 @@ +/**#bean*/ /*#bean.replace({{ .Copyright }})**/ +package error + +import ( + /**#bean*/ + ivalidator "demo/framework/internals/validator" + /*#bean.replace(ivalidator "{{ .PkgPath }}/framework/internals/validator")**/ + + "errors" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-playground/validator/v10" + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" +) + +type dummyWriter struct{ io.Writer } + +func (dummyWriter) Header() http.Header { return http.Header{} } +func (dummyWriter) WriteHeader(statusCode int) {} + +type fakeError struct{ Message string } + +func (f *fakeError) Error() string { return f.Message } + +func TestValidationErrorHanderFunc(t *testing.T) { + e := echo.New() + c := e.AcquireContext() + c.SetRequest(httptest.NewRequest("", "/", nil)) + c.SetResponse(echo.NewResponse(dummyWriter{io.Discard}, e)) + + fakeErr := &fakeError{"fake"} + got, err := ValidationErrorHanderFunc(fakeErr, c) + assert.NoError(t, err) + assert.Equal(t, false, got) + + validateErr := &ivalidator.ValidationError{Err: validator.ValidationErrors{}} + got, err = ValidationErrorHanderFunc(validateErr, c) + assert.NoError(t, err) + assert.Equal(t, true, got) + + e.ReleaseContext(c) +} + +func TestAPIErrorHanderFunc(t *testing.T) { + e := echo.New() + c := e.AcquireContext() + c.SetRequest(httptest.NewRequest("", "/", nil)) + c.SetResponse(echo.NewResponse(dummyWriter{io.Discard}, e)) + + fakeErr := &fakeError{"fake"} + got, err := APIErrorHanderFunc(fakeErr, c) + assert.NoError(t, err) + assert.Equal(t, false, got) + + apiErr := NewAPIError(http.StatusInternalServerError, INTERNAL_SERVER_ERROR, errors.New("internal")) + got, err = APIErrorHanderFunc(apiErr, c) + assert.NoError(t, err) + assert.Equal(t, true, got) + + e.ReleaseContext(c) +} + +func TestEchoHTTPErrorHanderFunc(t *testing.T) { + e := echo.New() + c := e.AcquireContext() + c.SetRequest(httptest.NewRequest("", "/", nil)) + c.SetResponse(echo.NewResponse(dummyWriter{io.Discard}, e)) + + fakeErr := &fakeError{"fake"} + got, err := EchoHTTPErrorHanderFunc(fakeErr, c) + assert.NoError(t, err) + assert.Equal(t, false, got) + + echoHTTPErr := echo.NewHTTPError(http.StatusInternalServerError, "internal") + got, err = EchoHTTPErrorHanderFunc(echoHTTPErr, c) + assert.NoError(t, err) + assert.Equal(t, true, got) + + e.ReleaseContext(c) +} + +func TestDefaultErrorHanderFunc(t *testing.T) { + e := echo.New() + c := e.AcquireContext() + c.SetRequest(httptest.NewRequest("", "/", nil)) + c.SetResponse(echo.NewResponse(dummyWriter{io.Discard}, e)) + + fakeErr := &fakeError{"fake"} + got, err := DefaultErrorHanderFunc(fakeErr, c) + assert.NoError(t, err) + assert.Equal(t, true, got) + + anyErr := echo.NewHTTPError(http.StatusInternalServerError, "internal") + got, err = DefaultErrorHanderFunc(anyErr, c) + assert.NoError(t, err) + assert.Equal(t, true, got) + + e.ReleaseContext(c) +} diff --git a/internal/project/framework/internals/error/error_middleware.go b/internal/project/framework/internals/error/error_middleware.go deleted file mode 100644 index 14cb36ef..00000000 --- a/internal/project/framework/internals/error/error_middleware.go +++ /dev/null @@ -1,126 +0,0 @@ -/**#bean*/ /*#bean.replace({{ .Copyright }})**/ - -package error - -import ( - /**#bean*/ - "demo/framework/internals/sentry" - /*#bean.replace("{{ .PkgPath }}/framework/internals/sentry")**/ - /**#bean*/ - "demo/framework/internals/validator" - /*#bean.replace("{{ .PkgPath }}/framework/internals/validator")**/ - "net/http" - "strings" - - "github.com/labstack/echo/v4" -) - -// return value: bool is true when HandlerMiddleware match the error,otherwise false -// return value: error will be sent to sentry if not nil - -type ErrorHandlerMiddleware func(err error, c echo.Context) (bool, error) - -func ErrorHandlerChain(middlewares ...ErrorHandlerMiddleware) echo.HTTPErrorHandler { - return func(err error, c echo.Context) { - - if c.Response().Committed { - return - } - - for _, middleware := range middlewares { - catched, e := middleware(err, c) - if e != nil { - sentry.PushData(c, e, nil, true) - } - if catched { - break - } - } - } -} - -func ValidationErrorHanderMiddleware(e error, c echo.Context) (bool, error) { - he, ok := e.(*validator.ValidationError) - if !ok { - return false, nil - } - err := c.JSON(http.StatusUnprocessableEntity, errorResp{ - ErrorCode: API_DATA_VALIDATION_FAILED, - Errors: he.ErrCollection(), - ErrorMsg: nil, - }) - - return ok, err -} - -func APIErrorHanderMiddleware(e error, c echo.Context) (bool, error) { - he, ok := e.(*APIError) - if !ok { - return false, nil - } - - if he.HTTPStatusCode >= 404 { - sentry.PushData(c, he, nil, true) - } - - err := c.JSON(he.HTTPStatusCode, errorResp{ - ErrorCode: he.GlobalErrCode, - Errors: nil, - ErrorMsg: he.Error(), - }) - - return ok, err -} - -func HTTPErrorHanderMiddleware(e error, c echo.Context) (bool, error) { - he, ok := e.(*echo.HTTPError) - if !ok { - return false, nil - } - - // Just in case to capture this unused type error. - var err error - switch he.Code { - case http.StatusNotFound: - err = c.JSON(he.Code, errorResp{ - ErrorCode: RESOURCE_NOT_FOUND, - Errors: nil, - ErrorMsg: he.Message, - }) - case http.StatusMethodNotAllowed: - err = c.JSON(he.Code, errorResp{ - ErrorCode: METHOD_NOT_ALLOWED, - Errors: nil, - ErrorMsg: he.Message, - }) - default: - err = c.JSON(he.Code, errorResp{ - ErrorCode: UNKNOWN_ERROR_CODE, - Errors: nil, - ErrorMsg: he.Message, - }) - } - - return ok, err -} - -func DefaultErrorHanderMiddleware(_ error, c echo.Context) (bool, error) { - // Get Content-Type parameter from request header to identify the request content type. If the request is for - // html then we should display the error in html. - contentType := c.Request().Header.Get("Content-Type") - - if strings.ToLower(contentType) == "text/html" { - err := c.HTML(http.StatusInternalServerError, "Internal server error.") - return true, err - } - - // All other panic errors. - // Sentry already captured the panic and send notification in sentry-recover middleware. - err := c.JSON(http.StatusInternalServerError, errorResp{ - ErrorCode: INTERNAL_SERVER_ERROR, - Errors: nil, - ErrorMsg: nil, // TODO: Put some generic message. - }) - - return true, err -} diff --git a/internal/project/framework/internals/error/error_middleware_test.go b/internal/project/framework/internals/error/error_middleware_test.go deleted file mode 100644 index e1aa0c24..00000000 --- a/internal/project/framework/internals/error/error_middleware_test.go +++ /dev/null @@ -1,153 +0,0 @@ -/**#bean*/ /*#bean.replace({{ .Copyright }})**/ - -package error - -import ( - /**#bean*/ - bvalidator "demo/framework/internals/validator" - /*#bean.replace(bvalidator "{{ .PkgPath }}/framework/internals/validator")**/ - "encoding/json" - "errors" - "github.com/go-playground/validator/v10" - "github.com/labstack/echo/v4" - "github.com/stretchr/testify/assert" - "net/http" - "net/http/httptest" - "testing" -) - -func TestErrorHandlerChain(t *testing.T) { - e := echo.New() - e.HTTPErrorHandler = ErrorHandlerChain( - FakeErrorHandlerMiddleware, - ValidationErrorHanderMiddleware, - APIErrorHanderMiddleware, - HTTPErrorHanderMiddleware, - DefaultErrorHanderMiddleware) - - //assert fake error - fakeErr := newFakeError("fake error message") - checkFakeErr := func(response *httptest.ResponseRecorder, err error) { - assert.Equal(t, fakeErr, err) - assert.Equal(t, http.StatusInternalServerError, response.Code) - var res errorResp - e := json.Unmarshal(response.Body.Bytes(), &res) - assert.Nil(t, e) - assert.Equal(t, ErrorCode("fakeErrorCode"), res.ErrorCode) - assert.Nil(t, res.Errors) - assert.Equal(t, fakeErr.Error(), res.ErrorMsg) - } - run(e, fakeErr, checkFakeErr) - - //assert api error - apiErr := NewAPIError(http.StatusUnauthorized, UNAUTHORIZED_ACCESS, errors.New("UNAUTHORIZED")) - checkAPIErr := func(response *httptest.ResponseRecorder, err error) { - assert.Equal(t, apiErr, err) - assert.Equal(t, http.StatusUnauthorized, response.Code) - var res errorResp - e := json.Unmarshal(response.Body.Bytes(), &res) - assert.Nil(t, e) - assert.Equal(t, UNAUTHORIZED_ACCESS, res.ErrorCode) - assert.Nil(t, res.Errors) - assert.Equal(t, apiErr.Error(), res.ErrorMsg) - } - run(e, apiErr, checkAPIErr) - - //assert validation error - validationErr := &bvalidator.ValidationError{ - Err: validator.ValidationErrors([]validator.FieldError{}), - } - checkValidationErr := func(response *httptest.ResponseRecorder, err error) { - assert.Equal(t, validationErr, err) - assert.Equal(t, http.StatusUnprocessableEntity, response.Code) - var res errorResp - e := json.Unmarshal(response.Body.Bytes(), &res) - assert.Nil(t, e) - assert.Equal(t, API_DATA_VALIDATION_FAILED, res.ErrorCode) - assert.Nil(t, res.ErrorMsg) - } - run(e, validationErr, checkValidationErr) - - //assert http error - httpErr := &echo.HTTPError{ - Code: http.StatusNotFound, - Message: "404 Not Found", - } - checkHttpErr := func(response *httptest.ResponseRecorder, err error) { - assert.Equal(t, httpErr, err) - assert.Equal(t, http.StatusNotFound, response.Code) - var res errorResp - e := json.Unmarshal(response.Body.Bytes(), &res) - assert.Nil(t, e) - assert.Equal(t, UNKNOWN_ERROR_CODE, res.ErrorCode) - assert.Nil(t, res.Errors) - assert.Equal(t, httpErr.Message, res.ErrorMsg) - } - run(e, httpErr, checkHttpErr) - - //assert default error - defaultErr := errors.New("default error") - checkDefaultErr := func(response *httptest.ResponseRecorder, err error) { - assert.Equal(t, defaultErr, err) - assert.Equal(t, http.StatusInternalServerError, response.Code) - var res errorResp - e := json.Unmarshal(response.Body.Bytes(), &res) - assert.Equal(t, http.StatusInternalServerError, response.Code) - assert.Nil(t, e) - assert.Equal(t, INTERNAL_SERVER_ERROR, res.ErrorCode) - assert.Nil(t, res.Errors) - assert.Nil(t, res.ErrorMsg) - } - run(e, defaultErr, checkDefaultErr) -} - -func FakeErrorHandlerMiddleware(e error, c echo.Context) (bool, error) { - he, ok := e.(*fakeError) - if !ok { - return false, nil - } - - err := c.JSON(http.StatusInternalServerError, errorResp{ - ErrorCode: "fakeErrorCode", - Errors: nil, - ErrorMsg: he.Error(), - }) - - return ok, err -} - -func run(e *echo.Echo, - err error, - assertFunc func(response *httptest.ResponseRecorder, err error), -) { - req := httptest.NewRequest(http.MethodGet, "/", nil) - runWithRequest(e, req, err, assertFunc) -} - -func runWithRequest(e *echo.Echo, - req *http.Request, - er error, - assertFunc func(response *httptest.ResponseRecorder, err error), -) { - rec := httptest.NewRecorder() - c := e.NewContext(req, rec) - err := func(c echo.Context) error { - return er - }(c) - e.HTTPErrorHandler(err, c) - assertFunc(rec, err) -} - -type fakeError struct { - Message string -} - -func (f *fakeError) Error() string { - return f.Message -} - -func newFakeError(msg string) error { - return &fakeError{ - Message: msg, - } -} diff --git a/internal/project/framework/internals/helpers/number.go b/internal/project/framework/internals/helpers/number.go index 126ecfd5..0a597040 100644 --- a/internal/project/framework/internals/helpers/number.go +++ b/internal/project/framework/internals/helpers/number.go @@ -1,3 +1,4 @@ +/**#bean*/ /*#bean.replace({{ .Copyright }})**/ package helpers // TODO: Change to generic after go1.18 release diff --git a/internal/project/framework/internals/helpers/runtime.go b/internal/project/framework/internals/helpers/runtime.go index cab43621..7e3497c1 100644 --- a/internal/project/framework/internals/helpers/runtime.go +++ b/internal/project/framework/internals/helpers/runtime.go @@ -1,10 +1,20 @@ +/**#bean*/ /*#bean.replace({{ .Copyright }})**/ package helpers import ( "runtime" + "runtime/debug" ) -// Gets the name of the current running function. +// Returns the current version, only support module mode binaries. +func CurrVersion() string { + if bi, ok := debug.ReadBuildInfo(); ok { + return bi.Main.Version + } + return "" +} + +// Returns the name of the current running function. func CurrFuncName() string { pc, _, _, _ := runtime.Caller(1) return runtime.FuncForPC(pc).Name() diff --git a/internal/project/framework/internals/middleware/access_log.go b/internal/project/framework/internals/middleware/access_log.go index 2136963b..462ee968 100644 --- a/internal/project/framework/internals/middleware/access_log.go +++ b/internal/project/framework/internals/middleware/access_log.go @@ -16,6 +16,9 @@ import ( /**#bean*/ "demo/framework/internals/helpers" /*#bean.replace("{{ .PkgPath }}/framework/internals/helpers")**/ + /**#bean*/ + "demo/packages/options" + /*#bean.replace("{{ .PkgPath }}/packages/options")**/ "github.com/getsentry/sentry-go" "github.com/labstack/echo/v4" @@ -108,9 +111,11 @@ func AccessLoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) (err error) { // Start a sentry span for tracing. - span := sentry.StartSpan(c.Request().Context(), "middleware") - span.Description = helpers.CurrFuncName() - defer span.Finish() + if options.SentryOn { + span := sentry.StartSpan(c.Request().Context(), "middleware") + span.Description = helpers.CurrFuncName() + defer span.Finish() + } // Log the access before processing the request. if err = config.logAccess(c); err != nil { diff --git a/internal/project/framework/internals/middleware/server_header.go b/internal/project/framework/internals/middleware/server_header.go index a84a1a07..a373183b 100644 --- a/internal/project/framework/internals/middleware/server_header.go +++ b/internal/project/framework/internals/middleware/server_header.go @@ -5,6 +5,9 @@ import ( /**#bean*/ "demo/framework/internals/helpers" /*#bean.replace("{{ .PkgPath }}/framework/internals/helpers")**/ + /**#bean*/ + "demo/packages/options" + /*#bean.replace("{{ .PkgPath }}/packages/options")**/ "github.com/getsentry/sentry-go" "github.com/labstack/echo/v4" ) @@ -14,9 +17,11 @@ func ServerHeader(name, version string) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) (err error) { // Start a sentry span for tracing. - span := sentry.StartSpan(c.Request().Context(), "middleware") - span.Description = helpers.CurrFuncName() - defer span.Finish() + if options.SentryOn { + span := sentry.StartSpan(c.Request().Context(), "middleware") + span.Description = helpers.CurrFuncName() + defer span.Finish() + } c.Response().Header().Set(echo.HeaderServer, name+"/"+version) return next(c) } diff --git a/internal/project/framework/internals/middleware/timeout.go b/internal/project/framework/internals/middleware/timeout.go index 00cce49c..1f5a32a1 100644 --- a/internal/project/framework/internals/middleware/timeout.go +++ b/internal/project/framework/internals/middleware/timeout.go @@ -8,6 +8,9 @@ import ( /**#bean*/ "demo/framework/internals/helpers" /*#bean.replace("{{ .PkgPath }}/framework/internals/helpers")**/ + /**#bean*/ + "demo/packages/options" + /*#bean.replace("{{ .PkgPath }}/packages/options")**/ "github.com/getsentry/sentry-go" "github.com/labstack/echo/v4" @@ -18,9 +21,11 @@ func RequestTimeout(timeout time.Duration) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { // Start a sentry span for tracing. - span := sentry.StartSpan(c.Request().Context(), "middleware") - span.Description = helpers.CurrFuncName() - defer span.Finish() + if options.SentryOn { + span := sentry.StartSpan(c.Request().Context(), "middleware") + span.Description = helpers.CurrFuncName() + defer span.Finish() + } timeoutCtx, cancel := context.WithTimeout(c.Request().Context(), timeout) c.SetRequest(c.Request().WithContext(timeoutCtx)) defer cancel() diff --git a/internal/project/framework/internals/sentry/sentry.go b/internal/project/framework/internals/sentry/sentry.go deleted file mode 100644 index 4e80ef1a..00000000 --- a/internal/project/framework/internals/sentry/sentry.go +++ /dev/null @@ -1,147 +0,0 @@ -/**#bean*/ /*#bean.replace({{ .Copyright }})**/ -package sentry - -import ( - "errors" - "reflect" - - "github.com/getsentry/sentry-go" - sentryecho "github.com/getsentry/sentry-go/echo" - "github.com/labstack/echo/v4" - "github.com/spf13/viper" -) - -const maxErrorDepth = 10 - -// PushData posting error/info in to sentry or console asynchronously. -func PushData(c echo.Context, data interface{}, event *sentry.Event, isAsync bool) { - - isSentry := viper.GetBool("sentry.isSentry") - sentryDSN := viper.GetString("sentry.dsn") - - // Check sentry is active or not and if active then priorartize sentry over console.log or stdout. - if !isSentry || sentryDSN == "" { - - if data, ok := data.(error); ok { - c.Echo().Logger.Error(data) - } else { - c.Echo().Logger.Info(data) - } - - return - } - - // IMPORTANT: Clone the current sentry hub from the echo context before it's gone. - hub := sentryecho.GetHubFromContext(c) - if hub != nil { - hub = hub.Clone() - } - - if hub == nil { - hub = sentry.CurrentHub().Clone() - } - - if isAsync { - go sendEventToSentry(hub, event, data) - } else { - sendEventToSentry(hub, event, data) - } -} - -func sendEventToSentry(hub *sentry.Hub, event *sentry.Event, data interface{}) { - - client, scope := hub.Client(), hub.Scope() - - if exception, ok := data.(error); ok { - - if event == nil { - event = EventFromException(exception) - } - client.CaptureEvent(event, &sentry.EventHint{RecoveredException: exception}, scope) - - } else { - - if event == nil { - event = EventFromRawData(data, sentry.LevelError) - } - client.CaptureEvent(event, &sentry.EventHint{Data: data}, scope) - } -} - -// EventFromException creates an sentry event from error. -func EventFromException(exception error) *sentry.Event { - - err := exception - if err == nil { - err = errors.New("called with nil error") - } - - event := sentry.NewEvent() - event.Level = sentry.LevelError - - for i := 0; i < maxErrorDepth && err != nil; i++ { - event.Exception = append(event.Exception, sentry.Exception{ - Value: err.Error(), - Type: reflect.TypeOf(err).String(), - Stacktrace: sentry.ExtractStacktrace(err), - }) - switch previous := err.(type) { - case interface{ Unwrap() error }: - err = previous.Unwrap() - case interface{ Cause() error }: - err = previous.Cause() - default: - err = nil - } - } - - // Add a trace of the current stack to the most recent error in a chain if - // it doesn't have a stack trace yet. - // We only add to the most recent error to avoid duplication and because the - // current stack is most likely unrelated to errors deeper in the chain. - if event.Exception[0].Stacktrace == nil { - event.Exception[0].Stacktrace = sentry.NewStacktrace() - } - - // event.Exception should be sorted such that the most recent error is last. - reverse(event.Exception) - - return event -} - -// EventFromRawData creates an sentry event. -func EventFromRawData(data interface{}, level sentry.Level) *sentry.Event { - - if data == nil { - err := errors.New("called with nil data") - return EventFromException(err) - } - - event := sentry.NewEvent() - event.Level = level - event.Extra["raw"] = data - - event.Threads = []sentry.Thread{ - { - Stacktrace: NewStacktrace(), - Crashed: false, - Current: true, - }, - } - - return event -} - -// Do not change: function copyied from sentry library -// reverse reverses the slice a in place. -func reverse(a []sentry.Exception) { - for i := len(a)/2 - 1; i >= 0; i-- { - opp := len(a) - 1 - i - a[i], a[opp] = a[opp], a[i] - } -} - -// NewStacktrace returns new sentry stacktrace -func NewStacktrace() *sentry.Stacktrace { - return sentry.NewStacktrace() -} diff --git a/internal/project/framework/kernel/kernel.go b/internal/project/framework/kernel/kernel.go index 95a0a571..a996595e 100644 --- a/internal/project/framework/kernel/kernel.go +++ b/internal/project/framework/kernel/kernel.go @@ -69,7 +69,6 @@ func NewEcho() *echo.Echo { } } e.Logger.SetLevel(log.DEBUG) - e.Logger.Info("ENVIRONMENT: ", viper.GetString("environment")) // IMPORTANT: Configure access log and body dumper. (can be turn off) if viper.GetBool("accessLog.on") { @@ -88,9 +87,10 @@ func NewEcho() *echo.Echo { // IMPORTANT: Capturing error and send to sentry if needed. // Sentry `panic` error handler and APM initialization if activated from `env.json` - if viper.GetBool("sentry.on") { + options.SentryOn = viper.GetBool("sentry.on") + if options.SentryOn { // To initialize Sentry's handler, we need to initialize sentry first. - if err := sentry.Init(options.DefaultSentryClientOptions); err != nil { + if err := sentry.Init(options.DefaultSentryClientOptions()); err != nil { e.Logger.Fatal("Sentry initialization failed: ", err, ". Server 🚀 crash landed. Exiting...") } diff --git a/internal/project/handlers/example.go b/internal/project/handlers/example.go index 61b9fc8a..b896b156 100644 --- a/internal/project/handlers/example.go +++ b/internal/project/handlers/example.go @@ -2,9 +2,6 @@ package handlers import ( - "net/http" - "time" - /**#bean*/ "demo/framework/internals/async" /*#bean.replace("{{ .PkgPath }}/framework/internals/async")**/ @@ -15,6 +12,9 @@ import ( "demo/services" /*#bean.replace("{{ .PkgPath }}/services")**/ + "net/http" + "time" + "github.com/getsentry/sentry-go" "github.com/labstack/echo/v4" ) @@ -42,9 +42,15 @@ func (handler *exampleHandler) JSONIndex(c echo.Context) error { return err } - // IMPORTANT: This is how you can execute some asynchronous code instead `go routine`. + // IMPORTANT: Panic inside a goroutine will crash the whole application. + // Example: How to execute some asynchronous code safely instead of plain goroutine. async.Execute(func(c echo.Context) { c.Logger().Debug(dbName) + // IMPORTANT: Using sentry directly in goroutine may cause data race! + // Need to create a new hub by cloning the existing one. + // Example: How to use sentry safely in goroutine. + // localHub := sentry.CurrentHub().Clone() + // localHub.CaptureMessage(dbName) }, c.Echo()) return c.JSON(http.StatusOK, map[string]string{ @@ -53,7 +59,6 @@ func (handler *exampleHandler) JSONIndex(c echo.Context) error { } func (handler *exampleHandler) HTMLIndex(c echo.Context) error { - return c.Render(http.StatusOK, "index", echo.Map{ "title": "Index title!", "add": func(a int, b int) int { diff --git a/internal/project/packages/options/sentry.go b/internal/project/packages/options/sentry.go index 4bc6c7ff..7c39cc9a 100644 --- a/internal/project/packages/options/sentry.go +++ b/internal/project/packages/options/sentry.go @@ -1,6 +1,7 @@ package options import ( + /**#bean*/ "demo/framework/internals/error" /*#bean.replace("{{ .PkgPath }}/framework/internals/error")**/ @@ -16,20 +17,27 @@ import ( "github.com/spf13/viper" ) -var DefaultSentryClientOptions = sentry.ClientOptions{ - Release: helpers.CurrVersion(), - Dsn: viper.GetString("sentry.dsn"), - BeforeSend: beforeSend, // Custom beforeSend function - BeforeBreadcrumb: beforeBreadcrumb, // Custom beforeBreadcrumb function - AttachStacktrace: viper.GetBool("sentry.attachStacktrace"), - TracesSampleRate: helpers.FloatInRange(viper.GetFloat64("sentry.tracesSampleRate"), 0.0, 1.0), +var SentryOn bool // Global variable + +func DefaultSentryClientOptions() sentry.ClientOptions { + return sentry.ClientOptions{ + Debug: viper.GetBool("sentry.debug"), + Dsn: viper.GetString("sentry.dsn"), + Environment: viper.GetString("environment"), + BeforeSend: beforeSend, // Custom beforeSend function + BeforeBreadcrumb: beforeBreadcrumb, // Custom beforeBreadcrumb function + AttachStacktrace: true, + TracesSampleRate: helpers.FloatInRange(viper.GetFloat64("sentry.tracesSampleRate"), 0.0, 1.0), + } } +// This will set the scope globally, if you want to set the scope per event, +// please check `sentry.WithScope()`. func ConfigureScope(scope *sentry.Scope) { // Set your parent scope here, for example: // scope.SetTag("my-tag", "my value") // scope.SetUser(sentry.User{ - // ID: "42", + // ID: "42", // Email: "john.doe@example.com", // }) // scope.SetContext("character", map[string]interface{}{ @@ -37,19 +45,25 @@ func ConfigureScope(scope *sentry.Scope) { // "age": 19, // "attack_type": "melee", // }) + // scope.AddBreadcrumb(&sentry.Breadcrumb{ + // Type: "debug", + // Category: "scope", + // Message: "testing scope.AddBreadcrumb()", + // Level: sentry.LevelInfo, + // }, 10) } func beforeSend(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { - // Add any aditional data to the event in here. + // You can change or add aditional data to the event in here. + // Example: switch err := hint.OriginalException.(type) { case *validator.ValidationError: - event.Contexts["example section"] = map[string]interface{}{ - "example key": "example value", - } return event case *error.APIError: - if err.HTTPStatusCode >= 404 { - // sentry.PushData(c, he, nil, true) + event.Contexts["Error"] = map[string]interface{}{ + "HTTPStatusCode": err.HTTPStatusCode, + "GlobalErrCode": err.GlobalErrCode, + "Message": err.Error(), } return event case *echo.HTTPError: @@ -60,6 +74,10 @@ func beforeSend(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { } func beforeBreadcrumb(breadcrumb *sentry.Breadcrumb, hint *sentry.BreadcrumbHint) *sentry.Breadcrumb { - + // You can customize breadcrumbs through this beforeBreadcrumb function. + // Example: discard the breadcrumb by return nil. + // if breadcrumb.Category == "example" { + // return nil + // } return breadcrumb } diff --git a/internal/project/routers/route.go b/internal/project/routers/route.go index 688a75bd..a7a92266 100644 --- a/internal/project/routers/route.go +++ b/internal/project/routers/route.go @@ -2,8 +2,6 @@ package routers import ( - "net/http" - /**#bean*/ "demo/framework/bean" /*#bean.replace("{{ .PkgPath }}/framework/bean")**/ @@ -17,6 +15,8 @@ import ( "demo/services" /*#bean.replace("{{ .PkgPath }}/services")**/ + "net/http" + "github.com/labstack/echo/v4" )