Skip to content

Commit

Permalink
Merge pull request #8 from spotlibs/feat/pkg-middlewares-and-ctx
Browse files Browse the repository at this point in the history
feat: new middlewares and pkg `ctx`
  • Loading branch information
mdanialr authored Sep 9, 2024
2 parents 59ad860 + 03d1382 commit 2351fa1
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 4 deletions.
77 changes: 77 additions & 0 deletions ctx/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package ctx

import "github.com/goravel/framework/contracts/http"

// metadataKey key to identify that a value in context is set and get from this
// package.
const metadataKey = "spotlibs-metadata-key"

// Metadata holds any request-scoped shared data within brispot microservice.
type Metadata struct {
Authorization string
UserAgent string
CacheControl string
ForwardedFor string
RequestFrom string
DeviceId string
App string
VersionApp string
ReqId string
ReqTags string
ReqUser string
ReqNama string
ReqKodeJabatan string
ReqNamaJabatan string
ReqKodeUker string
ReqNamaUker string
ReqJenisUker string
ReqKodeMainUker string
ReqKodeRegion string
PathGateway string
ApiKey string
}

// ParseRequest return Metadata from given http context but return empty data
// instead if no data were found.
func ParseRequest(c http.Context) Metadata {
mt, ok := c.Value(metadataKey).(Metadata)
if !ok {
mt = Metadata{}
}
return mt
}

// SetFromRequestHeader set any available metadata from given http context in
// the request header.
func SetFromRequestHeader(c http.Context) {
mt := Metadata{
Authorization: c.Request().Header("Authorization"),
UserAgent: c.Request().Header("User-Agent"),
CacheControl: c.Request().Header("Cache-Control"),
ApiKey: c.Request().Header("X-Api-Key"),
ForwardedFor: c.Request().Header("X-Forwarded-For"),
RequestFrom: c.Request().Header("X-Request-From"),
DeviceId: c.Request().Header("X-Device-Id"),
App: c.Request().Header("X-App"),
VersionApp: c.Request().Header("X-Version-App"),
ReqId: c.Request().Header("X-Request-Id"),
ReqTags: c.Request().Header("X-Request-Tags"),
ReqUser: c.Request().Header("X-Request-User"),
ReqNama: c.Request().Header("X-Request-Nama"),
ReqKodeJabatan: c.Request().Header("X-Request-Kode-Jabatan"),
ReqNamaJabatan: c.Request().Header("X-Request-Nama-Jabatan"),
ReqKodeUker: c.Request().Header("X-Request-Kode-Uker"),
ReqNamaUker: c.Request().Header("X-Request-Nama-Uker"),
ReqKodeMainUker: c.Request().Header("X-Request-Kode-MainUker"),
ReqKodeRegion: c.Request().Header("X-Request-Kode-Region"),
ReqJenisUker: c.Request().Header("X-Request-Jenis-Uker"),
PathGateway: c.Request().Header("X-Path-Gateway"),
}
c.WithValue(metadataKey, mt)
}

// GetReqId shortcut of ParseRequest with ReqId to get request id from given
// http context.
func GetReqId(c http.Context) string {
return ParseRequest(c).ReqId
}
106 changes: 106 additions & 0 deletions middleware/activity_monitor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package middleware

import (
"slices"
"strings"
"time"

"github.com/bytedance/sonic"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/spotlibs/go-lib/ctx"
)

// ActivityMonitor capture and log all request/response.
func ActivityMonitor(c http.Context) {
now := time.Now()
c.Request().Next()
apiActivityRecorder(c, now)
}

