Implement error pages, improve request logging
This commit is contained in:
parent
0cf51b8ff1
commit
e9c34a2337
19 changed files with 462 additions and 167 deletions
137
common/handlers/errors.go
Normal file
137
common/handlers/errors.go
Normal file
|
@ -0,0 +1,137 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
commonServices "git.cacert.org/oidc_login/common/services"
|
||||
)
|
||||
|
||||
type errorKey int
|
||||
|
||||
const (
|
||||
errorBucketKey errorKey = iota
|
||||
)
|
||||
|
||||
type ErrorDetails struct {
|
||||
ErrorMessage string
|
||||
ErrorDetails []string
|
||||
ErrorCode string
|
||||
Error error
|
||||
}
|
||||
|
||||
type ErrorBucket struct {
|
||||
errorDetails *ErrorDetails
|
||||
templates *template.Template
|
||||
logger *log.Logger
|
||||
bundle *i18n.Bundle
|
||||
messageCatalog *commonServices.MessageCatalog
|
||||
}
|
||||
|
||||
func (b *ErrorBucket) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if b.errorDetails != nil {
|
||||
accept := r.Header.Get("Accept-Language")
|
||||
localizer := i18n.NewLocalizer(b.bundle, accept)
|
||||
err := b.templates.Lookup("base").Execute(w, map[string]interface{}{
|
||||
"Title": b.messageCatalog.LookupMessage(
|
||||
"ErrorTitle",
|
||||
nil,
|
||||
localizer,
|
||||
),
|
||||
"details": b.errorDetails,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("error rendering error template: %v", err)
|
||||
http.Error(
|
||||
w,
|
||||
http.StatusText(http.StatusInternalServerError),
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GetErrorBucket(r *http.Request) *ErrorBucket {
|
||||
return r.Context().Value(errorBucketKey).(*ErrorBucket)
|
||||
}
|
||||
|
||||
// call this from your application's handler
|
||||
func (b *ErrorBucket) AddError(details *ErrorDetails) {
|
||||
b.errorDetails = details
|
||||
}
|
||||
|
||||
type errorResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
ctx context.Context
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func (w *errorResponseWriter) WriteHeader(code int) {
|
||||
w.statusCode = code
|
||||
if code >= 400 {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
errorBucket := w.ctx.Value(errorBucketKey).(*ErrorBucket)
|
||||
if errorBucket != nil && errorBucket.errorDetails == nil {
|
||||
errorBucket.AddError(&ErrorDetails{
|
||||
ErrorMessage: http.StatusText(code),
|
||||
})
|
||||
}
|
||||
}
|
||||
w.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
func (w *errorResponseWriter) Write(content []byte) (int, error) {
|
||||
if w.statusCode > 400 {
|
||||
errorBucket := w.ctx.Value(errorBucketKey).(*ErrorBucket)
|
||||
if errorBucket != nil {
|
||||
if errorBucket.errorDetails.ErrorDetails == nil {
|
||||
errorBucket.errorDetails.ErrorDetails = make([]string, 0)
|
||||
}
|
||||
errorBucket.errorDetails.ErrorDetails = append(
|
||||
errorBucket.errorDetails.ErrorDetails, string(content),
|
||||
)
|
||||
return len(content), nil
|
||||
}
|
||||
}
|
||||
return w.ResponseWriter.Write(content)
|
||||
}
|
||||
|
||||
func ErrorHandling(
|
||||
handlerContext context.Context,
|
||||
logger *log.Logger,
|
||||
templateBaseDir string,
|
||||
) (func(http.Handler) http.Handler, error) {
|
||||
errorTemplates, err := template.ParseFiles(
|
||||
path.Join(templateBaseDir, "base.gohtml"),
|
||||
path.Join(templateBaseDir, "errors.gohtml"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
errorBucket := &ErrorBucket{
|
||||
templates: errorTemplates,
|
||||
logger: logger,
|
||||
bundle: commonServices.GetI18nBundle(handlerContext),
|
||||
messageCatalog: commonServices.GetMessageCatalog(handlerContext),
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), errorBucketKey, errorBucket)
|
||||
interCeptingResponseWriter := &errorResponseWriter{
|
||||
w,
|
||||
ctx,
|
||||
http.StatusOK,
|
||||
}
|
||||
next.ServeHTTP(
|
||||
interCeptingResponseWriter,
|
||||
r.WithContext(ctx),
|
||||
)
|
||||
errorBucket.serveHTTP(w, r)
|
||||
})
|
||||
}, nil
|
||||
}
|
|
@ -14,17 +14,44 @@ const (
|
|||
requestIdKey key = iota
|
||||
)
|
||||
|
||||
type statusCodeInterceptor struct {
|
||||
http.ResponseWriter
|
||||
code int
|
||||
count int
|
||||
}
|
||||
|
||||
func (sci *statusCodeInterceptor) WriteHeader(code int) {
|
||||
sci.code = code
|
||||
sci.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
func (sci *statusCodeInterceptor) Write(content []byte) (int, error) {
|
||||
count, err := sci.ResponseWriter.Write(content)
|
||||
sci.count += count
|
||||
return count, err
|
||||
}
|
||||
|
||||
func Logging(logger *log.Logger) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
interceptor := &statusCodeInterceptor{w, http.StatusOK, 0}
|
||||
defer func() {
|
||||
requestId, ok := r.Context().Value(requestIdKey).(string)
|
||||
if !ok {
|
||||
requestId = "unknown"
|
||||
}
|
||||
logger.Infoln(requestId, r.Method, r.URL.Path, r.RemoteAddr, r.UserAgent())
|
||||
logger.Infof(
|
||||
"%s %s \"%s %s\" %d %d \"%s\"",
|
||||
requestId,
|
||||
r.RemoteAddr,
|
||||
r.Method,
|
||||
r.URL.Path,
|
||||
interceptor.code,
|
||||
interceptor.count,
|
||||
r.UserAgent(),
|
||||
)
|
||||
}()
|
||||
next.ServeHTTP(w, r)
|
||||
next.ServeHTTP(interceptor, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
45
common/handlers/startup.go
Normal file
45
common/handlers/startup.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/knadh/koanf"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func StartApplication(logger *logrus.Logger, ctx context.Context, server *http.Server, config *koanf.Koanf) {
|
||||
done := make(chan bool)
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, os.Interrupt)
|
||||
|
||||
go func() {
|
||||
<-quit
|
||||
logger.Infoln("Server is shutting down...")
|
||||
atomic.StoreInt32(&Healthy, 0)
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
server.SetKeepAlivesEnabled(false)
|
||||
if err := server.Shutdown(ctx); err != nil {
|
||||
logger.Fatalf("Could not gracefully shutdown the server: %v\n", err)
|
||||
}
|
||||
close(done)
|
||||
}()
|
||||
|
||||
logger.Infof("Server is ready to handle requests at https://%s/", server.Addr)
|
||||
atomic.StoreInt32(&Healthy, 1)
|
||||
if err := server.ListenAndServeTLS(
|
||||
config.String("server.certificate"), config.String("server.key"),
|
||||
); err != nil && err != http.ErrServerClosed {
|
||||
logger.Fatalf("Could not listen on %s: %v\n", server.Addr, err)
|
||||
}
|
||||
|
||||
<-done
|
||||
logger.Infoln("Server stopped")
|
||||
}
|
Reference in a new issue