@@ 3,6 3,7 @@ package config
import (
"fmt"
"strconv"
+ "strings"
"github.com/vaughan0/go-ini"
)
@@ 23,6 24,7 @@ type Config struct {
StorageSvc string
AdminEmail string
+ EmailAdminErrors bool
DefaultFromEmail string
DefaultLang string
@@ 53,6 55,7 @@ func LoadConfig(fname string) (*Config, error) {
EmailSvc: "console",
StorageSvc: "fs",
AdminEmail: "admin@localhost",
+ EmailAdminErrors: true,
DefaultFromEmail: "gobwebs@localhost",
DefaultLang: "en",
Scheme: "http",
@@ 110,6 113,14 @@ func LoadConfig(fname string) (*Config, error) {
if val, ok := file.Get("gobwebs", "default-from-email"); ok {
conf.DefaultFromEmail = val
}
+ if val, ok := file.Get("gobwebs", "email-admin-errors"); ok {
+ val = strings.ToLower(val)
+ if val == "true" || val == "false" {
+ conf.EmailAdminErrors = val == "true"
+ } else {
+ return nil, fmt.Errorf("gobwebs:email-admin-errors invalid value")
+ }
+ }
if val, ok := file.Get("gobwebs", "default-language"); ok {
conf.DefaultLang = val
}
@@ 9,6 9,7 @@ import (
"net/http"
"os"
"os/signal"
+ "runtime"
"syscall"
"time"
@@ 23,6 24,7 @@ import (
"hg.code.netlandish.com/~netlandish/gobwebs/internal/localizer"
"hg.code.netlandish.com/~netlandish/gobwebs/storage"
"hg.code.netlandish.com/~netlandish/gobwebs/validate"
+ "petersanchez.com/carrier"
// Postgres
_ "github.com/lib/pq"
@@ 82,16 84,105 @@ func (c *Context) Render(code int, name string, data interface{}) (err error) {
return c.HTMLBlob(code, buf.Bytes())
}
+// Error is needed because otherwise it will send an instance of
+// *echo.context, and we want the user info, etc. from the gobwebs
+// Context struct
+func (c *Context) Error(err error) {
+ c.Server.e.HTTPErrorHandler(err, c)
+}
+
// HTTPErrorHandler custom error handler for entire app
func (s *Server) HTTPErrorHandler(err error, c echo.Context) {
- // Send to Sentry or some other logger
- // Print Debug info
- if s.Config.Debug {
- s.e.Logger.Printf("%v - %v - %v\n", c.Path(), c.QueryParams(), err.Error())
+ // TODO: Send to Sentry or some other logger
+
+ he, ok := err.(*echo.HTTPError)
+ if ok {
+ if he.Internal != nil {
+ if herr, ok := he.Internal.(*echo.HTTPError); ok {
+ he = herr
+ }
+ }
+ } else {
+ he = &echo.HTTPError{
+ Code: http.StatusInternalServerError,
+ Message: http.StatusText(http.StatusInternalServerError),
+ }
+ }
+
+ gctx := c.(*Context)
+ errmap := gobwebs.Map{
+ "user": gctx.User,
+ }
+
+ if he.Code > 499 {
+ stack := make([]byte, 32768) // 32 KiB
+ i := runtime.Stack(stack, false)
+ stack = stack[:i]
+
+ // Print Debug info
+ if s.Config.Debug {
+ msg := fmt.Sprintf("[ERROR] %s\n%v\n%s\n", c.Path(), c.QueryParams(), stack)
+ s.e.Logger.Printf(msg)
+ fmt.Fprintf(os.Stderr, msg)
+ }
+
+ if !s.Config.Debug && s.Config.EmailAdminErrors && s.Email != nil {
+ // Send email to admin email address
+ var userStr string
+ if gctx.User.IsAuthenticated() {
+ userStr = fmt.Sprintf("ID: %d, Email: %s",
+ int(gctx.User.GetID()),
+ gctx.User.GetEmail(),
+ )
+ } else {
+ userStr = "Anonymous User"
+ }
+ emsg := fmt.Sprintf(`Error occured processing the following request:
+
+%v
+
+At the following path:
+
+%s
+
+With these variables:
+
+%v
+
+By the user: %s
+
+The following stack trace was produced:
+
+%s`, err, c.Path(), c.QueryParams(), userStr, stack)
+
+ msg := carrier.NewMessage().
+ SetTo(s.Config.AdminEmail).
+ SetFrom(s.Config.DefaultFromEmail)
+ msg.SetSubject(fmt.Sprintf("[gobwebs]: Internal Server Error: %s", c.Path()))
+ msg.SetBody(emsg)
+ if rerr := s.Email.SendMail(msg); rerr != nil {
+ s.e.Logger.Printf("Unable to send admin error email: %v", rerr)
+ }
+ }
+ }
+
+ tmpl := "500.html"
+ if he.Code == 404 {
+ tmpl = "404.html"
+ }
+
+ // Try to render custom template. On error use default handler
+ if rerr := gctx.Render(he.Code, tmpl, errmap); rerr != nil {
+ // Call the default handler to return the HTTP response
+ s.e.DefaultHTTPErrorHandler(err, c)
}
+}
- // Call the default handler to return the HTTP response
- s.e.DefaultHTTPErrorHandler(err, c)
+// LogErrorFunc will handle logging the stack trace for an error
+func (s *Server) LogErrorFunc(c echo.Context, err error, stack []byte) error {
+ msg := fmt.Sprintf("[PANIC RECOVER] %s\n%v\n%s\n", c.Path(), c.QueryParams(), stack)
+ s.e.Logger.Printf(msg)
+ return err
}
// GetSessionManager returns the configured session
@@ 194,10 285,19 @@ func (s *Server) WithDefaultMiddleware() *Server {
s.e.Pre(middleware.RemoveTrailingSlashWithConfig(middleware.TrailingSlashConfig{
RedirectCode: http.StatusMovedPermanently,
}))
+ s.e.Use(session.LoadAndSave(s.Session)) // Must be first
+ // Set custom context
+ s.e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(c echo.Context) error {
+ ctx := &Context{Context: c, Server: s}
+ return next(ctx)
+ }
+ })
s.e.Use(
- session.LoadAndSave(s.Session), // Must be first
- //middleware.RecoverWithConfig(middleware.RecoverConfig{LogErrorFunc: s.LogErrorFunc}),
- middleware.Recover(),
+ middleware.RecoverWithConfig(middleware.RecoverConfig{
+ StackSize: 32768, // 32KiB
+ LogErrorFunc: s.LogErrorFunc,
+ }),
middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: fmt.Sprintf(
"${time_rfc3339} method=${method}, uri=${uri}, status=${status} remote_ip=${remote_ip} app=\"%s\"\n",
@@ 210,13 310,6 @@ func (s *Server) WithDefaultMiddleware() *Server {
TokenLookup: "form:csrf",
}),
)
- // Set custom context
- s.e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
- return func(c echo.Context) error {
- ctx := &Context{Context: c, Server: s}
- return next(ctx)
- }
- })
return s
}
@@ 226,12 319,6 @@ func (s *Server) WithMiddleware(middlewares ...echo.MiddlewareFunc) *Server {
return s
}
-// LogErrorFunc ...
-func (s *Server) LogErrorFunc(c echo.Context, err error, stack []byte) error {
- fmt.Println(stack)
- return nil
-}
-
// WithQueues add dowork task queues for this server to manage
func (s *Server) WithQueues(queues ...*work.Queue) *Server {
ctx := context.Background()