This commit is contained in:
Kar
2024-01-08 14:00:41 +05:30
commit dbe86acfac
31 changed files with 1793 additions and 0 deletions

61
core/appserver/brotli.go Normal file
View 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
View 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()
}
}

View 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
}

View 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))
}
}

View 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
View 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,
}
}