package http import ( "context" "log/slog" "net/http" "os" "time" "github.com/getsentry/sentry-go" sentryecho "github.com/getsentry/sentry-go/echo" "github.com/go-playground/validator" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" echoSwagger "github.com/swaggo/echo-swagger" middlewareOtel "go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho" _ "github.com/chaitin/panda-wiki/docs" "github.com/chaitin/panda-wiki/config" "github.com/chaitin/panda-wiki/log" PWMiddleware "github.com/chaitin/panda-wiki/middleware" ) type HTTPServer struct { Echo *echo.Echo } type echoValidator struct { validator *validator.Validate } func (v *echoValidator) Validate(i any) error { if err := v.validator.Struct(i); err != nil { return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } return nil } func NewEcho( logger *log.Logger, config *config.Config, pwMiddleware *PWMiddleware.ReadOnlyMiddleware, sessionMiddleware *PWMiddleware.SessionMiddleware, ) *echo.Echo { // Initialize Sentry if enabled if config.Sentry.Enabled && config.Sentry.DSN != "" { err := sentry.Init(sentry.ClientOptions{ Dsn: config.Sentry.DSN, }) if err != nil { logger.Error("Failed to initialize Sentry", log.Error(err)) } else { logger.Info("Sentry initialized successfully") // Flush buffered events on the default client before the program terminates. defer sentry.Flush(2 * time.Second) } } e := echo.New() e.HideBanner = true e.HidePort = true e.Binder = &MyBinder{} if os.Getenv("ENV") == "local" { e.Debug = true e.GET("/swagger/*", echoSwagger.WrapHandler) } // register validator e.Validator = &echoValidator{validator: validator.New()} // Add Sentry middleware if enabled if config.Sentry.Enabled && config.Sentry.DSN != "" { e.Use(sentryecho.New(sentryecho.Options{ Repanic: true, Timeout: 5 * time.Second, })) sentry.CaptureMessage("It works!") } if config.GetBool("apm.enabled") { e.Use(middlewareOtel.Middleware(config.GetString("apm.service_name"))) } e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ LogStatus: true, LogURI: true, LogLatency: true, LogError: true, LogMethod: true, LogRemoteIP: true, HandleError: true, // forwards error to the global error handler, so it can decide appropriate status code LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error { // Get the real IP address realIP := c.RealIP() method := c.Request().Method uri := v.URI status := v.Status latency := v.Latency.Milliseconds() if v.Error == nil { logger.LogAttrs(context.Background(), slog.LevelInfo, "REQUEST", slog.String("remote_ip", realIP), slog.String("method", method), slog.String("uri", uri), slog.Int("status", status), slog.Int("latency", int(latency)), ) } else { logger.LogAttrs(context.Background(), slog.LevelError, "REQUEST_ERROR", slog.String("remote_ip", realIP), slog.String("method", method), slog.String("uri", uri), slog.Int("status", status), slog.Int("latency", int(latency)), slog.String("err", v.Error.Error()), ) } return nil }, })) e.Use(pwMiddleware.ReadOnly) e.Use(sessionMiddleware.Session()) return e } type MyBinder struct { echo.DefaultBinder } func (b *MyBinder) Bind(i interface{}, c echo.Context) (err error) { if err := b.BindPathParams(c, i); err != nil { return err } method := c.Request().Method if method == http.MethodGet || method == http.MethodDelete || method == http.MethodHead { if err = b.BindQueryParams(c, i); err != nil { return err } return nil } return b.BindBody(c, i) }