diff --git a/pkg/invitations/middleware/caching_middleware.go b/pkg/invitations/middleware/caching_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..97a1bcb3367fa0cf0ab0f1bc7c638191cb72911c --- /dev/null +++ b/pkg/invitations/middleware/caching_middleware.go @@ -0,0 +1,62 @@ +package service + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/cache" + service "git.perx.ru/perxis/perxis-go/pkg/invitations" + services "git.perx.ru/perxis/perxis-go/pkg/options" +) + +func CachingMiddleware(cache *cache.Cache) Middleware { + return func(next service.Invitations) service.Invitations { + return &cachingMiddleware{ + cache: cache, + next: next, + } + } +} + +type cachingMiddleware struct { + cache *cache.Cache + next service.Invitations +} + +func (m cachingMiddleware) Create(ctx context.Context, invitation *service.Invitation) (inv *service.Invitation, err error) { + return m.next.Create(ctx, invitation) +} + +func (m cachingMiddleware) Get(ctx context.Context, invitationId string) (inv *service.Invitation, err error) { + + value, e := m.cache.Get(invitationId) + if e == nil { + return value.(*service.Invitation), err + } + inv, err = m.next.Get(ctx, invitationId) + if err == nil { + m.cache.Set(invitationId, inv) + } + return inv, err +} + +func (m cachingMiddleware) Accept(ctx context.Context, invitationId string, userId string) (err error) { + + err = m.next.Accept(ctx, invitationId, userId) + if err == nil { + m.cache.Remove(invitationId) + } + return err +} + +func (m cachingMiddleware) Find(ctx context.Context, filter *service.Filter, opts *services.FindOptions) (invitations []*service.Invitation, total int, err error) { + return m.next.Find(ctx, filter, opts) +} + +func (m cachingMiddleware) Delete(ctx context.Context, invitationId string) (err error) { + + err = m.next.Delete(ctx, invitationId) + if err == nil { + m.cache.Remove(invitationId) + } + return err +} diff --git a/pkg/invitations/middleware/caching_middleware_test.go b/pkg/invitations/middleware/caching_middleware_test.go new file mode 100644 index 0000000000000000000000000000000000000000..852c7ddb8f1c7f03ef494fe5d31c8091aaff385a --- /dev/null +++ b/pkg/invitations/middleware/caching_middleware_test.go @@ -0,0 +1,126 @@ +package service + +import ( + "context" + "testing" + "time" + + "git.perx.ru/perxis/perxis-go/pkg/cache" + "git.perx.ru/perxis/perxis-go/pkg/invitations" + invmocks "git.perx.ru/perxis/perxis-go/pkg/invitations/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestLocalesCache(t *testing.T) { + + const ( + orgID = "orgID" + email = "123@321.ru" + invID = "invID" + usrID = "usrID" + size = 5 + ttl = 20 * time.Millisecond + ) + + ctx := context.Background() + + t.Run("Get from Cache", func(t *testing.T) { + inv := &invmocks.Invitations{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(inv) + + inv.On("Get", mock.Anything, invID).Return(&invitations.Invitation{ID: invID, Email: email, OrgID: orgID}, nil).Once() + + v1, err := svc.Get(ctx, invID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, invID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается что при повторном запросе объект будет получен из кэша.") + + inv.AssertExpectations(t) + }) + + t.Run("Invalidate cache", func(t *testing.T) { + t.Run("Get from Accept", func(t *testing.T) { + inv := &invmocks.Invitations{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(inv) + + inv.On("Get", mock.Anything, invID).Return(&invitations.Invitation{ID: invID, Email: email, OrgID: orgID}, nil).Once() + + v1, err := svc.Get(ctx, invID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, invID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается что при повторном запросе объект будет получен из кэша.") + + inv.On("Accept", mock.Anything, invID, usrID).Return(nil).Once() + inv.On("Get", mock.Anything, invID).Return(nil, services.ErrNotFound).Once() + + err = svc.Accept(ctx, invID, usrID) + require.NoError(t, err) + + _, err = svc.Get(ctx, invID) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидается что после подтверждения объект будет удален из кэша и получена ошибка от сервиса.") + + inv.AssertExpectations(t) + }) + + t.Run("Get from Delete", func(t *testing.T) { + inv := &invmocks.Invitations{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(inv) + + inv.On("Get", mock.Anything, invID).Return(&invitations.Invitation{ID: invID, Email: email, OrgID: orgID}, nil).Once() + + v1, err := svc.Get(ctx, invID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, invID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается что при повторном запросе объект будет получен из кэша.") + + inv.On("Delete", mock.Anything, invID).Return(nil).Once() + inv.On("Get", mock.Anything, invID).Return(nil, services.ErrNotFound).Once() + + err = svc.Delete(ctx, invID) + require.NoError(t, err) + + _, err = svc.Get(ctx, invID) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидается что после удаления кэша будет очищен и получена ошибка от сервиса.") + + inv.AssertExpectations(t) + }) + + t.Run("After TTL expired", func(t *testing.T) { + inv := &invmocks.Invitations{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(inv) + + inv.On("Get", mock.Anything, invID).Return(&invitations.Invitation{ID: invID, Email: email, OrgID: orgID}, nil).Once() + + v1, err := svc.Get(ctx, invID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, invID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается что при повторном запросе объект будет получен из кэша.") + + time.Sleep(2 * ttl) + + inv.On("Get", mock.Anything, invID).Return(&invitations.Invitation{ID: invID, Email: email, OrgID: orgID}, nil).Once() + + v3, err := svc.Get(ctx, invID) + require.NoError(t, err) + assert.NotSame(t, v2, v3, "Ожидается что при истечении ttl кеш будет очищен..") + + inv.AssertExpectations(t) + }) + }) +} diff --git a/pkg/invitations/middleware/error_logging_middleware.go b/pkg/invitations/middleware/error_logging_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..b83d054478cafa8a5dbf3ab823adf0715dae90bc --- /dev/null +++ b/pkg/invitations/middleware/error_logging_middleware.go @@ -0,0 +1,81 @@ +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/error_log +// gowrap: http://github.com/hexdigest/gowrap + +package service + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/invitations -i Invitations -t ../../../assets/templates/middleware/error_log -o error_logging_middleware.go -l "" + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/invitations" + "git.perx.ru/perxis/perxis-go/pkg/options" + "go.uber.org/zap" +) + +// errorLoggingMiddleware implements invitations.Invitations that is instrumented with logging +type errorLoggingMiddleware struct { + logger *zap.Logger + next invitations.Invitations +} + +// ErrorLoggingMiddleware instruments an implementation of the invitations.Invitations with simple logging +func ErrorLoggingMiddleware(logger *zap.Logger) Middleware { + return func(next invitations.Invitations) invitations.Invitations { + return &errorLoggingMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *errorLoggingMiddleware) Accept(ctx context.Context, invitationId string, userId string) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Accept(ctx, invitationId, userId) +} + +func (m *errorLoggingMiddleware) Create(ctx context.Context, invitation *invitations.Invitation) (created *invitations.Invitation, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Create(ctx, invitation) +} + +func (m *errorLoggingMiddleware) Delete(ctx context.Context, invitationId string) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Delete(ctx, invitationId) +} + +func (m *errorLoggingMiddleware) Find(ctx context.Context, filter *invitations.Filter, opts *options.FindOptions) (invitations []*invitations.Invitation, total int, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Find(ctx, filter, opts) +} + +func (m *errorLoggingMiddleware) Get(ctx context.Context, invitationId string) (invitation *invitations.Invitation, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Get(ctx, invitationId) +} diff --git a/pkg/invitations/middleware/logging_middleware.go b/pkg/invitations/middleware/logging_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..d0e4d9c4c4d9e51c2d219112bcfce644475e7d88 --- /dev/null +++ b/pkg/invitations/middleware/logging_middleware.go @@ -0,0 +1,216 @@ +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/access_log +// gowrap: http://github.com/hexdigest/gowrap + +package service + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/invitations -i Invitations -t ../../../assets/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/invitations" + "git.perx.ru/perxis/perxis-go/pkg/options" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// loggingMiddleware implements invitations.Invitations that is instrumented with logging +type loggingMiddleware struct { + logger *zap.Logger + next invitations.Invitations +} + +// LoggingMiddleware instruments an implementation of the invitations.Invitations with simple logging +func LoggingMiddleware(logger *zap.Logger) Middleware { + return func(next invitations.Invitations) invitations.Invitations { + return &loggingMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *loggingMiddleware) Accept(ctx context.Context, invitationId string, userId string) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "invitationId": invitationId, + "userId": userId} { + 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("Accept.Request", fields...) + + err = m.next.Accept(ctx, invitationId, userId) + + 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("Accept.Response", fields...) + + return err +} + +func (m *loggingMiddleware) Create(ctx context.Context, invitation *invitations.Invitation) (created *invitations.Invitation, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "invitation": invitation} { + 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, invitation) + + 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, invitationId string) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "invitationId": invitationId} { + 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, invitationId) + + 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) Find(ctx context.Context, filter *invitations.Filter, opts *options.FindOptions) (invitations []*invitations.Invitation, total int, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "filter": filter, + "opts": opts} { + 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("Find.Request", fields...) + + invitations, total, err = m.next.Find(ctx, filter, opts) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "invitations": invitations, + "total": total, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Find.Response", fields...) + + return invitations, total, err +} + +func (m *loggingMiddleware) Get(ctx context.Context, invitationId string) (invitation *invitations.Invitation, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "invitationId": invitationId} { + 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...) + + invitation, err = m.next.Get(ctx, invitationId) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "invitation": invitation, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Get.Response", fields...) + + return invitation, err +} diff --git a/pkg/invitations/middleware/middleware.go b/pkg/invitations/middleware/middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..1c054d8ae96ab5d45f1dd83af9bb440b3d429817 --- /dev/null +++ b/pkg/invitations/middleware/middleware.go @@ -0,0 +1,28 @@ +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/middleware +// gowrap: http://github.com/hexdigest/gowrap + +package service + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/invitations -i Invitations -t ../../../assets/templates/middleware/middleware -o middleware.go -l "" + +import ( + "git.perx.ru/perxis/perxis-go/pkg/invitations" + "go.uber.org/zap" +) + +type Middleware func(invitations.Invitations) invitations.Invitations + +func WithLog(s invitations.Invitations, logger *zap.Logger, log_access bool) invitations.Invitations { + if logger == nil { + logger = zap.NewNop() + } + + logger = logger.Named("Invitations") + s = ErrorLoggingMiddleware(logger)(s) + if log_access { + s = LoggingMiddleware(logger)(s) + } + s = RecoveringMiddleware(logger)(s) + return s +} diff --git a/pkg/invitations/middleware/recovering_middleware.go b/pkg/invitations/middleware/recovering_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..195933ecce0d9766d8a92bbe1e52df6dc4d7064e --- /dev/null +++ b/pkg/invitations/middleware/recovering_middleware.go @@ -0,0 +1,92 @@ +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/recovery +// gowrap: http://github.com/hexdigest/gowrap + +package service + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/invitations -i Invitations -t ../../../assets/templates/middleware/recovery -o recovering_middleware.go -l "" + +import ( + "context" + "fmt" + + "git.perx.ru/perxis/perxis-go/pkg/invitations" + "git.perx.ru/perxis/perxis-go/pkg/options" + "go.uber.org/zap" +) + +// recoveringMiddleware implements invitations.Invitations that is instrumented with logging +type recoveringMiddleware struct { + logger *zap.Logger + next invitations.Invitations +} + +// RecoveringMiddleware instruments an implementation of the invitations.Invitations with simple logging +func RecoveringMiddleware(logger *zap.Logger) Middleware { + return func(next invitations.Invitations) invitations.Invitations { + return &recoveringMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *recoveringMiddleware) Accept(ctx context.Context, invitationId string, userId 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.Accept(ctx, invitationId, userId) +} + +func (m *recoveringMiddleware) Create(ctx context.Context, invitation *invitations.Invitation) (created *invitations.Invitation, 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, invitation) +} + +func (m *recoveringMiddleware) Delete(ctx context.Context, invitationId 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, invitationId) +} + +func (m *recoveringMiddleware) Find(ctx context.Context, filter *invitations.Filter, opts *options.FindOptions) (invitations []*invitations.Invitation, total int, 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.Find(ctx, filter, opts) +} + +func (m *recoveringMiddleware) Get(ctx context.Context, invitationId string) (invitation *invitations.Invitation, 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, invitationId) +}