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"
)