feat(ginutils): Add ginutils that is a toolbox for gin gonic
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Matthieu 'JP' DERASSE
2023-08-11 18:25:36 +00:00
parent becfc84505
commit 959f245c01
8 changed files with 318 additions and 5 deletions

34
ginutils/auth.go Normal file
View File

@ -0,0 +1,34 @@
package ginutils
import (
"github.com/gin-gonic/gin"
)
// SimpleTokens is a middleware that will just check if a token match X-TOKEN header.
func SimpleTokens(tokens []string, forbiddenHandler gin.HandlerFunc) gin.HandlerFunc {
if forbiddenHandler == nil {
forbiddenHandler = func(c *gin.Context) {
c.AbortWithStatusJSON(403, map[string]string{
"message": "Forbidden",
"debugId": c.GetString(string(ContextKey_RequestID)),
})
}
}
return func(c *gin.Context) {
requestToken := c.GetHeader(string(HeaderKey_Token))
isAuthorized := false
for _, key := range tokens {
if key == requestToken {
isAuthorized = true
}
}
if isAuthorized {
c.Next()
} else {
forbiddenHandler(c)
}
}
}

31
ginutils/constant.go Normal file
View File

@ -0,0 +1,31 @@
package ginutils
type contextKey string
//nolint:exported // keeping the enum simple and readable.
const (
ContextKey_Logger contextKey = "logger"
ContextKey_RequestID contextKey = "requestID"
)
type headerKey string
//nolint:exported // keeping the enum simple and readable.
const (
HeaderKey_RequestID headerKey = "X-REQUEST-ID"
HeaderKey_Token headerKey = "X-TOKEN"
)
type logField string
//nolint:exported // keeping the enum simple and readable.
const (
LogField_RequestID logField = "request_id"
LogField_Method logField = "method"
LogField_CanonPath logField = "canon_path"
LogField_Path logField = "path"
LogField_StatusCode logField = "status_code"
LogField_Username logField = "username"
LogField_IP logField = "real_ip"
LogField_Duration logField = "duration_ms"
)

84
ginutils/logger.go Normal file
View File

@ -0,0 +1,84 @@
package ginutils
import (
"time"
"git.dev.m-and-m.ovh/mderasse/gocommon/webserver"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
// Log will create a new logrus entry and add it to the context.
// That logrus entry will include some useful extra fields.
func Log(l *logrus.Entry) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
// construct default fields
fields := logrus.Fields{
string(LogField_IP): webserver.GetClientIP(c.Request),
// Request information
string(LogField_Method): c.Request.Method,
string(LogField_CanonPath): c.FullPath(),
string(LogField_Path): c.Request.URL.String(),
string(LogField_RequestID): c.GetString(string(ContextKey_RequestID)),
}
// create the logrus entry and add it to gin context
log := l.Logger.WithFields(fields)
c.Set(string(ContextKey_Logger), log)
log.Info("[start]")
c.Next()
log.WithFields(
logrus.Fields{
string(LogField_Duration): time.Since(start).Microseconds(),
string(LogField_StatusCode): c.Writer.Status(),
}).Info("[end]")
}
}
// GetLogger will retrieve a logger instance from gin context and return it or return false.
func GetLogger(c *gin.Context) (*logrus.Entry, bool) {
if log, exist := c.Get(string(ContextKey_Logger)); exist {
return log.(*logrus.Entry), true
}
return nil, false
}
// GetLoggerWithField will add a key and value field to the logger, update the gin context, and return the new logger.
func GetLoggerWithField(c *gin.Context, key string, value interface{}) *logrus.Entry {
iLog, exist := c.Get(string(ContextKey_Logger))
if !exist {
panic("no logger in context")
}
log, ok := iLog.(*logrus.Entry)
if !ok {
panic("invalid logger in context")
}
log = log.WithField(key, value)
c.Set(string(ContextKey_Logger), log)
return log
}
// GetLoggerWithFields will add provided fields to the logger, update the gin context, and return the new logger.
func GetLoggerWithFields(c *gin.Context, fields logrus.Fields) *logrus.Entry {
iLog, exist := c.Get(string(ContextKey_Logger))
if !exist {
panic("no logger in context")
}
log, ok := iLog.(*logrus.Entry)
if !ok {
panic("invalid logger in context")
}
log = log.WithFields(fields)
c.Set(string(ContextKey_Logger), log)
return log
}

48
ginutils/recovery.go Normal file
View File

@ -0,0 +1,48 @@
package ginutils
import (
"fmt"
"runtime/debug"
"strings"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
// Recovery will catch a panic and :
// - Recover
// - log the stacktrace.
func Recovery(handler gin.HandlerFunc) gin.HandlerFunc {
if handler == nil {
handler = func(c *gin.Context) {
c.AbortWithStatusJSON(500, map[string]string{
"message": "Internal Server Error",
"debugId": c.GetString(string(ContextKey_RequestID)),
})
}
}
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
// log the error
log := GetLoggerWithFields(c, logrus.Fields{
"error": err,
"stacktrace": fmt.Sprintf("%v\n%s", err, getStacktrace()),
})
if log != nil {
log.Error("A Panic happened and has been caught")
}
handler(c)
}
}()
c.Next()
gin.Recovery()
}
}
func getStacktrace() string {
stackLines := strings.Split(string(debug.Stack()), "\n")
// removing a few first line as they are either that function or the middleware
return strings.Join(stackLines[9:], "\n")
}

26
ginutils/requestid.go Normal file
View File

@ -0,0 +1,26 @@
package ginutils
import (
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// RequestID is a middleware that will get the request_id from the incoming request if exist and
// add it to the response or generate a new ID.
func RequestID() gin.HandlerFunc {
return func(c *gin.Context) {
requestID := c.GetHeader(string(HeaderKey_RequestID))
if _, err := uuid.Parse(requestID); err != nil {
// no request ID or invalid. let's generate a new one
requestID = uuid.New().String()
}
// Adding to context
c.Set(string(ContextKey_RequestID), requestID)
// Add request-id to the header before sending the response
c.Header(string(HeaderKey_RequestID), requestID)
// Let's the next part run
c.Next()
}
}