From b5a3a296c4bfa193af0708b047dc2f3bf56f5872 Mon Sep 17 00:00:00 2001 From: itsLeonB Date: Wed, 3 Sep 2025 22:35:52 +0700 Subject: [PATCH 1/2] feat: add logging middleware --- .github/workflows/ci.yml | 2 +- error_middleware.go | 4 +- go.mod | 14 ++-- go.sum | 28 ++++---- logging_middleware.go | 73 ++++++++++++++++++++ test/logging_middleware_test.go | 117 ++++++++++++++++++++++++++++++++ 6 files changed, 217 insertions(+), 21 deletions(-) create mode 100644 logging_middleware.go create mode 100644 test/logging_middleware_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2448f20..d8d0e9d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [main, dev] + branches: [main] pull_request: branches: [main, dev] diff --git a/error_middleware.go b/error_middleware.go index 06c52ca..5c60cae 100644 --- a/error_middleware.go +++ b/error_middleware.go @@ -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) diff --git a/go.mod b/go.mod index 298fcfe..6fecf90 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -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 ) diff --git a/go.sum b/go.sum index ec8fc03..2f91c83 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= @@ -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= diff --git a/logging_middleware.go b/logging_middleware.go new file mode 100644 index 0000000..d26e95a --- /dev/null +++ b/logging_middleware.go @@ -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, + ) + } + } +} diff --git a/test/logging_middleware_test.go b/test/logging_middleware_test.go new file mode 100644 index 0000000..4bb3982 --- /dev/null +++ b/test/logging_middleware_test.go @@ -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) + }) + } +} From 870cb132e07c69f03b23fb5811d96a5246a651fd Mon Sep 17 00:00:00 2001 From: itsLeonB Date: Wed, 3 Sep 2025 22:53:19 +0700 Subject: [PATCH 2/2] fix: errcheck --- test/logging_middleware_test.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/logging_middleware_test.go b/test/logging_middleware_test.go index 4bb3982..2dcf194 100644 --- a/test/logging_middleware_test.go +++ b/test/logging_middleware_test.go @@ -15,14 +15,14 @@ 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 string + method string + path string + query string + statusCode int + hasError bool + errorMsg string + setupMock func(*MockLogger) }{ { name: "successful GET request", @@ -93,7 +93,7 @@ func TestMiddlewareProvider_NewLoggingMiddleware(t *testing.T) { w := httptest.NewRecorder() ctx, engine := gin.CreateTestContext(w) - + url := tt.path if tt.query != "" { url += "?" + tt.query @@ -105,7 +105,7 @@ func TestMiddlewareProvider_NewLoggingMiddleware(t *testing.T) { engine.Any("/*path", func(c *gin.Context) { c.Status(tt.statusCode) if tt.hasError { - c.Error(errors.New(tt.errorMsg)) + _ = c.Error(errors.New(tt.errorMsg)) } })