~netlandish/gobwebs

cf84d97e47b2ff4f9022dc164f61b04f4083438d — Peter Sanchez 2 years ago 436d844
Add ability to send email to admins on errors.
3 files changed, 121 insertions(+), 23 deletions(-)

M config/config.go
M go.mod
M server/server.go
M config/config.go => config/config.go +11 -0
@@ 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
	}

M go.mod => go.mod +1 -1
@@ 14,6 14,7 @@ require (
	github.com/minio/minio-go/v7 v7.0.22
	github.com/spazzymoto/echo-scs-session v1.0.0
	github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec
	golang.org/x/text v0.3.7
	petersanchez.com/carrier v0.1.0
)



@@ 55,7 56,6 @@ require (
	golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b // indirect
	golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect
	golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect
	golang.org/x/text v0.3.7 // indirect
	golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect
	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
	google.golang.org/protobuf v1.23.0 // indirect

M server/server.go => server/server.go +109 -22
@@ 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()