func apiActivityRecorder(c http.Context, start time.Time) {
ct := c.Request().Header("Content-Type")

// check the content type and capture the request body according to it
var req any
switch {
case hasPrefix(ct, "application/json", "application/x-www-form-urlencoded"):
req = captureRequestMap(c)
case hasPrefix(ct, "multipart/form-data"):
req = captureRequestMultipart(c)
default:
req = captureRequestMap(c) // treat any unhandled content-type as map
}

// transform back response to an object before capturing
var res map[string]any
_ = sonic.ConfigFastest.Unmarshal(c.Response().Origin().Body().Bytes(), &res)

// get metadata from context
mt := ctx.ParseRequest(c)

activityData := map[string]any{
"app_name": facades.Config().GetString("app.name", "Microservice"),
"host": c.Request().Host(),
"path": c.Request().Path(),
"client_ip": c.Request().Header("X-Forwarded-For", c.Request().Ip()),
"client_app": mt.App,
"path_alias": mt.PathGateway,
"requestID": mt.ReqId,
"requestFrom": mt.RequestFrom,
"requestUser": mt.ReqUser,
"deviceID": mt.DeviceId,
"requestTags": mt.ReqTags,
"requestBody": req,
"responseBody": res,
"responseTime": time.Since(start).Milliseconds(),
"httpCode": c.Response().Origin().Status(),
"requestAt": start.Format(time.RFC3339Nano),
//"memoryUsage": // coming soon
}
st, _ := sonic.ConfigFastest.MarshalToString(activityData)
// currently only log to stdout
println(st)
}

// captureRequestMap capture request as map and transform it to json string.
func captureRequestMap(c http.Context) any {
return c.Request().All()
}

// captureRequestMultipart capture request multipart data and only get
// the information of that file such as the filename, size and extension.
func captureRequestMultipart(c http.Context) any {
reqOrg := c.Request().Origin()
_ = reqOrg.ParseMultipartForm(2 << 9) // 1024

var bagOfForm []map[string]any
// grab any available form-value
for k, v := range reqOrg.MultipartForm.Value {
bagOfForm = append(bagOfForm, map[string]any{
"field": k,
"value": v,
})
}
// grab any available files
for field, header := range reqOrg.MultipartForm.File {
for _, headerFile := range header {
if headerFile != nil {
bagOfForm = append(bagOfForm, map[string]any{
"field": field,
"filename": headerFile.Filename,
"size": int(headerFile.Size),
})
}
}
}

return bagOfForm
}

// hasPrefix return true if the given s has at least one of the given prefixes.
func hasPrefix(s string, prefix ...string) bool {
return slices.ContainsFunc(prefix, func(pre string) bool {
return strings.HasPrefix(s, pre)
})
}
13 changes: 13 additions & 0 deletions middleware/metadata_header.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package middleware

import (
"github.com/goravel/framework/contracts/http"
"github.com/spotlibs/go-lib/ctx"
)

// MetadataHeader set metadata information come from request header to current
// context.
func MetadataHeader(c http.Context) {
ctx.SetFromRequestHeader(c)
c.Request().Next()
}
8 changes: 4 additions & 4 deletions stderr/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,22 +56,22 @@ func ErrNotFound(msg ...string) error {
return errWithDebug(ERROR_CODE_DATA_NOT_FOUND, msg[0], http.StatusOK)
}

// ErrPermission error permission denied, this may commonly be used in brispot
// ErrPermission error permission denied, this may commonly be used in api
// gateway, or any other middleware that need to return non-200 http status
// code.
//
// This error will set the response http code to 403 via stderr.WithErr.
// This error will set the response http code to 403 via stdresp.WithErr.
func ErrPermission(msg ...string) error {
if len(msg) < 1 {
msg = append(msg, ERROR_DESC_PERMISSION)
}
return errWithDebug(ERROR_CODE_ACCESS_PERMISSION, msg[0], http.StatusForbidden)
}

// ErrInvHeader error invalid header, this may commonly be used in brispot
// ErrInvHeader error invalid header, this may commonly be used in spotlibs
// gateway.
//
// This error will set the response http code to 400 via stderr.WithErr.
// This error will set the response http code to 400 via stdresp.WithErr.
func ErrInvHeader(msg ...string) error {
if len(msg) < 1 {
msg = append(msg, ERROR_DESC_INVALID_HEADER)
Expand Down

0 comments on commit 2351fa1

Please sign in to comment.