diff --git a/pkg/locales/middleware/caching_middleware.go b/pkg/locales/middleware/caching_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..8b4635ab4a0214f99407b67020737724ebf3c841 --- /dev/null +++ b/pkg/locales/middleware/caching_middleware.go @@ -0,0 +1,53 @@ +package service + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/cache" + service "git.perx.ru/perxis/perxis-go/pkg/locales" +) + +func CachingMiddleware(cache *cache.Cache) Middleware { + return func(next service.Locales) service.Locales { + return &cachingMiddleware{ + cache: cache, + next: next, + } + } +} + +type cachingMiddleware struct { + cache *cache.Cache + next service.Locales +} + +func (m cachingMiddleware) Create(ctx context.Context, locale *service.Locale) (loc *service.Locale, err error) { + + loc, err = m.next.Create(ctx, locale) + if err == nil { + m.cache.Remove(loc.SpaceID) + } + return loc, err +} + +func (m cachingMiddleware) List(ctx context.Context, spaceId string) (locales []*service.Locale, err error) { + + value, e := m.cache.Get(spaceId) + if e == nil { + return value.([]*service.Locale), err + } + locales, err = m.next.List(ctx, spaceId) + if err == nil { + m.cache.Set(spaceId, locales) + } + return locales, err +} + +func (m cachingMiddleware) Delete(ctx context.Context, spaceId string, localeId string) (err error) { + + err = m.next.Delete(ctx, spaceId, localeId) + if err == nil { + m.cache.Remove(spaceId) + } + return err +} diff --git a/pkg/locales/middleware/caching_middleware_test.go b/pkg/locales/middleware/caching_middleware_test.go new file mode 100644 index 0000000000000000000000000000000000000000..de5e7a9f742b6336ecd5a67d529ed468236838f4 --- /dev/null +++ b/pkg/locales/middleware/caching_middleware_test.go @@ -0,0 +1,130 @@ +package service + +import ( + "context" + "testing" + "time" + + "git.perx.ru/perxis/perxis-go/pkg/cache" + "git.perx.ru/perxis/perxis-go/pkg/locales" + locmocks "git.perx.ru/perxis/perxis-go/pkg/locales/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestLocalesCache(t *testing.T) { + + const ( + loc1 = "loc1" + loc2 = "loc2" + spaceID = "spaceID" + size = 5 + ttl = 20 * time.Millisecond + ) + + ctx := context.Background() + + t.Run("List from Cache", func(t *testing.T) { + loc := &locmocks.Locales{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(loc) + + loc.On("List", mock.Anything, spaceID).Return([]*locales.Locale{{ID: loc1, Name: "name1", SpaceID: spaceID}}, 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], "Ожидается что при повторном запросе объекты будут получены из кэша.") + + loc.AssertExpectations(t) + }) + + t.Run("Invalidate cache", func(t *testing.T) { + t.Run("After Delete", func(t *testing.T) { + loc := &locmocks.Locales{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(loc) + + loc.On("List", mock.Anything, spaceID).Return([]*locales.Locale{{ID: loc1, Name: "name1", SpaceID: spaceID}}, 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], "Ожидается что при повторном запросе объекты будут получены из кэша.") + + loc.On("Delete", mock.Anything, spaceID, loc1).Return(nil).Once() + + err = svc.Delete(ctx, spaceID, loc1) + require.NoError(t, err) + + loc.On("List", mock.Anything, spaceID).Return([]*locales.Locale{}, nil).Once() + + vl3, err := svc.List(ctx, spaceID) + require.NoError(t, err) + assert.Len(t, vl3, 0, "Ожидается что после удаление объекты будут удалены из кеша.") + + loc.AssertExpectations(t) + }) + + t.Run("After Create", func(t *testing.T) { + loc := &locmocks.Locales{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(loc) + + loc.On("List", mock.Anything, spaceID).Return([]*locales.Locale{{ID: loc1, Name: "name1", SpaceID: spaceID}}, 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], "Ожидается что при повторном запросе объекты будут получены из кэша.") + + loc.On("Create", mock.Anything, mock.Anything).Return(&locales.Locale{ID: loc2, Name: "name2", SpaceID: spaceID}, nil).Once() + + _, err = svc.Create(ctx, &locales.Locale{ID: loc2, Name: "name2", SpaceID: spaceID}) + require.NoError(t, err) + + loc.On("List", mock.Anything, spaceID). + Return([]*locales.Locale{ + {ID: loc1, Name: "name1", SpaceID: spaceID}, + {ID: loc2, Name: "name2", SpaceID: spaceID}, + }, nil).Once() + + vl3, err := svc.List(ctx, spaceID) + require.NoError(t, err) + assert.Len(t, vl3, 2, "Ожидается что после создания нового объекта данные будут удалены из кеша и получены из сервиса.") + + loc.AssertExpectations(t) + }) + + t.Run("After TTL expired", func(t *testing.T) { + loc := &locmocks.Locales{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(loc) + + loc.On("List", mock.Anything, spaceID).Return([]*locales.Locale{{ID: loc1, Name: "name1", SpaceID: spaceID}}, 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], "Ожидается что при повторном запросе объекты будут получены из кэша.") + + time.Sleep(2 * ttl) + loc.On("List", mock.Anything, spaceID).Return([]*locales.Locale{{ID: loc1, Name: "name1", SpaceID: spaceID}}, nil).Once() + + vl3, err := svc.List(ctx, spaceID) + require.NoError(t, err) + assert.NotSame(t, vl2[0], vl3[0], "Ожидается что элементы будут получены из кэша.") + + loc.AssertExpectations(t) + }) + }) +} diff --git a/pkg/locales/middleware/error_logging_middleware.go b/pkg/locales/middleware/error_logging_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..171482568af8d1988bb1e2e6625884e78683c71e --- /dev/null +++ b/pkg/locales/middleware/error_logging_middleware.go @@ -0,0 +1,60 @@ +// 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/locales -i Locales -t ../../../assets/templates/middleware/error_log -o error_logging_middleware.go -l "" + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/locales" + "go.uber.org/zap" +) + +// errorLoggingMiddleware implements locales.Locales that is instrumented with logging +type errorLoggingMiddleware struct { + logger *zap.Logger + next locales.Locales +} + +// ErrorLoggingMiddleware instruments an implementation of the locales.Locales with simple logging +func ErrorLoggingMiddleware(logger *zap.Logger) Middleware { + return func(next locales.Locales) locales.Locales { + return &errorLoggingMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *errorLoggingMiddleware) Create(ctx context.Context, locale *locales.Locale) (created *locales.Locale, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Create(ctx, locale) +} + +func (m *errorLoggingMiddleware) Delete(ctx context.Context, spaceId string, localeId string) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Delete(ctx, spaceId, localeId) +} + +func (m *errorLoggingMiddleware) List(ctx context.Context, spaceId string) (locales []*locales.Locale, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.List(ctx, spaceId) +} diff --git a/pkg/locales/middleware/logging_middleware.go b/pkg/locales/middleware/logging_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..7e9a16b14ba1b2c619271d746245de4f1ddbcaae --- /dev/null +++ b/pkg/locales/middleware/logging_middleware.go @@ -0,0 +1,142 @@ +// 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/locales -i Locales -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/locales" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// loggingMiddleware implements locales.Locales that is instrumented with logging +type loggingMiddleware struct { + logger *zap.Logger + next locales.Locales +} + +// LoggingMiddleware instruments an implementation of the locales.Locales with simple logging +func LoggingMiddleware(logger *zap.Logger) Middleware { + return func(next locales.Locales) locales.Locales { + return &loggingMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *loggingMiddleware) Create(ctx context.Context, locale *locales.Locale) (created *locales.Locale, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "locale": locale} { + 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, locale) + + 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, localeId string) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "localeId": localeId} { + 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, localeId) + + 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) List(ctx context.Context, spaceId string) (locales []*locales.Locale, 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...) + + locales, 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{}{ + "locales": locales, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("List.Response", fields...) + + return locales, err +} diff --git a/pkg/locales/middleware/middleware.go b/pkg/locales/middleware/middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..726b535247256a109f0fc5aa100e0a61cc928555 --- /dev/null +++ b/pkg/locales/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/locales -i Locales -t ../../../assets/templates/middleware/middleware -o middleware.go -l "" + +import ( + "git.perx.ru/perxis/perxis-go/pkg/locales" + "go.uber.org/zap" +) + +type Middleware func(locales.Locales) locales.Locales + +func WithLog(s locales.Locales, logger *zap.Logger, log_access bool) locales.Locales { + if logger == nil { + logger = zap.NewNop() + } + + logger = logger.Named("Locales") + s = ErrorLoggingMiddleware(logger)(s) + if log_access { + s = LoggingMiddleware(logger)(s) + } + s = RecoveringMiddleware(logger)(s) + return s +} diff --git a/pkg/locales/middleware/recovering_middleware.go b/pkg/locales/middleware/recovering_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..44b198550418034e1963fec5ccbd405e0ea12ef4 --- /dev/null +++ b/pkg/locales/middleware/recovering_middleware.go @@ -0,0 +1,67 @@ +// 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/locales -i Locales -t ../../../assets/templates/middleware/recovery -o recovering_middleware.go -l "" + +import ( + "context" + "fmt" + + "git.perx.ru/perxis/perxis-go/pkg/locales" + "go.uber.org/zap" +) + +// recoveringMiddleware implements locales.Locales that is instrumented with logging +type recoveringMiddleware struct { + logger *zap.Logger + next locales.Locales +} + +// RecoveringMiddleware instruments an implementation of the locales.Locales with simple logging +func RecoveringMiddleware(logger *zap.Logger) Middleware { + return func(next locales.Locales) locales.Locales { + return &recoveringMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *recoveringMiddleware) Create(ctx context.Context, locale *locales.Locale) (created *locales.Locale, 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, locale) +} + +func (m *recoveringMiddleware) Delete(ctx context.Context, spaceId string, localeId 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, localeId) +} + +func (m *recoveringMiddleware) List(ctx context.Context, spaceId string) (locales []*locales.Locale, 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) +}