feat(ginutils): Add ginutils that is a toolbox for gin gonic
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
34
ginutils/auth.go
Normal file
34
ginutils/auth.go
Normal 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
31
ginutils/constant.go
Normal 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
84
ginutils/logger.go
Normal 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
48
ginutils/recovery.go
Normal 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
26
ginutils/requestid.go
Normal 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()
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user