diff --git a/Makefile b/Makefile index edffb61feca4e592fbd263981a3ebcf5ea682477..43529816aa96bb8c4fc0d29c72809323a53e9b44 100644 --- a/Makefile +++ b/Makefile @@ -33,6 +33,20 @@ ifeq (,$(wildcard $(GOPATH)/bin/protoc-gen-go)) or visit \"https://github.com/golang/protobuf/tree/v1.3.2#installation\" for more.\n") endif +SERVICESDIR=pkg +SERVICELOGGING=$(shell find $(SERVICESDIR) -name "logging_middleware.go" -type f) +ERRORLOGGING=$(shell find $(SERVICESDIR) -name "error_logging_middleware.go" -type f) + +# Генерация логгирования (access & error) для всех сервисов. Предполагается наличие файлов `logging_middleware.go/error_middleware.go` +# с директивой go:generate и командой генерации кода в директориях `/pkg` сервисов +# Для установки инструмента генерации выполнить команду `go get -u github.com/hexdigest/gowrap/cmd/gowrap` +logging: $(ERRORLOGGING) $(SERVICELOGGING) + +%/middlewares/logging_middleware.go: % .FORCE + @go generate "$@" + +%/middlewares/error_logging_middleware.go: % .FORCE + @go generate "$@" #MICROGENFILES?=$(shell find $(SERVICESDIR) -name "service.go" -exec grep -Ril "microgen" {} \;) diff --git a/go.mod b/go.mod index 05e37d08d81a77ffc8319f1094590b4ec4f236ab..95dd4216c26fe2acc979e53bcff3a6ce0c37f889 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,8 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-kit/log v0.2.0 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect + github.com/go-logr/logr v1.2.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/snappy v0.0.1 // indirect github.com/google/go-cmp v0.5.7 // indirect github.com/gosimple/unidecode v1.0.1 // indirect @@ -44,10 +46,13 @@ require ( github.com/xdg-go/scram v1.1.1 // indirect github.com/xdg-go/stringprep v1.0.3 // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect + go.opentelemetry.io/otel v1.4.1 // indirect + go.opentelemetry.io/otel/trace v1.4.1 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.7.0 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/sys v0.4.0 // indirect golang.org/x/text v0.6.0 // indirect + google.golang.org/appengine v1.6.6 // indirect google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4 // indirect ) diff --git a/go.sum b/go.sum index 1b2e49b505ab646f23e970362f978a04fae0a2b4..96cbc60d3b37aaf7cc3d7d81872bae398df354e2 100644 --- a/go.sum +++ b/go.sum @@ -41,6 +41,10 @@ github.com/go-kit/log v0.2.0 h1:7i2K3eKTos3Vc0enKCfnVcgHh2olr/MyfboYq7cAcFw= github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.2.2 h1:ahHml/yUpnlb96Rp8HCvtYVPY8ZYpxq3g7UYchIYwbs= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -155,6 +159,18 @@ github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7Jul github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.mongodb.org/mongo-driver v1.11.4 h1:4ayjakA013OdpGyL2K3ZqylTac/rMjrJOMZ1EHizXas= go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= +<<<<<<< Updated upstream +======= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opentelemetry.io/otel v1.4.1 h1:QbINgGDDcoQUoMJa2mMaWno49lja9sHwp6aoa2n3a4g= +go.opentelemetry.io/otel v1.4.1/go.mod h1:StM6F/0fSwpd8dKWDCdRr7uRvEPYdW0hBSlbdTiUde4= +go.opentelemetry.io/otel/trace v1.4.1 h1:O+16qcdTrT7zxv2J6GejTPFinSwA++cYerC5iSiF8EQ= +go.opentelemetry.io/otel/trace v1.4.1/go.mod h1:iYEVbroFCNut9QkwEczV9vMRPHNKSSwYZjulEtsmhFc= +>>>>>>> Stashed changes go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= @@ -192,6 +208,11 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +<<<<<<< Updated upstream +======= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +>>>>>>> Stashed changes golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -215,6 +236,11 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +<<<<<<< Updated upstream +======= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +>>>>>>> Stashed changes golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -226,6 +252,12 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +<<<<<<< Updated upstream +======= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +>>>>>>> Stashed changes golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go index 7f7248e5208f1691d7e094eaf640f361383faab6..de104186d7fd449880b0f9554635c6e763cf62b1 100644 --- a/pkg/cache/cache.go +++ b/pkg/cache/cache.go @@ -1,11 +1,16 @@ package cache import ( + "context" "errors" "fmt" + "runtime" + "strings" "time" lru "github.com/hashicorp/golang-lru" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" "go.uber.org/zap" ) @@ -88,3 +93,24 @@ func (c *Cache) Remove(key interface{}) (err error) { return } + +func GetChildSpan(ctx context.Context, opts ...trace.SpanStartOption) (context.Context, trace.Span) { + if ctx == nil { + return otel.GetTracerProvider().Tracer("").Start(context.Background(), getFuncName(), opts...) + } + return trace.SpanFromContext(ctx).TracerProvider().Tracer("").Start(ctx, getFuncName(), opts...) +} + +func getFuncName() string { + pc, _, _, ok := runtime.Caller(2) + if !ok { + return "?" + } + + fn := runtime.FuncForPC(pc) + if fn == nil { + return "?" + } + + return strings.ReplaceAll(strings.TrimPrefix(fn.Name(), "git.perx.ru/perxis/perxis/"), "/", ".") +} diff --git a/pkg/clients/errors.go b/pkg/clients/errors.go new file mode 100644 index 0000000000000000000000000000000000000000..8d627bdb69e497134017e364a2df6e1fd2909e44 --- /dev/null +++ b/pkg/clients/errors.go @@ -0,0 +1,11 @@ +package clients + +import ( + "git.perx.ru/perxis/perxis-go/pkg/errors" +) + +var ( + ErrAccessDenied = errors.PermissionDenied(errors.New("access denied")) + ErrNotFound = errors.NotFound(errors.New("not found")) + ErrAlreadyExists = errors.AlreadyExists(errors.New("already exists")) +) diff --git a/pkg/clients/middlewares/caching_middleware.go b/pkg/clients/middlewares/caching_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..9025c2c956f73f71d067a6756c41cede10f71157 --- /dev/null +++ b/pkg/clients/middlewares/caching_middleware.go @@ -0,0 +1,179 @@ +package middlewares + +import ( + "context" + "strings" + + "git.perx.ru/perxis/perxis-go/pkg/cache" + service "git.perx.ru/perxis/perxis-go/pkg/clients" +) + +func makeKey(ss ...string) string { + return strings.Join(ss, "-") +} + +func CachingMiddleware(cache *cache.Cache) Middleware { + return func(next service.Clients) service.Clients { + return &cachingMiddleware{ + cache: cache, + next: next, + } + } +} + +type cachingMiddleware struct { + cache *cache.Cache + next service.Clients +} + +func (m cachingMiddleware) Create(ctx context.Context, client *service.Client) (cl *service.Client, err error) { + ctx, span := cache.GetChildSpan(ctx) + defer span.End() + + cl, err = m.next.Create(ctx, client) + if err == nil { + m.cache.Remove(cl.SpaceID) + } + return cl, err +} + +func (m cachingMiddleware) Get(ctx context.Context, spaceId string, id string) (cl *service.Client, err error) { + ctx, span := cache.GetChildSpan(ctx) + defer span.End() + + key := makeKey(spaceId, id) + value, e := m.cache.Get(key) + if e == nil { + return value.(*service.Client), err + } + cl, err = m.next.Get(ctx, spaceId, id) + if err == nil { + m.cache.Set(key, cl) + for _, key := range keysFromIdentities(spaceId, cl) { + m.cache.Set(key, cl) + } + } + return cl, err +} + +func (m cachingMiddleware) GetBy(ctx context.Context, spaceId string, params *service.GetByParams) (cl *service.Client, err error) { + if params == nil { + return m.next.GetBy(ctx, spaceId, params) + } + ctx, span := cache.GetChildSpan(ctx) + defer span.End() + + key := getIdentKey(spaceId, params) + value, e := m.cache.Get(key) + if e == nil { + return value.(*service.Client), err + } + cl, err = m.next.GetBy(ctx, spaceId, params) + if err == nil { + m.cache.Set(makeKey(spaceId, cl.ID), cl) + for _, key := range keysFromIdentities(spaceId, cl) { + m.cache.Set(key, cl) + } + } + return cl, err +} + +func (m cachingMiddleware) List(ctx context.Context, spaceId string) (clients []*service.Client, err error) { + ctx, span := cache.GetChildSpan(ctx) + defer span.End() + + value, e := m.cache.Get(spaceId) + if e == nil { + return value.([]*service.Client), err + } + clients, err = m.next.List(ctx, spaceId) + if err == nil { + m.cache.Set(spaceId, clients) + } + return clients, err +} + +func (m cachingMiddleware) Update(ctx context.Context, client *service.Client) (err error) { + ctx, span := cache.GetChildSpan(ctx) + defer span.End() + + err = m.next.Update(ctx, client) + + if err == nil { + m.cache.Remove(client.SpaceID) + value, e := m.cache.Get(makeKey(client.SpaceID, client.ID)) + if e == nil { + client := value.(*service.Client) + m.cache.Remove(makeKey(client.SpaceID, client.ID)) + for _, key := range keysFromIdentities(client.SpaceID, client) { + m.cache.Remove(key) + } + } + } + return err +} + +func (m cachingMiddleware) Delete(ctx context.Context, spaceId string, id string) (err error) { + ctx, span := cache.GetChildSpan(ctx) + defer span.End() + + err = m.next.Delete(ctx, spaceId, id) + if err == nil { + value, e := m.cache.Get(makeKey(spaceId, id)) + if e == nil { + client := value.(*service.Client) + m.cache.Remove(makeKey(client.SpaceID, client.ID)) + for _, key := range keysFromIdentities(client.SpaceID, client) { + m.cache.Remove(key) + } + } + m.cache.Remove(spaceId) + } + return err +} + +func (m cachingMiddleware) Enable(ctx context.Context, spaceId string, id string, enable bool) (err error) { + ctx, span := cache.GetChildSpan(ctx) + defer span.End() + + err = m.next.Enable(ctx, spaceId, id, enable) + if err == nil { + value, e := m.cache.Get(makeKey(spaceId, id)) + if e == nil { + client := value.(*service.Client) + m.cache.Remove(makeKey(client.SpaceID, client.ID)) + for _, key := range keysFromIdentities(client.SpaceID, client) { + m.cache.Remove(key) + } + } + m.cache.Remove(spaceId) + } + return err +} + +func keysFromIdentities(spaceID string, client *service.Client) []string { + res := make([]string, 0) + if client.APIKey != nil && client.APIKey.Key != "" { + res = append(res, makeKey(spaceID, "api-key", client.APIKey.Key)) + } + if client.TLS != nil && client.TLS.Subject != "" { + res = append(res, makeKey(spaceID, "tls", client.TLS.Subject)) + } + if client.OAuth != nil && client.OAuth.ClientID != "" { + res = append(res, makeKey(spaceID, "oauth", client.OAuth.ClientID)) + } + return res +} + +func getIdentKey(spaceID string, params *service.GetByParams) string { + switch { + case params.APIKey != "": + return makeKey(spaceID, "api-key", params.APIKey) + case params.TLSSubject != "": + return makeKey(spaceID, "tls", params.TLSSubject) + case params.OAuthClientID != "": + return makeKey(spaceID, "oauth", params.OAuthClientID) + default: + return "" + } +} diff --git a/pkg/clients/middlewares/caching_middleware_test.go b/pkg/clients/middlewares/caching_middleware_test.go new file mode 100644 index 0000000000000000000000000000000000000000..5348a00490465e366c30ba2bbf6f61c232c0b279 --- /dev/null +++ b/pkg/clients/middlewares/caching_middleware_test.go @@ -0,0 +1,379 @@ +package middlewares + +import ( + "context" + "testing" + "time" + + "git.perx.ru/perxis/perxis-go/pkg/cache" + "git.perx.ru/perxis/perxis-go/pkg/clients" + csmocks "git.perx.ru/perxis/perxis-go/pkg/clients/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestClientsCache(t *testing.T) { + + const ( + cltID = "cltID" + spaceID = "spaceID" + clientID = "123@client" + size = 5 + ttl = 20 * time.Millisecond + ) + + ctx := context.Background() + + t.Run("Get from cache", func(t *testing.T) { + cs := &csmocks.Clients{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(cs) + + cs.On("Get", mock.Anything, spaceID, cltID).Return(&clients.Client{ID: cltID, SpaceID: spaceID, Name: "client_1", OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, cltID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, cltID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта из кэша, после повторного запроса.") + + v3, err := svc.GetBy(ctx, spaceID, &clients.GetByParams{OAuthClientID: clientID}) + require.NoError(t, err) + assert.Same(t, v2, v3, "Ожидается получение объекта из кэша при запросе по ClientID.") + + cs.AssertExpectations(t) + }) + + t.Run("GetBy from cache", func(t *testing.T) { + cs := &csmocks.Clients{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(cs) + + cs.On("GetBy", mock.Anything, spaceID, &clients.GetByParams{OAuthClientID: clientID}).Return(&clients.Client{ID: cltID, SpaceID: spaceID, Name: "client_1", OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}}, nil).Once() + + v1, err := svc.GetBy(ctx, spaceID, &clients.GetByParams{OAuthClientID: clientID}) + require.NoError(t, err) + + v2, err := svc.GetBy(ctx, spaceID, &clients.GetByParams{OAuthClientID: clientID}) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта из кэша, после повторного запроса.") + + v3, err := svc.Get(ctx, spaceID, cltID) + require.NoError(t, err) + assert.Same(t, v2, v3, "Ожидается получение объекта из кэша, после запроса Get.") + + cs.AssertExpectations(t) + }) + + t.Run("List", func(t *testing.T) { + cs := &csmocks.Clients{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(cs) + + cs.On("List", mock.Anything, spaceID).Return([]*clients.Client{{ID: cltID, SpaceID: spaceID, Name: "client_1", OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}}}, nil).Once() + + vl1, err := svc.List(ctx, spaceID) + require.NoError(t, err) + + vl2, err := svc.List(ctx, spaceID) + require.NoError(t, err) + assert.Same(t, vl1[0], vl2[0], "Ожидается получение объектов из кэша, после повторного запроса.") + + cs.AssertExpectations(t) + }) + + t.Run("Invalidate cache", func(t *testing.T) { + + t.Run("After Update", func(t *testing.T) { + cs := &csmocks.Clients{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(cs) + + cs.On("Get", mock.Anything, spaceID, cltID).Return(&clients.Client{ID: cltID, SpaceID: spaceID, Name: "client_1", OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}}, nil).Once() + cs.On("List", mock.Anything, spaceID).Return([]*clients.Client{{ID: cltID, SpaceID: spaceID, Name: "client_1", OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}}}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, cltID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, cltID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта из кэша.") + + v3, err := svc.GetBy(ctx, spaceID, &clients.GetByParams{OAuthClientID: clientID}) + require.NoError(t, err) + assert.Same(t, v2, v3, "Ожидается получение объекта из кэша по ClientID.") + + vl1, err := svc.List(ctx, spaceID) + require.NoError(t, err) + + vl2, err := svc.List(ctx, spaceID) + require.NoError(t, err) + assert.Len(t, vl2, 1) + assert.Same(t, vl1[0], vl2[0], "Ожидается получение объектов из кэша, после повторного запроса.") + + cs.On("Update", mock.Anything, mock.Anything).Return(nil).Once() + + err = svc.Update(ctx, &clients.Client{ID: cltID, SpaceID: spaceID, Name: "client_2", OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}}) + require.NoError(t, err) + + cs.On("Get", mock.Anything, spaceID, cltID).Return(&clients.Client{ID: cltID, SpaceID: spaceID, Name: "client_2", OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}}, nil).Once() + cs.On("List", mock.Anything, spaceID).Return([]*clients.Client{{ID: cltID, SpaceID: spaceID, Name: "client_2", OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}}}, nil).Once() + + vl3, err := svc.List(ctx, spaceID) + require.NoError(t, err) + assert.NotSame(t, vl2[0], vl3[0], "Ожидается получение объектов из кэша, после повторного запроса.") + + v4, err := svc.Get(ctx, spaceID, cltID) + require.NoError(t, err) + assert.NotSame(t, v2, v4, "Ожидает что после обновления объект был удален из кэша и будет запрошен заново из сервиса.") + + v5, err := svc.GetBy(ctx, spaceID, &clients.GetByParams{OAuthClientID: clientID}) + require.NoError(t, err) + assert.NotSame(t, v3, v5) + assert.Same(t, v4, v5, "Ожидается что после обновления объект был удален из кеша и после запроса Get в кеш попал объект запрошенный заново из сервиса.") + + cs.AssertExpectations(t) + }) + + t.Run("After Update(List)", func(t *testing.T) { + cs := &csmocks.Clients{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(cs) + + cs.On("List", mock.Anything, spaceID).Return([]*clients.Client{{ID: cltID, SpaceID: spaceID, Name: "client_1", OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}}}, nil).Once() + + vl1, err := svc.List(ctx, spaceID) + require.NoError(t, err) + + vl2, err := svc.List(ctx, spaceID) + require.NoError(t, err) + assert.Len(t, vl2, 1) + assert.Same(t, vl1[0], vl2[0], "Ожидается получение объектов из кэша, после повторного запроса.") + + cs.On("Update", mock.Anything, mock.Anything).Return(nil).Once() + + err = svc.Update(ctx, &clients.Client{ID: cltID, SpaceID: spaceID, Name: "client_2", OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}}) + require.NoError(t, err) + + cs.On("List", mock.Anything, spaceID).Return([]*clients.Client{{ID: cltID, SpaceID: spaceID, Name: "client_2", OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}}}, nil).Once() + + vl3, err := svc.List(ctx, spaceID) + require.NoError(t, err) + assert.NotSame(t, vl2[0], vl3[0], "Ожидается получение объектов из кэша, после повторного запроса.") + + cs.AssertExpectations(t) + }) + + t.Run("After Delete", func(t *testing.T) { + cs := &csmocks.Clients{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(cs) + + cs.On("Get", mock.Anything, spaceID, cltID).Return(&clients.Client{ID: cltID, SpaceID: spaceID, Name: "client_1", OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}}, nil).Once() + cs.On("List", mock.Anything, spaceID).Return([]*clients.Client{{ID: cltID, SpaceID: spaceID, Name: "client_1", OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}}}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, cltID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, cltID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта из кэша.") + + v3, err := svc.GetBy(ctx, spaceID, &clients.GetByParams{OAuthClientID: clientID}) + require.NoError(t, err) + assert.Same(t, v2, v3, "Ожидается получение объекта из кэша по ClientID.") + + vl1, err := svc.List(ctx, spaceID) + require.NoError(t, err) + + vl2, err := svc.List(ctx, spaceID) + require.NoError(t, err) + assert.Same(t, vl1[0], vl2[0], "Ожидается получение объектов из кэша, после повторного запроса.") + + cs.On("Delete", mock.Anything, spaceID, cltID).Return(nil).Once() + + err = svc.Delete(ctx, spaceID, cltID) + require.NoError(t, err) + + cs.On("Get", mock.Anything, spaceID, cltID).Return(nil, clients.ErrNotFound).Once() + cs.On("GetBy", mock.Anything, spaceID, &clients.GetByParams{OAuthClientID: clientID}).Return(nil, clients.ErrNotFound).Once() + cs.On("List", mock.Anything, spaceID).Return(nil, clients.ErrNotFound).Once() + + _, err = svc.Get(ctx, spaceID, cltID) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидается что после удаление из хранилища объект был удален из кэша и получена ошибка из сервиса.") + + _, err = svc.GetBy(ctx, spaceID, &clients.GetByParams{OAuthClientID: clientID}) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидается что после удаление из хранилища объект был удален из кэша и получена ошибка из сервиса.") + + _, err = svc.List(ctx, spaceID) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидается что после удаление из хранилища объекты были удалены из кэша.") + + cs.AssertExpectations(t) + }) + + t.Run("After Delete(List)", func(t *testing.T) { + cs := &csmocks.Clients{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(cs) + + cs.On("List", mock.Anything, spaceID).Return([]*clients.Client{{ID: cltID, SpaceID: spaceID, Name: "client_1", OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}}}, nil).Once() + + vl1, err := svc.List(ctx, spaceID) + require.NoError(t, err) + + vl2, err := svc.List(ctx, spaceID) + require.NoError(t, err) + assert.Same(t, vl1[0], vl2[0], "Ожидается получение объектов из кэша, после повторного запроса.") + + cs.On("Delete", mock.Anything, spaceID, cltID).Return(nil).Once() + + err = svc.Delete(ctx, spaceID, cltID) + require.NoError(t, err) + + cs.On("List", mock.Anything, spaceID).Return(nil, clients.ErrNotFound).Once() + + _, err = svc.List(ctx, spaceID) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидается что после удаление из хранилища объекты были удалены из кэша.") + + cs.AssertExpectations(t) + }) + + t.Run("After Create", func(t *testing.T) { + cs := &csmocks.Clients{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(cs) + + cs.On("List", mock.Anything, spaceID).Return([]*clients.Client{{ID: cltID, SpaceID: spaceID, Name: "client_1"}}, nil).Once() + + vl1, err := svc.List(ctx, spaceID) + require.NoError(t, err) + + vl2, err := svc.List(ctx, spaceID) + require.NoError(t, err) + assert.Same(t, vl1[0], vl2[0], "Ожидается получение объектов из кэша, после повторного запроса.") + assert.Len(t, vl2, 1, "Ожидается получение объектов из кэша.") + + cs.On("Create", mock.Anything, mock.Anything).Return(&clients.Client{ID: "cltID2", SpaceID: spaceID, Name: "client_2"}, nil).Once() + + _, err = svc.Create(ctx, &clients.Client{ID: "cltID2", SpaceID: spaceID, Name: "client_2"}) + require.NoError(t, err) + + cs.On("List", mock.Anything, spaceID).Return([]*clients.Client{{ID: cltID, SpaceID: spaceID, Name: "client_1"}, {ID: "cltID2", SpaceID: spaceID, Name: "client_2"}}, nil).Once() + + vl3, err := svc.List(ctx, spaceID) + require.NoError(t, err) + assert.Len(t, vl3, 2, "Ожидается что после создания нового объекта кеш будет очищен и объекты запрошены заново из сервиса.") + + cs.AssertExpectations(t) + }) + + t.Run("After Enable", func(t *testing.T) { + cs := &csmocks.Clients{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(cs) + + tr := true + cs.On("Get", mock.Anything, spaceID, cltID).Return(&clients.Client{ID: cltID, SpaceID: spaceID, OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}, Disabled: &tr}, nil).Once() + cs.On("List", mock.Anything, spaceID).Return([]*clients.Client{{ID: cltID, SpaceID: spaceID, OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}, Disabled: &tr}}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, cltID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, cltID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта из кэша.") + + v3, err := svc.GetBy(ctx, spaceID, &clients.GetByParams{OAuthClientID: clientID}) + require.NoError(t, err) + assert.Same(t, v2, v3, "Ожидается получение объекта из кэша по ClientID.") + + vl1, err := svc.List(ctx, spaceID) + require.NoError(t, err) + + vl2, err := svc.List(ctx, spaceID) + require.NoError(t, err) + assert.Same(t, vl1[0], vl2[0], "Ожидается получение объектов из кэша, после повторного запроса.") + + cs.On("Enable", mock.Anything, spaceID, cltID, tr).Return(nil).Once() + + err = svc.Enable(ctx, spaceID, cltID, tr) + require.NoError(t, err) + + fl := false + cs.On("Get", mock.Anything, spaceID, cltID).Return(&clients.Client{ID: cltID, SpaceID: spaceID, Name: "client_1", OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}, Disabled: &fl}, nil).Once() + cs.On("List", mock.Anything, spaceID).Return([]*clients.Client{{ID: cltID, SpaceID: spaceID, OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}, Disabled: &fl}}, nil).Once() + + v4, err := svc.Get(ctx, spaceID, cltID) + require.NoError(t, err) + assert.NotSame(t, v2, v4, "Ожидается что после активации объект был удален из кэша и запрошен у сервиса.") + + v5, err := svc.GetBy(ctx, spaceID, &clients.GetByParams{OAuthClientID: clientID}) + assert.NotSame(t, v3, v5, "Ожидается что после активации объект был удален из кеша и после запроса Get в кеш попал объект запрошенный заново из сервиса.") + + vl3, err := svc.List(ctx, spaceID) + require.NoError(t, err) + assert.NotSame(t, vl2[0], vl3[0], "Ожидается что после активации объекта, кеш будет очищен и объекты будут запрошены заново из сервиса.") + + cs.AssertExpectations(t) + }) + + t.Run("After Enable(List)", func(t *testing.T) { + cs := &csmocks.Clients{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(cs) + + tr := true + cs.On("List", mock.Anything, spaceID).Return([]*clients.Client{{ID: cltID, SpaceID: spaceID, OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}, Disabled: &tr}}, nil).Once() + + vl1, err := svc.List(ctx, spaceID) + require.NoError(t, err) + + vl2, err := svc.List(ctx, spaceID) + require.NoError(t, err) + assert.Same(t, vl1[0], vl2[0], "Ожидается получение объектов из кэша, после повторного запроса.") + + cs.On("Enable", mock.Anything, spaceID, cltID, tr).Return(nil).Once() + + err = svc.Enable(ctx, spaceID, cltID, tr) + require.NoError(t, err) + + fl := false + cs.On("List", mock.Anything, spaceID).Return([]*clients.Client{{ID: cltID, SpaceID: spaceID, OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}, Disabled: &fl}}, nil).Once() + + vl3, err := svc.List(ctx, spaceID) + require.NoError(t, err) + assert.NotSame(t, vl2[0], vl3[0], "Ожидается что после активации объекта, кеш будет очищен и объекты будут запрошены заново из сервиса.") + + cs.AssertExpectations(t) + }) + + t.Run("After TTL expired", func(t *testing.T) { + cs := &csmocks.Clients{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(cs) + + cs.On("Get", mock.Anything, spaceID, cltID).Return(&clients.Client{ID: cltID, SpaceID: spaceID, Name: "client_1", OAuth: &clients.OAuth{ClientID: clientID}}, nil).Once() + cs.On("Get", mock.Anything, spaceID, cltID).Return(&clients.Client{ID: cltID, SpaceID: spaceID, Name: "client_1", OAuth: &clients.OAuth{ClientID: clientID}}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, cltID) + require.NoError(t, err) + v2, err := svc.Get(ctx, spaceID, cltID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта из кэша после повторного запроса.") + + time.Sleep(2 * ttl) + + v3, err := svc.Get(ctx, spaceID, cltID) + require.NoError(t, err) + assert.NotSame(t, v2, v3, "Ожидается что элемент был удален из кэша по истечению ttl и будет запрошен заново из сервиса.") + + cs.AssertExpectations(t) + }) + }) +} diff --git a/pkg/clients/middlewares/error_logging_middleware.go b/pkg/clients/middlewares/error_logging_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..06e6d4dad91e05db23dce8bd6a1c1003e49e2e4e --- /dev/null +++ b/pkg/clients/middlewares/error_logging_middleware.go @@ -0,0 +1,100 @@ +// Code generated by gowrap. DO NOT EDIT. +// template: ../../templates/middleware/error_log +// gowrap: http://github.com/hexdigest/gowrap + +package middlewares + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/clients -i Clients -t ../../templates/middleware/error_log -o error_logging_middleware.go -l "" + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/clients" + "go.uber.org/zap" +) + +// errorLoggingMiddleware implements clients.Clients that is instrumented with logging +type errorLoggingMiddleware struct { + logger *zap.Logger + next clients.Clients +} + +// ErrorLoggingMiddleware instruments an implementation of the clients.Clients with simple logging +func ErrorLoggingMiddleware(logger *zap.Logger) Middleware { + return func(next clients.Clients) clients.Clients { + return &errorLoggingMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *errorLoggingMiddleware) Create(ctx context.Context, client *clients.Client) (created *clients.Client, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Create(ctx, client) +} + +func (m *errorLoggingMiddleware) Delete(ctx context.Context, spaceId string, id string) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Delete(ctx, spaceId, id) +} + +func (m *errorLoggingMiddleware) Enable(ctx context.Context, spaceId string, id string, enable bool) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Enable(ctx, spaceId, id, enable) +} + +func (m *errorLoggingMiddleware) Get(ctx context.Context, spaceId string, id string) (client *clients.Client, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Get(ctx, spaceId, id) +} + +func (m *errorLoggingMiddleware) GetBy(ctx context.Context, spaceId string, params *clients.GetByParams) (client *clients.Client, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.GetBy(ctx, spaceId, params) +} + +func (m *errorLoggingMiddleware) List(ctx context.Context, spaceId string) (clients []*clients.Client, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.List(ctx, spaceId) +} + +func (m *errorLoggingMiddleware) Update(ctx context.Context, client *clients.Client) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Update(ctx, client) +} diff --git a/pkg/clients/middlewares/logging_middleware.go b/pkg/clients/middlewares/logging_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..1e664c65bc688ee62cf9fe808a46020d735cb877 --- /dev/null +++ b/pkg/clients/middlewares/logging_middleware.go @@ -0,0 +1,288 @@ +// Code generated by gowrap. DO NOT EDIT. +// template: ../../templates/middleware/access_log +// gowrap: http://github.com/hexdigest/gowrap + +package middlewares + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/clients -i Clients -t ../../templates/middleware/access_log -o logging_middleware.go -l "" + +import ( + "context" + "fmt" + "time" + + "git.perx.ru/perxis/perxis-go/pkg/auth" + "git.perx.ru/perxis/perxis-go/pkg/clients" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// loggingMiddleware implements clients.Clients that is instrumented with logging +type loggingMiddleware struct { + logger *zap.Logger + next clients.Clients +} + +// LoggingMiddleware instruments an implementation of the clients.Clients with simple logging +func LoggingMiddleware(logger *zap.Logger) Middleware { + return func(next clients.Clients) clients.Clients { + return &loggingMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *loggingMiddleware) Create(ctx context.Context, client *clients.Client) (created *clients.Client, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "client": client} { + if k == "ctx" { + fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx)))) + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Create.Request", fields...) + + created, err = m.next.Create(ctx, client) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "created": created, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Create.Response", fields...) + + return created, err +} + +func (m *loggingMiddleware) Delete(ctx context.Context, spaceId string, id string) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "id": id} { + if k == "ctx" { + fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx)))) + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Delete.Request", fields...) + + err = m.next.Delete(ctx, spaceId, id) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Delete.Response", fields...) + + return err +} + +func (m *loggingMiddleware) Enable(ctx context.Context, spaceId string, id string, enable bool) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "id": id, + "enable": enable} { + if k == "ctx" { + fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx)))) + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Enable.Request", fields...) + + err = m.next.Enable(ctx, spaceId, id, enable) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Enable.Response", fields...) + + return err +} + +func (m *loggingMiddleware) Get(ctx context.Context, spaceId string, id string) (client *clients.Client, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "id": id} { + if k == "ctx" { + fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx)))) + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Get.Request", fields...) + + client, err = m.next.Get(ctx, spaceId, id) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "client": client, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Get.Response", fields...) + + return client, err +} + +func (m *loggingMiddleware) GetBy(ctx context.Context, spaceId string, params *clients.GetByParams) (client *clients.Client, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "params": params} { + if k == "ctx" { + fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx)))) + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("GetBy.Request", fields...) + + client, err = m.next.GetBy(ctx, spaceId, params) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "client": client, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("GetBy.Response", fields...) + + return client, err +} + +func (m *loggingMiddleware) List(ctx context.Context, spaceId string) (clients []*clients.Client, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId} { + if k == "ctx" { + fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx)))) + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("List.Request", fields...) + + clients, err = m.next.List(ctx, spaceId) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "clients": clients, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("List.Response", fields...) + + return clients, err +} + +func (m *loggingMiddleware) Update(ctx context.Context, client *clients.Client) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "client": client} { + if k == "ctx" { + fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx)))) + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Update.Request", fields...) + + err = m.next.Update(ctx, client) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Update.Response", fields...) + + return err +} diff --git a/pkg/clients/middlewares/middleware.go b/pkg/clients/middlewares/middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..63e60aeea105f8172ecb5bf6604f480962db3dc2 --- /dev/null +++ b/pkg/clients/middlewares/middleware.go @@ -0,0 +1,28 @@ +// Code generated by gowrap. DO NOT EDIT. +// template: ../../templates/middleware/middleware +// gowrap: http://github.com/hexdigest/gowrap + +package middlewares + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/clients -i Clients -t ../../templates/middleware/middleware -o middleware.go -l "" + +import ( + "git.perx.ru/perxis/perxis-go/pkg/clients" + "go.uber.org/zap" +) + +type Middleware func(clients.Clients) clients.Clients + +func WithLog(s clients.Clients, logger *zap.Logger, log_access bool) clients.Clients { + if logger == nil { + logger = zap.NewNop() + } + + logger = logger.Named("Clients") + s = ErrorLoggingMiddleware(logger)(s) + if log_access { + s = LoggingMiddleware(logger)(s) + } + s = RecoveringMiddleware(logger)(s) + return s +} diff --git a/pkg/clients/middlewares/recovering_middleware.go b/pkg/clients/middlewares/recovering_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..93763d12e2885eb2e1f2150715ef75e60ffa096c --- /dev/null +++ b/pkg/clients/middlewares/recovering_middleware.go @@ -0,0 +1,115 @@ +// Code generated by gowrap. DO NOT EDIT. +// template: ../../templates/middleware/recovery +// gowrap: http://github.com/hexdigest/gowrap + +package middlewares + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/clients -i Clients -t ../../templates/middleware/recovery -o recovering_middleware.go -l "" + +import ( + "context" + "fmt" + + "git.perx.ru/perxis/perxis-go/pkg/clients" + "go.uber.org/zap" +) + +// recoveringMiddleware implements clients.Clients that is instrumented with logging +type recoveringMiddleware struct { + logger *zap.Logger + next clients.Clients +} + +// RecoveringMiddleware instruments an implementation of the clients.Clients with simple logging +func RecoveringMiddleware(logger *zap.Logger) Middleware { + return func(next clients.Clients) clients.Clients { + return &recoveringMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *recoveringMiddleware) Create(ctx context.Context, client *clients.Client) (created *clients.Client, err error) { + logger := m.logger + defer func() { + if r := recover(); r != nil { + logger.Error("panic", zap.Error(fmt.Errorf("%v", r))) + err = fmt.Errorf("%v", r) + } + }() + + return m.next.Create(ctx, client) +} + +func (m *recoveringMiddleware) Delete(ctx context.Context, spaceId string, id string) (err error) { + logger := m.logger + defer func() { + if r := recover(); r != nil { + logger.Error("panic", zap.Error(fmt.Errorf("%v", r))) + err = fmt.Errorf("%v", r) + } + }() + + return m.next.Delete(ctx, spaceId, id) +} + +func (m *recoveringMiddleware) Enable(ctx context.Context, spaceId string, id string, enable bool) (err error) { + logger := m.logger + defer func() { + if r := recover(); r != nil { + logger.Error("panic", zap.Error(fmt.Errorf("%v", r))) + err = fmt.Errorf("%v", r) + } + }() + + return m.next.Enable(ctx, spaceId, id, enable) +} + +func (m *recoveringMiddleware) Get(ctx context.Context, spaceId string, id string) (client *clients.Client, err error) { + logger := m.logger + defer func() { + if r := recover(); r != nil { + logger.Error("panic", zap.Error(fmt.Errorf("%v", r))) + err = fmt.Errorf("%v", r) + } + }() + + return m.next.Get(ctx, spaceId, id) +} + +func (m *recoveringMiddleware) GetBy(ctx context.Context, spaceId string, params *clients.GetByParams) (client *clients.Client, err error) { + logger := m.logger + defer func() { + if r := recover(); r != nil { + logger.Error("panic", zap.Error(fmt.Errorf("%v", r))) + err = fmt.Errorf("%v", r) + } + }() + + return m.next.GetBy(ctx, spaceId, params) +} + +func (m *recoveringMiddleware) List(ctx context.Context, spaceId string) (clients []*clients.Client, err error) { + logger := m.logger + defer func() { + if r := recover(); r != nil { + logger.Error("panic", zap.Error(fmt.Errorf("%v", r))) + err = fmt.Errorf("%v", r) + } + }() + + return m.next.List(ctx, spaceId) +} + +func (m *recoveringMiddleware) Update(ctx context.Context, client *clients.Client) (err error) { + logger := m.logger + defer func() { + if r := recover(); r != nil { + logger.Error("panic", zap.Error(fmt.Errorf("%v", r))) + err = fmt.Errorf("%v", r) + } + }() + + return m.next.Update(ctx, client) +} diff --git a/pkg/templates/middleware/access_log b/pkg/templates/middleware/access_log new file mode 100644 index 0000000000000000000000000000000000000000..a8587b82d5a72130690a61c81e9f78e5eeb6e726 --- /dev/null +++ b/pkg/templates/middleware/access_log @@ -0,0 +1,64 @@ +import ( + "fmt" + "time" + "context" + + "go.uber.org/zap" +) + +{{ $funcName := (or .Vars.FuncName ("LoggingMiddleware")) }} +{{ $decorator := (or .Vars.DecoratorName ("loggingMiddleware")) }} + +// {{$decorator}} implements {{.Interface.Type}} that is instrumented with logging +type {{$decorator}} struct { + logger *zap.Logger + next {{.Interface.Type}} +} + +// {{$funcName}} instruments an implementation of the {{.Interface.Type}} with simple logging +func {{$funcName}}(logger *zap.Logger) Middleware { + return func(next {{.Interface.Type}}) {{.Interface.Type}} { + return &{{$decorator}}{ + next: next, + logger: logger, + } + } +} + +{{range $method := .Interface.Methods}} + func (m *{{$decorator}}) {{$method.Declaration}} { + begin := time.Now() + {{- if $method.HasParams}} + var fields []zapcore.Field + for k, v := range {{$method.ParamsMap}} { + if k == "ctx" { + fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx)))) + continue + } + fields = append(fields, zap.Reflect(k,v)) + } + {{end}} + + m.logger.Debug("{{$method.Name}}.Request",fields...) + + {{ $method.ResultsNames }} = m.next.{{ $method.Call }} + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + {{ if $method.HasResults}} + for k, v := range {{$method.ResultsMap}} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k,v)) + } + {{end}} + + m.logger.Debug("{{$method.Name}}.Response", fields...) + + return {{ $method.ResultsNames }} + } +{{end}} diff --git a/pkg/templates/middleware/error_log b/pkg/templates/middleware/error_log new file mode 100755 index 0000000000000000000000000000000000000000..9455e907b738801eb7f2d43d428d98cc620370a0 --- /dev/null +++ b/pkg/templates/middleware/error_log @@ -0,0 +1,40 @@ +import ( + "io" + "time" + + "go.uber.org/zap" +) + +{{ $funcName := (or .Vars.FuncName ("ErrorLoggingMiddleware")) }} +{{ $decorator := (or .Vars.DecoratorName ("errorLoggingMiddleware")) }} + +// {{$decorator}} implements {{.Interface.Type}} that is instrumented with logging +type {{$decorator}} struct { + logger *zap.Logger + next {{.Interface.Type}} +} + +// {{$funcName}} instruments an implementation of the {{.Interface.Type}} with simple logging +func {{$funcName}}(logger *zap.Logger) Middleware { + return func(next {{.Interface.Type}}) {{.Interface.Type}} { + return &{{$decorator}}{ + next: next, + logger: logger, + } + } +} + +{{range $method := .Interface.Methods}} + func (m *{{$decorator}}) {{$method.Declaration}} { + logger := m.logger + {{- if $method.ReturnsError}} + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + {{end -}} + + {{ $method.Pass "m.next." }} + } +{{end}} diff --git a/pkg/templates/middleware/middleware b/pkg/templates/middleware/middleware new file mode 100755 index 0000000000000000000000000000000000000000..89877774c933840c2bdd569f2beed8105588aae2 --- /dev/null +++ b/pkg/templates/middleware/middleware @@ -0,0 +1,21 @@ +import ( + "go.uber.org/zap" +) + +type Middleware func({{.Interface.Type}}) {{.Interface.Type}} + + +func WithLog(s {{.Interface.Type}}, logger *zap.Logger, log_access bool) {{.Interface.Type}} { + if logger == nil { + logger = zap.NewNop() + } + + logger = logger.Named("{{ .Interface.Name }}") + s = ErrorLoggingMiddleware(logger)(s) + if log_access { + s = LoggingMiddleware(logger)(s) + } + s = RecoveringMiddleware(logger)(s) + return s +} + diff --git a/pkg/templates/middleware/recovery b/pkg/templates/middleware/recovery new file mode 100644 index 0000000000000000000000000000000000000000..a84fa3f913e885a1c9b8f1ed71848856137a92fe --- /dev/null +++ b/pkg/templates/middleware/recovery @@ -0,0 +1,38 @@ +import ( + "go.uber.org/zap" +) + +{{ $funcName := (or .Vars.FuncName ("RecoveringMiddleware")) }} +{{ $decorator := (or .Vars.DecoratorName ("recoveringMiddleware")) }} + +// {{$decorator}} implements {{.Interface.Type}} that is instrumented with logging +type {{$decorator}} struct { + logger *zap.Logger + next {{.Interface.Type}} +} + +// {{$funcName}} instruments an implementation of the {{.Interface.Type}} with simple logging +func {{$funcName}}(logger *zap.Logger) Middleware { + return func(next {{.Interface.Type}}) {{.Interface.Type}} { + return &{{$decorator}}{ + next: next, + logger: logger, + } + } +} + +{{range $method := .Interface.Methods}} +func (m *{{$decorator}}) {{$method.Declaration}} { + logger := m.logger + defer func() { + if r := recover(); r != nil { + logger.Error("panic", zap.Error(fmt.Errorf("%v", r))) + {{- if $method.ReturnsError}} + err = fmt.Errorf("%v", r) + {{end -}} + } + }() + + {{ $method.Pass "m.next." }} +} +{{end}} \ No newline at end of file