Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: CI

on:
push:
branches: [main, dev]
branches: [main]
pull_request:
branches: [main, dev]

Expand Down
4 changes: 2 additions & 2 deletions error_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ func newErrorMiddleware(logger ezutil.Logger) gin.HandlerFunc {
middleware := &errorMiddleware{
logger: logger,
}
return middleware.Handle
return middleware.handle
}

// Handle is the main middleware function that processes errors and panics
func (em *errorMiddleware) Handle(ctx *gin.Context) {
func (em *errorMiddleware) handle(ctx *gin.Context) {
defer func() {
if r := recover(); r != nil {
em.handlePanic(r, ctx)
Expand Down
14 changes: 8 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require (
github.com/gin-contrib/cors v1.7.6
github.com/gin-gonic/gin v1.10.1
github.com/go-playground/validator/v10 v10.27.0
github.com/itsLeonB/ezutil/v2 v2.0.0-alpha
github.com/itsLeonB/ezutil/v2 v2.1.0
github.com/itsLeonB/ungerr v0.1.0
github.com/rotisserie/eris v0.5.4
github.com/stretchr/testify v1.11.0
Expand All @@ -32,14 +32,16 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
golang.org/x/arch v0.18.0 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // indirect
google.golang.org/genproto v0.0.0-20250826171959-ef028d996bc1 // indirect
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
28 changes: 16 additions & 12 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/itsLeonB/ezutil/v2 v2.0.0-alpha h1:hXkL4U6KC7BAamUkdyCfbxK1mVJv/CDkLlVcVWxIcT4=
github.com/itsLeonB/ezutil/v2 v2.0.0-alpha/go.mod h1:fiUusldH3h+Y3vYimdloT+CBVO2AKT0xEtXvSHgvTts=
github.com/itsLeonB/ezutil/v2 v2.1.0 h1:kzsUtToV4j2OMN3jzkSsHkFZiJY8Pron8VJVnBQRJFk=
github.com/itsLeonB/ezutil/v2 v2.1.0/go.mod h1:xEfkSPgylgeJV0jNGxDkQp7gtaHCH6ILTamWVCDY9Tw=
github.com/itsLeonB/ungerr v0.1.0 h1:t2Ezk7xYQ859U2Tx/u+5+k/Rt7D3XXqXor6w11FeO5Y=
github.com/itsLeonB/ungerr v0.1.0/go.mod h1:d1ZnTmRnnkccpRlhUMQGN8+PuMk82NqiRhcDOQ5PirY=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
Expand Down Expand Up @@ -64,6 +64,8 @@ github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUA
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rotisserie/eris v0.5.4 h1:Il6IvLdAapsMhvuOahHWiBnl1G++Q0/L5UIkI5mARSk=
github.com/rotisserie/eris v0.5.4/go.mod h1:Z/kgYTJiJtocxCbFfvRmO+QejApzG6zpyky9G1A4g9s=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
Expand All @@ -82,17 +84,19 @@ github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
google.golang.org/genproto v0.0.0-20250826171959-ef028d996bc1 h1:Nm5SEGIguOIBDXs5rhfz2aKwEVWlgwC58UcmEnLDc8Y=
google.golang.org/genproto v0.0.0-20250826171959-ef028d996bc1/go.mod h1:Jz9LrroM7Mcm+a0QrLh4UpZ1B/WhjIbqwEcUf4y08nQ=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
Expand Down
73 changes: 73 additions & 0 deletions logging_middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package ginkgo

import (
"net/http"
"time"

"github.com/gin-gonic/gin"
)

func (mp *MiddlewareProvider) NewLoggingMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
if ctx.Request.Method == http.MethodOptions {
ctx.Next()
return
}

start := time.Now()
path := ctx.Request.URL.Path
method := ctx.Request.Method

// Build full path with query string for logging
fullPath := path
if rawQuery := ctx.Request.URL.RawQuery; rawQuery != "" {
fullPath = path + "?" + rawQuery
}

// Process request
ctx.Next()

// Calculate duration
elapsed := time.Since(start)
statusCode := ctx.Writer.Status()
clientIP := ctx.ClientIP()

// Log based on status code (similar to gRPC error handling)
if statusCode >= 400 {
errorMsg := ""
if len(ctx.Errors) > 0 {
errorMsg = ctx.Errors.String()
}

if errorMsg != "" {
mp.logger.Errorf(
"[HTTP] method=%s path=%s status=%d duration=%s client_ip=%s error=%s",
method,
fullPath,
statusCode,
elapsed,
clientIP,
errorMsg,
)
} else {
mp.logger.Errorf(
"[HTTP] method=%s path=%s status=%d duration=%s client_ip=%s",
method,
fullPath,
statusCode,
elapsed,
clientIP,
)
}
} else {
mp.logger.Infof(
"[HTTP] method=%s path=%s status=%d duration=%s client_ip=%s",
method,
fullPath,
statusCode,
elapsed,
clientIP,
)
}
}
}
117 changes: 117 additions & 0 deletions test/logging_middleware_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package ginkgo_test

import (
"errors"
"net/http/httptest"
"testing"

"github.com/gin-gonic/gin"
"github.com/itsLeonB/ginkgo"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)

func TestMiddlewareProvider_NewLoggingMiddleware(t *testing.T) {
gin.SetMode(gin.TestMode)

tests := []struct {
name string
method string
path string
query string
statusCode int
hasError bool
errorMsg string
setupMock func(*MockLogger)
}{
{
name: "successful GET request",
method: "GET",
path: "/api/users",
query: "",
statusCode: 200,
hasError: false,
setupMock: func(m *MockLogger) {
m.On("Infof", mock.AnythingOfType("string"), mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything)
},
},
{
name: "successful POST with query params",
method: "POST",
path: "/api/users",
query: "include=profile",
statusCode: 201,
hasError: false,
setupMock: func(m *MockLogger) {
m.On("Infof", mock.AnythingOfType("string"), mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything)
},
},
{
name: "client error without error message",
method: "GET",
path: "/api/users/999",
query: "",
statusCode: 404,
hasError: false,
setupMock: func(m *MockLogger) {
m.On("Errorf", mock.AnythingOfType("string"), mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything)
},
},
{
name: "server error with error message",
method: "POST",
path: "/api/users",
query: "",
statusCode: 500,
hasError: true,
errorMsg: "database connection failed",
setupMock: func(m *MockLogger) {
m.On("Errorf", mock.AnythingOfType("string"), mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything)
},
},
{
name: "OPTIONS request skipped",
method: "OPTIONS",
path: "/api/users",
query: "",
statusCode: 200,
hasError: false,
setupMock: func(m *MockLogger) {
// No logging expected for OPTIONS
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockLogger := &MockLogger{}
tt.setupMock(mockLogger)

provider := ginkgo.NewMiddlewareProvider(mockLogger)
middleware := provider.NewLoggingMiddleware()
assert.NotNil(t, middleware)

w := httptest.NewRecorder()
ctx, engine := gin.CreateTestContext(w)

url := tt.path
if tt.query != "" {
url += "?" + tt.query
}
ctx.Request = httptest.NewRequest(tt.method, url, nil)

// Set up handler chain
engine.Use(middleware)
engine.Any("/*path", func(c *gin.Context) {
c.Status(tt.statusCode)
if tt.hasError {
_ = c.Error(errors.New(tt.errorMsg))
}
})

engine.ServeHTTP(w, ctx.Request)

mockLogger.AssertExpectations(t)
})
}
}
Loading