init
This commit is contained in:
61
core/appserver/brotli.go
Normal file
61
core/appserver/brotli.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package appserver
|
||||
|
||||
import (
|
||||
"github.com/andybalholm/brotli"
|
||||
"net/http"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type brotliResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
|
||||
w *brotli.Writer
|
||||
statusCode int
|
||||
headerWritten bool
|
||||
}
|
||||
|
||||
var (
|
||||
poolbr = sync.Pool{
|
||||
New: func() interface{} {
|
||||
w := brotli.NewWriterLevel(nil, brotli.BestSpeed)
|
||||
return &brotliResponseWriter{
|
||||
w: w,
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func (br *brotliResponseWriter) WriteHeader(statusCode int) {
|
||||
br.statusCode = statusCode
|
||||
br.headerWritten = true
|
||||
|
||||
if br.statusCode != http.StatusNotModified && br.statusCode != http.StatusNoContent {
|
||||
br.ResponseWriter.Header().Del("Content-Length")
|
||||
br.ResponseWriter.Header().Set("Content-Encoding", "br")
|
||||
}
|
||||
|
||||
br.ResponseWriter.WriteHeader(statusCode)
|
||||
}
|
||||
func (br *brotliResponseWriter) Write(b []byte) (int, error) {
|
||||
if _, ok := br.Header()["Content-Type"]; !ok {
|
||||
// If no content type, apply sniffing algorithm to un-gzipped body.
|
||||
br.ResponseWriter.Header().Set("Content-Type", http.DetectContentType(b))
|
||||
}
|
||||
|
||||
if !br.headerWritten {
|
||||
// This is exactly what Go would also do if it hasn't been written yet.
|
||||
br.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
return br.w.Write(b)
|
||||
}
|
||||
|
||||
func (br *brotliResponseWriter) Flush() {
|
||||
if br.w != nil {
|
||||
br.w.Flush()
|
||||
}
|
||||
|
||||
if fw, ok := br.ResponseWriter.(http.Flusher); ok {
|
||||
fw.Flush()
|
||||
}
|
||||
}
|
||||
62
core/appserver/gzip.go
Normal file
62
core/appserver/gzip.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package appserver
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"net/http"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type gzipResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
|
||||
w *gzip.Writer
|
||||
statusCode int
|
||||
headerWritten bool
|
||||
}
|
||||
|
||||
var (
|
||||
pool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
w, _ := gzip.NewWriterLevel(nil, gzip.BestSpeed)
|
||||
return &gzipResponseWriter{
|
||||
w: w,
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func (gzr *gzipResponseWriter) WriteHeader(statusCode int) {
|
||||
gzr.statusCode = statusCode
|
||||
gzr.headerWritten = true
|
||||
|
||||
if gzr.statusCode != http.StatusNotModified && gzr.statusCode != http.StatusNoContent {
|
||||
gzr.ResponseWriter.Header().Del("Content-Length")
|
||||
gzr.ResponseWriter.Header().Set("Content-Encoding", "gzip")
|
||||
}
|
||||
|
||||
gzr.ResponseWriter.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
func (gzr *gzipResponseWriter) Write(b []byte) (int, error) {
|
||||
if _, ok := gzr.Header()["Content-Type"]; !ok {
|
||||
// If no content type, apply sniffing algorithm to un-gzipped body.
|
||||
gzr.ResponseWriter.Header().Set("Content-Type", http.DetectContentType(b))
|
||||
}
|
||||
|
||||
if !gzr.headerWritten {
|
||||
// This is exactly what Go would also do if it hasn't been written yet.
|
||||
gzr.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
return gzr.w.Write(b)
|
||||
}
|
||||
|
||||
func (gzr *gzipResponseWriter) Flush() {
|
||||
if gzr.w != nil {
|
||||
gzr.w.Flush()
|
||||
}
|
||||
|
||||
if fw, ok := gzr.ResponseWriter.(http.Flusher); ok {
|
||||
fw.Flush()
|
||||
}
|
||||
}
|
||||
108
core/appserver/httpServer.go
Normal file
108
core/appserver/httpServer.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package appserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/gorilla/context"
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
"gitlab.com/arkadooti.sarkar/go-boilerplate/core/appcontext"
|
||||
"gitlab.com/arkadooti.sarkar/go-boilerplate/core/log"
|
||||
"go.elastic.co/apm/module/apmgorilla"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
)
|
||||
|
||||
var (
|
||||
AppName string
|
||||
)
|
||||
|
||||
func (s *server) Start(ctx appcontext.AppContext) {
|
||||
allowedOrigins := handlers.AllowedOrigins([]string{"*"}) // Allowing all origin as of now
|
||||
|
||||
allowedHeaders := handlers.AllowedHeaders([]string{
|
||||
"Accept",
|
||||
"Content-Type",
|
||||
"contentType",
|
||||
"Content-Length",
|
||||
"Accept-Encoding",
|
||||
"Client-Security-Token",
|
||||
"X-CSRF-Token",
|
||||
"X-Auth-Token",
|
||||
"processData",
|
||||
"Authorization",
|
||||
"Access-Control-Request-Headers",
|
||||
"Access-Control-Request-Method",
|
||||
"Connection",
|
||||
"Host",
|
||||
"Origin",
|
||||
"User-Agent",
|
||||
"Referer",
|
||||
"Cache-Control",
|
||||
"X-header",
|
||||
"X-Requested-With",
|
||||
"timezone",
|
||||
"locale",
|
||||
"gzip-compress",
|
||||
"task",
|
||||
"access_token",
|
||||
"application",
|
||||
})
|
||||
|
||||
allowedMethods := handlers.AllowedMethods([]string{
|
||||
"POST",
|
||||
"GET",
|
||||
"DELETE",
|
||||
"PUT",
|
||||
"PATCH",
|
||||
"OPTIONS"})
|
||||
|
||||
allowCredential := handlers.AllowCredentials()
|
||||
|
||||
serverHandler := handlers.CORS(
|
||||
allowedHeaders,
|
||||
allowedMethods,
|
||||
allowedOrigins,
|
||||
allowCredential)(
|
||||
context.ClearHandler(
|
||||
s.newRouter(s.subRoute),
|
||||
),
|
||||
)
|
||||
log.GenericInfo(ctx, "Starting Server",
|
||||
log.FieldsMap{
|
||||
"Port": s.port,
|
||||
"SubRoute": s.subRoute,
|
||||
"App": AppName,
|
||||
})
|
||||
|
||||
err := http.ListenAndServe(":"+s.port, serverHandler)
|
||||
if err != nil {
|
||||
log.GenericError(ctx, errors.New("failed to start appserver"),
|
||||
log.FieldsMap{
|
||||
"Port": s.port,
|
||||
"SubRoute": s.subRoute,
|
||||
"App": AppName,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Handles all incoming request who matches registered routes against the request.
|
||||
func (s *server) newRouter(subRoute string) *mux.Router {
|
||||
muxRouter := mux.NewRouter().StrictSlash(true)
|
||||
muxRouter.HandleFunc(subRoute+"/debug/pprof", pprof.Index)
|
||||
muxRouter.HandleFunc(subRoute+"/debug/pprof/cmdline", pprof.Cmdline)
|
||||
muxRouter.HandleFunc(subRoute+"/debug/pprof/profile", pprof.Profile)
|
||||
muxRouter.HandleFunc(subRoute+"/debug/pprof/symbol", pprof.Symbol)
|
||||
muxRouter.HandleFunc(subRoute+"/debug/pprof/trace", pprof.Trace)
|
||||
muxRouter.Handle(subRoute+"/debug/pprof/goroutine", pprof.Handler("goroutine"))
|
||||
muxRouter.Handle(subRoute+"/debug/pprof/heap", pprof.Handler("heap"))
|
||||
muxRouter.Handle(subRoute+"/debug/pprof/thread/create", pprof.Handler("threadcreate"))
|
||||
muxRouter.Handle(subRoute+"/debug/pprof/block", pprof.Handler("block"))
|
||||
muxRouter.Use(SetTraceID, apmgorilla.Middleware())
|
||||
for _, r := range s.routes {
|
||||
muxRouter.HandleFunc(subRoute+r.Pattern, r.HandlerFunc).Methods(r.Method)
|
||||
}
|
||||
|
||||
return muxRouter
|
||||
}
|
||||
195
core/appserver/middleware.go
Normal file
195
core/appserver/middleware.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package appserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/felixge/httpsnoop"
|
||||
"github.com/google/uuid"
|
||||
"gitlab.com/arkadooti.sarkar/go-boilerplate/core/appcontext"
|
||||
"gitlab.com/arkadooti.sarkar/go-boilerplate/core/log"
|
||||
"go.elastic.co/apm"
|
||||
"go.elastic.co/apm/module/apmhttp"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const TraceID = "traceid"
|
||||
|
||||
func SetTraceID(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var traceID apm.TraceID
|
||||
if values := r.Header[apmhttp.W3CTraceparentHeader]; len(values) == 1 && values[0] != "" {
|
||||
if c, err := apmhttp.ParseTraceparentHeader(values[0]); err == nil {
|
||||
traceID = c.Trace
|
||||
}
|
||||
}
|
||||
if err := traceID.Validate(); err != nil {
|
||||
uuidId := uuid.New()
|
||||
var spanID apm.SpanID
|
||||
var traceOptions apm.TraceOptions
|
||||
copy(traceID[:], uuidId[:])
|
||||
copy(spanID[:], traceID[8:])
|
||||
traceContext := apm.TraceContext{
|
||||
Trace: traceID,
|
||||
Span: spanID,
|
||||
Options: traceOptions.WithRecorded(true),
|
||||
}
|
||||
r.Header.Set(apmhttp.W3CTraceparentHeader, apmhttp.FormatTraceparentHeader(traceContext))
|
||||
}
|
||||
|
||||
w.Header().Set(TraceID, traceID.String())
|
||||
r.Header.Set(requestID, traceID.String())
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func enableCompression(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.Contains(r.Header.Get("Accept-Encoding"), "br") && !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
next(w, r)
|
||||
return
|
||||
} else if !strings.Contains(r.Header.Get("Accept-Encoding"), "br") {
|
||||
gzr := pool.Get().(*gzipResponseWriter)
|
||||
gzr.statusCode = 0
|
||||
gzr.headerWritten = false
|
||||
gzr.ResponseWriter = w
|
||||
gzr.w.Reset(w)
|
||||
defer func() {
|
||||
// gzr.w.Close will write a footer even if no data has been written.
|
||||
// StatusNotModified and StatusNoContent expect an empty body so don't close it.
|
||||
if gzr.statusCode != http.StatusNotModified && gzr.statusCode != http.StatusNoContent {
|
||||
if err := gzr.w.Close(); err != nil {
|
||||
ctx := appcontext.UpgradeCtx(r.Context())
|
||||
log.GenericError(ctx, err, nil)
|
||||
}
|
||||
}
|
||||
pool.Put(gzr)
|
||||
}()
|
||||
next(gzr, r)
|
||||
return
|
||||
}
|
||||
br := poolbr.Get().(*brotliResponseWriter)
|
||||
br.statusCode = 0
|
||||
br.headerWritten = false
|
||||
br.ResponseWriter = w
|
||||
br.w.Reset(w)
|
||||
|
||||
defer func() {
|
||||
// brotli.w.Close will write a footer even if no data has been written.x
|
||||
// StatusNotModified and StatusNoContent expect an empty body so don't close it.
|
||||
if br.statusCode != http.StatusNotModified && br.statusCode != http.StatusNoContent {
|
||||
if err := br.w.Close(); err != nil {
|
||||
ctx := appcontext.UpgradeCtx(r.Context())
|
||||
log.GenericError(ctx, err, nil)
|
||||
}
|
||||
}
|
||||
poolbr.Put(br)
|
||||
}()
|
||||
next(br, r)
|
||||
}
|
||||
}
|
||||
|
||||
func recovery(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
ctx := appcontext.UpgradeCtx(r.Context())
|
||||
rec := recover()
|
||||
if rec != nil {
|
||||
span, _ := apm.StartSpan(ctx.Context, "recovery", "custom")
|
||||
defer span.End()
|
||||
trace := string(debug.Stack())
|
||||
trace = strings.Replace(trace, "\n", " ", -1)
|
||||
trace = strings.Replace(trace, "\t", " ", -1)
|
||||
log.GenericError(ctx, fmt.Errorf("%v", rec),
|
||||
log.FieldsMap{
|
||||
"msg": "recovering from panic",
|
||||
"stackTrace": trace,
|
||||
})
|
||||
jsonBody, _ := json.Marshal(map[string]string{
|
||||
"error": "There was an internal server error",
|
||||
})
|
||||
e := apm.DefaultTracer.Recovered(rec)
|
||||
e.SetSpan(span)
|
||||
e.Send()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write(jsonBody)
|
||||
}
|
||||
}()
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func logRequest(handler http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
m := httpsnoop.CaptureMetrics(handler, w, r)
|
||||
log.HTTPLog(constructHTTPLog(r, m, time.Since(start)))
|
||||
}
|
||||
}
|
||||
|
||||
func constructHTTPLog(r *http.Request, m httpsnoop.Metrics, duration time.Duration) string {
|
||||
ctx := r.Context().Value(appcontext.AppCtx)
|
||||
|
||||
rawBody, _ := ioutil.ReadAll(r.Body)
|
||||
if len(rawBody) > 0 {
|
||||
r.Body = ioutil.NopCloser(bytes.NewBuffer(rawBody))
|
||||
}
|
||||
var jsonBody interface{}
|
||||
// For Testing
|
||||
json.Unmarshal(rawBody, &jsonBody)
|
||||
bodyJsonByte, _ := json.Marshal(jsonBody)
|
||||
if ctx != nil {
|
||||
tCtx := ctx.(appcontext.AppExtContext)
|
||||
return fmt.Sprintf("|%s|%s|%s|%s|%s|%d|%d|%s|%s|%s|",
|
||||
tCtx.UserEmail,
|
||||
"requestId="+tCtx.RequestID,
|
||||
r.RemoteAddr,
|
||||
r.Method,
|
||||
r.URL,
|
||||
m.Code,
|
||||
m.Written,
|
||||
r.UserAgent(),
|
||||
duration,
|
||||
"Body:"+string(bodyJsonByte),
|
||||
)
|
||||
}
|
||||
return fmt.Sprintf("|%s|%s|%s|%d|%d|%s|%s|%s|",
|
||||
r.RemoteAddr,
|
||||
r.Method,
|
||||
r.URL,
|
||||
m.Code,
|
||||
m.Written,
|
||||
r.UserAgent(),
|
||||
duration,
|
||||
"Body:"+string(rawBody),
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
func createContext(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
header := r.Header
|
||||
ctx := r.Context()
|
||||
reqID := header.Get(requestID)
|
||||
if reqID == "" {
|
||||
reqID = strings.ReplaceAll(uuid.NewString(), "-", "")
|
||||
}
|
||||
email, app := header.Get(userEmail), header.Get(application)
|
||||
locale := header.Get(locale)
|
||||
|
||||
tempCtx := appcontext.AppExtContext{
|
||||
RequestID: reqID,
|
||||
UserEmail: email,
|
||||
Locale: locale,
|
||||
Application: app,
|
||||
}
|
||||
|
||||
ctx = appcontext.WithAppCtx(ctx, tempCtx)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
}
|
||||
}
|
||||
19
core/appserver/routedef.go
Normal file
19
core/appserver/routedef.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package appserver
|
||||
|
||||
import "net/http"
|
||||
|
||||
func (s *server) AddNoAuthRoutes(methodName string, methodType string, mRoute string, handlerFunc http.HandlerFunc) {
|
||||
r := route{
|
||||
Name: methodName,
|
||||
Method: methodType,
|
||||
Pattern: mRoute,
|
||||
HandlerFunc: useMiddleware(handlerFunc, recovery, enableCompression, logRequest, createContext)}
|
||||
s.routes = append(s.routes, r)
|
||||
}
|
||||
|
||||
func useMiddleware(h http.HandlerFunc, middleware ...func(http.HandlerFunc) http.HandlerFunc) http.HandlerFunc {
|
||||
for _, m := range middleware {
|
||||
h = m(h)
|
||||
}
|
||||
return h
|
||||
}
|
||||
40
core/appserver/server.go
Normal file
40
core/appserver/server.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package appserver
|
||||
|
||||
import (
|
||||
"gitlab.com/arkadooti.sarkar/go-boilerplate/core/appcontext"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type route struct {
|
||||
Name string
|
||||
Method string
|
||||
Pattern string
|
||||
Modules []string
|
||||
ResourcesPermissionMap interface{}
|
||||
HandlerFunc http.HandlerFunc
|
||||
}
|
||||
|
||||
type server struct {
|
||||
port string
|
||||
subRoute string
|
||||
routes []route
|
||||
}
|
||||
|
||||
const (
|
||||
requestID = "requestId"
|
||||
userEmail = "email"
|
||||
application = "application"
|
||||
locale = "locale"
|
||||
)
|
||||
|
||||
type AppServer interface {
|
||||
Start(ctx appcontext.AppContext)
|
||||
AddNoAuthRoutes(methodName string, methodType string, mRoute string, handlerFunc http.HandlerFunc)
|
||||
}
|
||||
|
||||
func NewAppServer(port, subRoute string) AppServer {
|
||||
return &server{
|
||||
port: port,
|
||||
subRoute: subRoute,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user