diff --git a/Makefile b/Makefile index edffb61feca4e592fbd263981a3ebcf5ea682477..9a64367046d3ac93fdf4ec05437cfb15ffc43777 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,7 @@ +.PHONY: proto logging mocks .FORCE + +SHELL = bash + PROTODIR=perxis-proto/proto DSTDIR=./proto ALLPROTO?=$(shell find $(PROTODIR) -name '*.proto' ) @@ -6,6 +10,10 @@ PROTOFILES= $(filter-out $(PROTODIR)/status/status.proto, $(ALLPROTO)) PROTOGOFILES=$(PROTOFILES:.proto=.pb.go) PROTOGOGRPCFILES=$(PROTOFILES:.proto=_grpc.pb.go) +PKGDIR=pkg +ACCESSLOGGING=$(shell find $(PKGDIR) -name "logging_middleware.go" -type f) +ERRORLOGGING=$(shell find $(PKGDIR) -name "error_logging_middleware.go" -type f) + # Генерация grpc-клиентов для go proto: protoc-check protoc-gen-go-check $(PROTOGOFILES) @echo "Generated all protobuf Go files" @@ -33,11 +41,18 @@ ifeq (,$(wildcard $(GOPATH)/bin/protoc-gen-go)) or visit \"https://github.com/golang/protobuf/tree/v1.3.2#installation\" for more.\n") endif +# Генерация логгирования (access & error) для всех сервисов. Предполагается наличие файлов `logging_middleware.go/error_middleware.go` +# СЃ директивой go:generate Рё командой генерации РєРѕРґР° РІ директориях `/pkg` сервисов +# Для установки инструмента генерации выполнить команду `go get -u github.com/hexdigest/gowrap/cmd/gowrap` +logging: $(ERRORLOGGING) $(ACCESSLOGGING) +%/middleware/logging_middleware.go: .FORCE + @echo "$@" + @go generate "$@" -#MICROGENFILES?=$(shell find $(SERVICESDIR) -name "service.go" -exec grep -Ril "microgen" {} \;) -#SERVICEDIRS?=$(shell find $(SERVICESDIR) -name "service" -type d -exec dirname {} \;) -#SERVICEFILES?=$(shell find $(SERVICESDIR) -name "service.go" -exec grep -Ril "go:generate" {} \;) +%/middleware/error_logging_middleware.go: .FORCE + @echo "$@" + @go generate "$@" # Генерация РјРѕРєРѕРІ для всех интерфейсов, найденных РІ директории. Выходные файлы СЃ моками сохраняются РІ `./mocks` MOCKSDIRS?=$(shell find . -name "service.go" -exec dirname {} \;) diff --git a/assets/templates/middleware/access_log b/assets/templates/middleware/access_log new file mode 100644 index 0000000000000000000000000000000000000000..a8587b82d5a72130690a61c81e9f78e5eeb6e726 --- /dev/null +++ b/assets/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/assets/templates/middleware/error_log b/assets/templates/middleware/error_log new file mode 100755 index 0000000000000000000000000000000000000000..9455e907b738801eb7f2d43d428d98cc620370a0 --- /dev/null +++ b/assets/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/assets/templates/middleware/middleware b/assets/templates/middleware/middleware new file mode 100755 index 0000000000000000000000000000000000000000..89877774c933840c2bdd569f2beed8105588aae2 --- /dev/null +++ b/assets/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/assets/templates/middleware/recovery b/assets/templates/middleware/recovery new file mode 100644 index 0000000000000000000000000000000000000000..a84fa3f913e885a1c9b8f1ed71848856137a92fe --- /dev/null +++ b/assets/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 diff --git a/go.mod b/go.mod index 05e37d08d81a77ffc8319f1094590b4ec4f236ab..c4bc532e1b85714da9e5a283f5b60f65748fa1f5 100644 --- a/go.mod +++ b/go.mod @@ -17,28 +17,38 @@ require ( github.com/stretchr/testify v1.8.0 go.mongodb.org/mongo-driver v1.11.4 go.uber.org/zap v1.19.1 - golang.org/x/crypto v0.5.0 - golang.org/x/net v0.5.0 - google.golang.org/grpc v1.45.0 - google.golang.org/protobuf v1.28.0 + golang.org/x/crypto v0.8.0 + golang.org/x/net v0.9.0 + google.golang.org/grpc v1.54.0 + google.golang.org/protobuf v1.28.1 gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.2.1 // indirect + github.com/Masterminds/sprig/v3 v3.2.3 // indirect 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/golang/snappy v0.0.1 // indirect - github.com/google/go-cmp v0.5.7 // indirect + github.com/google/uuid v1.3.0 // indirect github.com/gosimple/unidecode v1.0.1 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hexdigest/gowrap v1.3.2 // indirect + github.com/huandu/xstrings v1.4.0 // indirect + github.com/imdario/mergo v0.3.15 // indirect github.com/klauspost/compress v1.13.6 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect github.com/nats-io/nkeys v0.3.0 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/shopspring/decimal v1.3.1 // indirect + github.com/spf13/cast v1.5.0 // indirect github.com/stretchr/objx v0.4.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.1 // indirect @@ -46,8 +56,10 @@ require ( github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // 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/genproto v0.0.0-20210917145530-b395a37504d4 // indirect + golang.org/x/mod v0.10.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.7.0 // indirect + golang.org/x/text v0.9.0 // indirect + golang.org/x/tools v0.8.0 // indirect + google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect ) diff --git a/go.sum b/go.sum index 1b2e49b505ab646f23e970362f978a04fae0a2b4..53493f56d5ddbe17ec5350541663552f0dcb311c 100644 --- a/go.sum +++ b/go.sum @@ -1,86 +1,59 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= +github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/antonmedv/expr v1.9.0 h1:j4HI3NHEdgDnN9p6oI6Ndr0G5QryMY0FNxT4ONrFDGU= github.com/antonmedv/expr v1.9.0/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmHhwGEk8= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-kit/kit v0.12.0 h1:e4o3o3IsBfAKQh5Qbbiqyfu97Ku7jrO/JbohvztANh4= github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs= 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/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= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gosimple/slug v1.13.1 h1:bQ+kpX9Qa6tHRaK+fZR0A0M2Kd7Pa5eHPPsb1JpHD+Q= github.com/gosimple/slug v1.13.1/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hexdigest/gowrap v1.3.2 h1:ZDhDFhrbAHYRdt9ZnULKZyggC/3+W9EpfX6R8DjlggY= +github.com/hexdigest/gowrap v1.3.2/go.mod h1:g8N2jI4n9AKrf843erksNTrt4sdkG+TGVfhWe8dWrJQ= +github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= +github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= +github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= @@ -100,8 +73,14 @@ github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo= github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -123,18 +102,22 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rivo/tview v0.0.0-20200219210816-cd38d7432498/go.mod h1:6lkG1x+13OShEf0EaOCaTQYyB7d5nSbb181KtjlS+84= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/sanity-io/litter v1.2.0/go.mod h1:JF6pZUFgu2Q0sBZ+HSV35P8TVPI1TTzEwyu9FXAw2W4= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -153,9 +136,9 @@ github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgk github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 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= -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= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -168,121 +151,94 @@ go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= +golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 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= -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= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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= -golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 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= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y= +golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4 h1:ysnBoUyeL/H6RCvNRhWHjKoDEmguI+mPU+qHgK8qv/w= -google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.45.0 h1:NEpgUqV3Z+ZjkqMsxMg11IaDrXY4RY6CQukSGK0uI1M= -google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w= +google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag= +google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/pkg/clients/middleware/caching_middleware.go b/pkg/clients/middleware/caching_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..1733c0de4a8dde8f011d46b2b77005f8924e60bb --- /dev/null +++ b/pkg/clients/middleware/caching_middleware.go @@ -0,0 +1,165 @@ +package middleware + +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) { + + 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) { + + 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) + } + + 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) { + + 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) { + + 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) { + + 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) { + + 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/middleware/caching_middleware_test.go b/pkg/clients/middleware/caching_middleware_test.go new file mode 100644 index 0000000000000000000000000000000000000000..b842e1183a5425a4b3aec8d59b0720e5627767ed --- /dev/null +++ b/pkg/clients/middleware/caching_middleware_test.go @@ -0,0 +1,382 @@ +package middleware + +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" + "git.perx.ru/perxis/perxis-go/pkg/errors" + "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 + ) + + errNotFound := errors.NotFound(errors.New("not found")) + + 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, errNotFound).Once() + cs.On("GetBy", mock.Anything, spaceID, &clients.GetByParams{OAuthClientID: clientID}).Return(nil, errNotFound).Once() + cs.On("List", mock.Anything, spaceID).Return(nil, 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, 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/middleware/error_logging_middleware.go b/pkg/clients/middleware/error_logging_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..0b96827a0c620ad1ca1aa5aaf6b93a821af6279d --- /dev/null +++ b/pkg/clients/middleware/error_logging_middleware.go @@ -0,0 +1,100 @@ +package middleware + +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/error_log +// gowrap: http://github.com/hexdigest/gowrap + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/clients -i Clients -t ../../../assets/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/middleware/logging_middleware.go b/pkg/clients/middleware/logging_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..ef3ea5947a637bfa9caffa3af64f1f7cd2b8e019 --- /dev/null +++ b/pkg/clients/middleware/logging_middleware.go @@ -0,0 +1,288 @@ +package middleware + +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/access_log +// gowrap: http://github.com/hexdigest/gowrap + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/clients -i Clients -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/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/middleware/middleware.go b/pkg/clients/middleware/middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..a49c9b3ebb0c041c23178d457ea5ddf2d2357d91 --- /dev/null +++ b/pkg/clients/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 middleware + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/clients -i Clients -t ../../../assets/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/middleware/recovering_middleware.go b/pkg/clients/middleware/recovering_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..2406ca994112a148e6a204589ee1844a782922fc --- /dev/null +++ b/pkg/clients/middleware/recovering_middleware.go @@ -0,0 +1,115 @@ +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/recovery +// gowrap: http://github.com/hexdigest/gowrap + +package middleware + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/clients -i Clients -t ../../../assets/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/collaborators/middleware/caching_middleware.go b/pkg/collaborators/middleware/caching_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..f57995acf8387c80edc622313ba4729ae11bc6f6 --- /dev/null +++ b/pkg/collaborators/middleware/caching_middleware.go @@ -0,0 +1,88 @@ +package service + +import ( + "context" + "strings" + + "git.perx.ru/perxis/perxis-go/pkg/cache" + service "git.perx.ru/perxis/perxis-go/pkg/collaborators" +) + +func makeKey(ss ...string) string { + return strings.Join(ss, "-") +} + +func CachingMiddleware(cache *cache.Cache) Middleware { + return func(next service.Collaborators) service.Collaborators { + return &cachingMiddleware{ + cache: cache, + next: next, + } + } +} + +type cachingMiddleware struct { + cache *cache.Cache + next service.Collaborators +} + +func (m cachingMiddleware) Set(ctx context.Context, spaceId, subject, role string) (err error) { + + err = m.next.Set(ctx, spaceId, subject, role) + if err == nil { + m.cache.Remove(spaceId) + m.cache.Remove(subject) + } + return err +} + +func (m cachingMiddleware) Get(ctx context.Context, spaceId, subject string) (role string, err error) { + + key := makeKey(spaceId, subject) + value, e := m.cache.Get(key) + if e == nil { + return value.(string), err + } + role, err = m.next.Get(ctx, spaceId, subject) + if err == nil { + m.cache.Set(key, role) + } + return role, err +} + +func (m cachingMiddleware) Remove(ctx context.Context, spaceId, subject string) (err error) { + + err = m.next.Remove(ctx, spaceId, subject) + if err == nil { + m.cache.Remove(makeKey(spaceId, subject)) + m.cache.Remove(spaceId) + m.cache.Remove(subject) + } + return err +} + +func (m cachingMiddleware) ListCollaborators(ctx context.Context, spaceId string) (collaborators []*service.Collaborator, err error) { + + value, e := m.cache.Get(spaceId) + if e == nil { + return value.([]*service.Collaborator), err + } + collaborators, err = m.next.ListCollaborators(ctx, spaceId) + if err == nil { + m.cache.Set(spaceId, collaborators) + } + return collaborators, err +} + +func (m cachingMiddleware) ListSpaces(ctx context.Context, subject string) (collaborators []*service.Collaborator, err error) { + + value, e := m.cache.Get(subject) + if e == nil { + return value.([]*service.Collaborator), err + } + collaborators, err = m.next.ListSpaces(ctx, subject) + if err == nil { + m.cache.Set(subject, collaborators) + } + return collaborators, err +} diff --git a/pkg/collaborators/middleware/caching_middleware_test.go b/pkg/collaborators/middleware/caching_middleware_test.go new file mode 100644 index 0000000000000000000000000000000000000000..da1d6e842f542e1c664f988f38c4797ea31c38f1 --- /dev/null +++ b/pkg/collaborators/middleware/caching_middleware_test.go @@ -0,0 +1,190 @@ +package service + +import ( + "context" + "testing" + "time" + + "git.perx.ru/perxis/perxis-go/pkg/cache" + "git.perx.ru/perxis/perxis-go/pkg/collaborators" + csmocks "git.perx.ru/perxis/perxis-go/pkg/collaborators/mocks" + "git.perx.ru/perxis/perxis-go/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestCollaboratorsCache(t *testing.T) { + + const ( + userID = "userID" + spaceID = "spaceID" + spaceRole = "spaceRole" + size = 5 + ttl = 20 * time.Millisecond + ) + + errNotFound := errors.NotFound(errors.New("not found")) + + ctx := context.Background() + + t.Run("Get from cache", func(t *testing.T) { + cs := &csmocks.Collaborators{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(cs) + + cs.On("Get", mock.Anything, spaceID, userID).Return(spaceRole, nil).Once() + + _, err := svc.Get(ctx, spaceID, userID) + require.NoError(t, err) + + rl, err := svc.Get(ctx, spaceID, userID) + require.NoError(t, err) + assert.Equal(t, spaceRole, rl) + + cs.AssertExpectations(t) + }) + + t.Run("ListCollaborators from cache", func(t *testing.T) { + cs := &csmocks.Collaborators{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(cs) + + cs.On("ListCollaborators", mock.Anything, spaceID).Return([]*collaborators.Collaborator{{SpaceID: spaceID, Subject: userID, Role: spaceRole}}, nil).Once() + + v1, err := svc.ListCollaborators(ctx, spaceID) + require.NoError(t, err) + v2, err := svc.ListCollaborators(ctx, spaceID) + require.NoError(t, err) + assert.Same(t, v1[0], v2[0], "Ожидается получение объектов РёР· кэша РїСЂРё повторном запросе.") + + cs.AssertExpectations(t) + }) + + t.Run("ListSpaces from cache", func(t *testing.T) { + cs := &csmocks.Collaborators{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(cs) + + cs.On("ListSpaces", mock.Anything, userID).Return([]*collaborators.Collaborator{{SpaceID: spaceID, Subject: userID, Role: spaceRole}}, nil).Once() + + v1, err := svc.ListSpaces(ctx, userID) + require.NoError(t, err) + v2, err := svc.ListSpaces(ctx, userID) + require.NoError(t, err) + assert.Same(t, v1[0], v2[0], "Ожидается получение объектов РёР· кэша РїСЂРё повторном запросе.") + + cs.AssertExpectations(t) + }) + + t.Run("Invalidate cache", func(t *testing.T) { + t.Run("After Remove", func(t *testing.T) { + cs := &csmocks.Collaborators{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(cs) + + cs.On("Get", mock.Anything, spaceID, userID).Return(spaceRole, nil).Once() + cs.On("ListCollaborators", mock.Anything, spaceID).Return([]*collaborators.Collaborator{{SpaceID: spaceID, Subject: userID, Role: spaceRole}}, nil).Once() + cs.On("ListSpaces", mock.Anything, userID).Return([]*collaborators.Collaborator{{SpaceID: spaceID, Subject: userID, Role: spaceRole}}, nil).Once() + + _, err := svc.Get(ctx, spaceID, userID) + require.NoError(t, err) + + rl, err := svc.Get(ctx, spaceID, userID) + require.NoError(t, err) + assert.Equal(t, spaceRole, rl, "Ожидается получение данных РёР· кэша.") + + lc1, err := svc.ListCollaborators(ctx, spaceID) + require.NoError(t, err) + lc2, err := svc.ListCollaborators(ctx, spaceID) + require.NoError(t, err) + assert.Same(t, lc1[0], lc2[0], "Ожидается получение объектов РёР· кэша.") + + ls1, err := svc.ListSpaces(ctx, userID) + require.NoError(t, err) + ls2, err := svc.ListSpaces(ctx, userID) + require.NoError(t, err) + assert.Same(t, ls1[0], ls2[0], "Ожидается получение объектов РёР· кэша.") + + cs.On("Remove", mock.Anything, spaceID, userID).Return(nil).Once() + + cs.On("Get", mock.Anything, spaceID, userID).Return("", errNotFound).Once() + cs.On("ListCollaborators", mock.Anything, spaceID).Return(nil, errNotFound).Once() + cs.On("ListSpaces", mock.Anything, userID).Return(nil, errNotFound).Once() + + err = svc.Remove(ctx, spaceID, userID) + + rl, err = svc.Get(ctx, spaceID, userID) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидается удаление данных РёР· кеша, Рё получение ошибки РѕС‚ сервиса") + assert.Empty(t, rl) + + lc, err := svc.ListCollaborators(ctx, spaceID) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидается удаление данных РёР· кеша, Рё получение ошибки РѕС‚ сервиса") + assert.Nil(t, lc) + + ls, err := svc.ListSpaces(ctx, userID) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидается удаление данных РёР· кеша, Рё получение ошибки РѕС‚ сервиса") + assert.Nil(t, ls) + + cs.AssertExpectations(t) + }) + + t.Run("After TTL expired", func(t *testing.T) { + cs := &csmocks.Collaborators{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(cs) + + cs.On("Get", mock.Anything, spaceID, userID).Return(spaceRole, nil).Once() + cs.On("ListCollaborators", mock.Anything, spaceID).Return([]*collaborators.Collaborator{{SpaceID: spaceID, Subject: userID, Role: spaceRole}}, nil).Once() + cs.On("ListSpaces", mock.Anything, userID).Return([]*collaborators.Collaborator{{SpaceID: spaceID, Subject: userID, Role: spaceRole}}, nil).Once() + + _, err := svc.Get(ctx, spaceID, userID) + require.NoError(t, err) + + rl, err := svc.Get(ctx, spaceID, userID) + require.NoError(t, err) + assert.Equal(t, spaceRole, rl, "Ожидается получение данных РёР· кэша.") + + lc1, err := svc.ListCollaborators(ctx, spaceID) + require.NoError(t, err) + lc2, err := svc.ListCollaborators(ctx, spaceID) + require.NoError(t, err) + assert.Same(t, lc1[0], lc2[0], "Ожидается получение объектов РёР· кэша.") + + ls1, err := svc.ListSpaces(ctx, userID) + require.NoError(t, err) + ls2, err := svc.ListSpaces(ctx, userID) + require.NoError(t, err) + assert.Same(t, ls1[0], ls2[0], "Ожидается получение объектов РёР· кэша.") + + cs.On("Remove", mock.Anything, spaceID, userID).Return(nil).Once() + + cs.On("Get", mock.Anything, spaceID, userID).Return("", errNotFound).Once() + cs.On("ListCollaborators", mock.Anything, spaceID).Return(nil, errNotFound).Once() + cs.On("ListSpaces", mock.Anything, userID).Return(nil, errNotFound).Once() + + err = svc.Remove(ctx, spaceID, userID) + + rl, err = svc.Get(ctx, spaceID, userID) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидается удаление данных РёР· кеша, Рё получение ошибки РѕС‚ сервиса") + assert.Empty(t, rl) + + lc, err := svc.ListCollaborators(ctx, spaceID) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидается удаление данных РёР· кеша, Рё получение ошибки РѕС‚ сервиса") + assert.Nil(t, lc) + + ls, err := svc.ListSpaces(ctx, userID) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидается удаление данных РёР· кеша, Рё получение ошибки РѕС‚ сервиса") + assert.Nil(t, ls) + + cs.AssertExpectations(t) + }) + }) + +} diff --git a/pkg/collaborators/middleware/error_logging_middleware.go b/pkg/collaborators/middleware/error_logging_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..a45dfd8053e1718fdc139f1792a9f0b1547c08d1 --- /dev/null +++ b/pkg/collaborators/middleware/error_logging_middleware.go @@ -0,0 +1,80 @@ +package service + +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/error_log +// gowrap: http://github.com/hexdigest/gowrap + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/collaborators -i Collaborators -t ../../../assets/templates/middleware/error_log -o error_logging_middleware.go -l "" + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/collaborators" + "go.uber.org/zap" +) + +// errorLoggingMiddleware implements collaborators.Collaborators that is instrumented with logging +type errorLoggingMiddleware struct { + logger *zap.Logger + next collaborators.Collaborators +} + +// ErrorLoggingMiddleware instruments an implementation of the collaborators.Collaborators with simple logging +func ErrorLoggingMiddleware(logger *zap.Logger) Middleware { + return func(next collaborators.Collaborators) collaborators.Collaborators { + return &errorLoggingMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *errorLoggingMiddleware) Get(ctx context.Context, spaceId string, subject string) (role string, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Get(ctx, spaceId, subject) +} + +func (m *errorLoggingMiddleware) ListCollaborators(ctx context.Context, spaceId string) (collaborators []*collaborators.Collaborator, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.ListCollaborators(ctx, spaceId) +} + +func (m *errorLoggingMiddleware) ListSpaces(ctx context.Context, subject string) (spaces []*collaborators.Collaborator, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.ListSpaces(ctx, subject) +} + +func (m *errorLoggingMiddleware) Remove(ctx context.Context, spaceId string, subject string) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Remove(ctx, spaceId, subject) +} + +func (m *errorLoggingMiddleware) Set(ctx context.Context, spaceId string, subject string, role string) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Set(ctx, spaceId, subject, role) +} diff --git a/pkg/collaborators/middleware/logging_middleware.go b/pkg/collaborators/middleware/logging_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..41f541e6cc0e09f2fc6240d585159bda09c10a74 --- /dev/null +++ b/pkg/collaborators/middleware/logging_middleware.go @@ -0,0 +1,216 @@ +package service + +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/access_log +// gowrap: http://github.com/hexdigest/gowrap + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/collaborators -i Collaborators -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/collaborators" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// loggingMiddleware implements collaborators.Collaborators that is instrumented with logging +type loggingMiddleware struct { + logger *zap.Logger + next collaborators.Collaborators +} + +// LoggingMiddleware instruments an implementation of the collaborators.Collaborators with simple logging +func LoggingMiddleware(logger *zap.Logger) Middleware { + return func(next collaborators.Collaborators) collaborators.Collaborators { + return &loggingMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *loggingMiddleware) Get(ctx context.Context, spaceId string, subject string) (role string, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "subject": subject} { + 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...) + + role, err = m.next.Get(ctx, spaceId, subject) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "role": role, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Get.Response", fields...) + + return role, err +} + +func (m *loggingMiddleware) ListCollaborators(ctx context.Context, spaceId string) (collaborators []*collaborators.Collaborator, 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("ListCollaborators.Request", fields...) + + collaborators, err = m.next.ListCollaborators(ctx, spaceId) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "collaborators": collaborators, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("ListCollaborators.Response", fields...) + + return collaborators, err +} + +func (m *loggingMiddleware) ListSpaces(ctx context.Context, subject string) (spaces []*collaborators.Collaborator, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "subject": subject} { + 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("ListSpaces.Request", fields...) + + spaces, err = m.next.ListSpaces(ctx, subject) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "spaces": spaces, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("ListSpaces.Response", fields...) + + return spaces, err +} + +func (m *loggingMiddleware) Remove(ctx context.Context, spaceId string, subject string) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "subject": subject} { + 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("Remove.Request", fields...) + + err = m.next.Remove(ctx, spaceId, subject) + + 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("Remove.Response", fields...) + + return err +} + +func (m *loggingMiddleware) Set(ctx context.Context, spaceId string, subject string, role string) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "subject": subject, + "role": role} { + 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("Set.Request", fields...) + + err = m.next.Set(ctx, spaceId, subject, role) + + 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("Set.Response", fields...) + + return err +} diff --git a/pkg/collaborators/middleware/middleware.go b/pkg/collaborators/middleware/middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..682fd963ae290298adaec9001b7f60215c80d4db --- /dev/null +++ b/pkg/collaborators/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/collaborators -i Collaborators -t ../../../assets/templates/middleware/middleware -o middleware.go -l "" + +import ( + "git.perx.ru/perxis/perxis-go/pkg/collaborators" + "go.uber.org/zap" +) + +type Middleware func(collaborators.Collaborators) collaborators.Collaborators + +func WithLog(s collaborators.Collaborators, logger *zap.Logger, log_access bool) collaborators.Collaborators { + if logger == nil { + logger = zap.NewNop() + } + + logger = logger.Named("Collaborators") + s = ErrorLoggingMiddleware(logger)(s) + if log_access { + s = LoggingMiddleware(logger)(s) + } + s = RecoveringMiddleware(logger)(s) + return s +} diff --git a/pkg/collaborators/middleware/recovering_middleware.go b/pkg/collaborators/middleware/recovering_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..84e9dfb59514b1a146c96f0239cd3c8e83d8e7ba --- /dev/null +++ b/pkg/collaborators/middleware/recovering_middleware.go @@ -0,0 +1,91 @@ +// 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/collaborators -i Collaborators -t ../../../assets/templates/middleware/recovery -o recovering_middleware.go -l "" + +import ( + "context" + "fmt" + + "git.perx.ru/perxis/perxis-go/pkg/collaborators" + "go.uber.org/zap" +) + +// recoveringMiddleware implements collaborators.Collaborators that is instrumented with logging +type recoveringMiddleware struct { + logger *zap.Logger + next collaborators.Collaborators +} + +// RecoveringMiddleware instruments an implementation of the collaborators.Collaborators with simple logging +func RecoveringMiddleware(logger *zap.Logger) Middleware { + return func(next collaborators.Collaborators) collaborators.Collaborators { + return &recoveringMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *recoveringMiddleware) Get(ctx context.Context, spaceId string, subject string) (role 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.Get(ctx, spaceId, subject) +} + +func (m *recoveringMiddleware) ListCollaborators(ctx context.Context, spaceId string) (collaborators []*collaborators.Collaborator, 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.ListCollaborators(ctx, spaceId) +} + +func (m *recoveringMiddleware) ListSpaces(ctx context.Context, subject string) (spaces []*collaborators.Collaborator, 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.ListSpaces(ctx, subject) +} + +func (m *recoveringMiddleware) Remove(ctx context.Context, spaceId string, subject 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.Remove(ctx, spaceId, subject) +} + +func (m *recoveringMiddleware) Set(ctx context.Context, spaceId string, subject string, role 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.Set(ctx, spaceId, subject, role) +} diff --git a/pkg/collections/middleware/caching_middleware.go b/pkg/collections/middleware/caching_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..0bb41c9e28027e8d9c4cfa5e305b91ac76f44165 --- /dev/null +++ b/pkg/collections/middleware/caching_middleware.go @@ -0,0 +1,136 @@ +package service + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/cache" + service "git.perx.ru/perxis/perxis-go/pkg/collections" + envService "git.perx.ru/perxis/perxis-go/pkg/environments" + "git.perx.ru/perxis/perxis-go/pkg/schema" +) + +func makeKey(spaceId, envId, collectionId string, disableSchemaIncludes bool) string { + s := spaceId + "-" + envId + "-" + collectionId + "-" + if disableSchemaIncludes { + s += "1" + } else { + s += "0" + } + return s +} + +func CachingMiddleware(cache *cache.Cache, envs envService.Environments) Middleware { + return func(next service.Collections) service.Collections { + return &cachingMiddleware{ + cache: cache, + next: next, + envs: envs, + } + } +} + +type cachingMiddleware struct { + cache *cache.Cache + next service.Collections + envs envService.Environments +} + +func (m cachingMiddleware) Create(ctx context.Context, collection *service.Collection) (coll *service.Collection, err error) { + return m.next.Create(ctx, collection) +} + +func (m cachingMiddleware) Get(ctx context.Context, spaceId string, envId string, collectionId string, options ...*service.GetOptions) (coll *service.Collection, err error) { + + opts := service.MergeGetOptions(options...) + value, e := m.cache.Get(makeKey(spaceId, envId, collectionId, opts.DisableSchemaIncludes)) + if e == nil { + return value.(*service.Collection), err + } + coll, err = m.next.Get(ctx, spaceId, envId, collectionId, options...) + if err == nil { + env, err := m.envs.Get(ctx, coll.SpaceID, coll.EnvID) + if err != nil { + return nil, err + } + m.cache.Set(makeKey(coll.SpaceID, env.ID, coll.ID, opts.DisableSchemaIncludes), coll) + for _, al := range env.Aliases { + m.cache.Set(makeKey(coll.SpaceID, al, coll.ID, opts.DisableSchemaIncludes), coll) + } + + } + return coll, err +} + +func (m cachingMiddleware) List(ctx context.Context, spaceId, envId string, filter *service.Filter) (collections []*service.Collection, err error) { + return m.next.List(ctx, spaceId, envId, filter) +} + +func (m cachingMiddleware) Update(ctx context.Context, coll *service.Collection) (err error) { + + err = m.next.Update(ctx, coll) + if err == nil { + env, err := m.envs.Get(ctx, coll.SpaceID, coll.EnvID) + if err != nil { + return err + } + m.cache.Remove(makeKey(env.SpaceID, env.ID, coll.ID, true)) + m.cache.Remove(makeKey(env.SpaceID, env.ID, coll.ID, false)) + for _, al := range env.Aliases { + m.cache.Remove(makeKey(env.SpaceID, al, coll.ID, true)) + m.cache.Remove(makeKey(env.SpaceID, al, coll.ID, false)) + } + } + return err +} + +func (m cachingMiddleware) SetSchema(ctx context.Context, spaceId, envId, collectionId string, schema *schema.Schema) (err error) { + err = m.next.SetSchema(ctx, spaceId, envId, collectionId, schema) + if err == nil { + env, err := m.envs.Get(ctx, spaceId, envId) + if err != nil { + return err + } + m.cache.Remove(makeKey(env.SpaceID, env.ID, collectionId, true)) + m.cache.Remove(makeKey(env.SpaceID, env.ID, collectionId, false)) + for _, al := range env.Aliases { + m.cache.Remove(makeKey(env.SpaceID, al, collectionId, true)) + m.cache.Remove(makeKey(env.SpaceID, al, collectionId, false)) + } + } + return err +} + +func (m cachingMiddleware) SetState(ctx context.Context, spaceId, envId, collectionId string, state *service.StateInfo) (err error) { + err = m.next.SetState(ctx, spaceId, envId, collectionId, state) + if err == nil { + env, err := m.envs.Get(ctx, spaceId, envId) + if err != nil { + return err + } + m.cache.Remove(makeKey(env.SpaceID, env.ID, collectionId, true)) + m.cache.Remove(makeKey(env.SpaceID, env.ID, collectionId, false)) + for _, al := range env.Aliases { + m.cache.Remove(makeKey(env.SpaceID, al, collectionId, true)) + m.cache.Remove(makeKey(env.SpaceID, al, collectionId, false)) + } + } + return err +} + +func (m cachingMiddleware) Delete(ctx context.Context, spaceId string, envId string, collectionId string) (err error) { + + err = m.next.Delete(ctx, spaceId, envId, collectionId) + if err == nil { + env, err := m.envs.Get(ctx, spaceId, envId) + if err != nil { + return err + } + m.cache.Remove(makeKey(env.SpaceID, env.ID, collectionId, true)) + m.cache.Remove(makeKey(env.SpaceID, env.ID, collectionId, false)) + for _, al := range env.Aliases { + m.cache.Remove(makeKey(env.SpaceID, al, collectionId, true)) + m.cache.Remove(makeKey(env.SpaceID, al, collectionId, false)) + } + } + return err +} diff --git a/pkg/collections/middleware/caching_middleware_test.go b/pkg/collections/middleware/caching_middleware_test.go new file mode 100644 index 0000000000000000000000000000000000000000..967a75bce7fa7d933918edc9f05600f75a6c0ff5 --- /dev/null +++ b/pkg/collections/middleware/caching_middleware_test.go @@ -0,0 +1,458 @@ +package service + +import ( + "context" + "testing" + "time" + + "git.perx.ru/perxis/perxis-go/pkg/cache" + "git.perx.ru/perxis/perxis-go/pkg/collections" + colsmocks "git.perx.ru/perxis/perxis-go/pkg/collections/mocks" + "git.perx.ru/perxis/perxis-go/pkg/environments" + envmocks "git.perx.ru/perxis/perxis-go/pkg/environments/mocks" + "git.perx.ru/perxis/perxis-go/pkg/errors" + "git.perx.ru/perxis/perxis-go/pkg/schema" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestCollections_Cache(t *testing.T) { + + const ( + colID = "colID" + spaceID = "spaceID" + envID = "envId" + envAlias = "envAlias" + size = 5 + ttl = 20 * time.Millisecond + ) + + errNotFound := errors.NotFound(errors.New("not found")) + + ctx := context.Background() + + t.Run("Get from cache", func(t *testing.T) { + col := &colsmocks.Collections{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + col.On("Get", mock.Anything, spaceID, envID, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, envID, colID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envID, colID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кеша РїСЂРё повторном запросе РїРѕ ID окружения.") + + v3, err := svc.Get(ctx, spaceID, envAlias, colID) + require.NoError(t, err) + assert.Same(t, v3, v2, "Ожидается получение объекта РёР· кеша, РїСЂРё запросе того Р¶Рµ объекта РїРѕ alias окружения.") + + env.AssertExpectations(t) + col.AssertExpectations(t) + }) + + t.Run("Get from cache(by Alias)", func(t *testing.T) { + col := &colsmocks.Collections{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col) + + col.On("Get", mock.Anything, spaceID, envAlias, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}, nil).Once() + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, envAlias, colID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envAlias, colID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кеша РїСЂРё повторном запросе РїРѕ Alias окружения.") + + v3, err := svc.Get(ctx, spaceID, envID, colID) + require.NoError(t, err) + assert.Same(t, v3, v2, "Ожидается получение объекта РёР· кеша, РїСЂРё запросе того Р¶Рµ объекта РїРѕ ID окружения.") + + env.AssertExpectations(t) + col.AssertExpectations(t) + }) + + t.Run("Get from cache with options", func(t *testing.T) { + col := &colsmocks.Collections{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + col.On("Get", mock.Anything, spaceID, envID, colID, mock.Anything).Run(func(args mock.Arguments) { + require.Len(t, args, 5) + opt := args.Get(4).(*collections.GetOptions) + assert.True(t, opt.DisableSchemaIncludes) + }).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}, nil).Once() + + _, err := svc.Get(ctx, spaceID, envID, colID, []*collections.GetOptions{{DisableSchemaIncludes: true}}...) + require.NoError(t, err) + + env.AssertExpectations(t) + col.AssertExpectations(t) + }) + + //t.Run("List from cache", func(t *testing.T) { + // col := &colsmocks.Collections{} + // env := &envmocks.Environments{} + // + // svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col) + // + // col.On("List", mock.Anything, spaceID, envID).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Once() + // env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + // + // vl1, err := svc.List(ctx, spaceID, envID, nil) + // require.NoError(t, err) + // + // vl2, err := svc.List(ctx, spaceID, envID, nil) + // require.NoError(t, err) + // assert.Len(t, vl2, 1) + // assert.Same(t, vl1[0], vl2[0], "РџСЂРё повторном запросе РїРѕ ID окружения, ожидается получение СЃРїРёСЃРєР° объектов РёР· кеша.") + // + // vl3, err := svc.List(ctx, spaceID, envAlias, nil) + // require.NoError(t, err) + // assert.Len(t, vl3, 1) + // assert.Same(t, vl3[0], vl2[0], "РџСЂРё повторном запросе РїРѕ Alias окружения, ожидается получение СЃРїРёСЃРєР° объектов РёР· кеша.") + // + // env.AssertExpectations(t) + // col.AssertExpectations(t) + //}) + + t.Run("List", func(t *testing.T) { + col := &colsmocks.Collections{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col) + + col.On("List", mock.Anything, spaceID, envAlias, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Once() + col.On("List", mock.Anything, spaceID, envID, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Once() + + _, err := svc.List(ctx, spaceID, envAlias, nil) + require.NoError(t, err) + + _, err = svc.List(ctx, spaceID, envID, nil) + require.NoError(t, err) + + env.AssertExpectations(t) + col.AssertExpectations(t) + }) + + t.Run("Invalidate cache", func(t *testing.T) { + t.Run("After Update", func(t *testing.T) { + col := &colsmocks.Collections{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + col.On("Get", mock.Anything, spaceID, envID, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}, nil).Once() + col.On("List", mock.Anything, spaceID, envID, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, envID, colID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envID, colID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кеша РїРѕ ID окружения.") + + v3, err := svc.Get(ctx, spaceID, envAlias, colID) + require.NoError(t, err) + assert.Same(t, v2, v3, "Ожидается получение объекта РёР· кеша РїРѕ Alias окружения.") + + vl1, err := svc.List(ctx, spaceID, envID, nil) + require.NoError(t, err) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + col.On("Update", mock.Anything, mock.Anything).Return(nil).Once() + err = svc.Update(ctx, &collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "nameUPD"}) + require.NoError(t, err) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + col.On("Get", mock.Anything, spaceID, envID, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "nameUPD"}, nil).Once() + col.On("List", mock.Anything, spaceID, envID, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "nameUPD"}}, nil).Once() + + v4, err := svc.Get(ctx, spaceID, envID, colID) + require.NoError(t, err) + assert.NotSame(t, v3, v4, "Ожидает что элемент после обновления был удален РёР· кэша Рё будет запрошен заново РёР· сервиса.") + + v5, err := svc.Get(ctx, spaceID, envAlias, colID) + require.NoError(t, err) + assert.Same(t, v4, v5, "Ожидается получение объекта РёР· кеша РїРѕ Alias окружения.") + + vl2, err := svc.List(ctx, spaceID, envID, nil) + require.NoError(t, err) + assert.NotSame(t, vl1[0], vl2[0], "Ожидает что после обновления элементы Р±СѓРґСѓС‚ запрошены заново РёР· сервиса.") + + env.AssertExpectations(t) + col.AssertExpectations(t) + }) + + t.Run("After Update(by Alias)", func(t *testing.T) { + col := &colsmocks.Collections{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + //env.On("Get", mock.Anything, spaceID, envAlias).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + + col.On("Get", mock.Anything, spaceID, envAlias, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}, nil).Once() + col.On("List", mock.Anything, spaceID, envAlias, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, envAlias, colID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envAlias, colID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кеша РїРѕ Alias окружения.") + + v3, err := svc.Get(ctx, spaceID, envID, colID) + require.NoError(t, err) + assert.Same(t, v2, v3, "Ожидается получение объекта РёР· кеша РїРѕ ID окружения.") + + vl1, err := svc.List(ctx, spaceID, envAlias, nil) + require.NoError(t, err) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + col.On("Update", mock.Anything, mock.Anything).Return(nil).Once() + err = svc.Update(ctx, &collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "nameUPD"}) + require.NoError(t, err) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + //env.On("Get", mock.Anything, spaceID, envAlias).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + + col.On("Get", mock.Anything, spaceID, envAlias, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "nameUPD"}, nil).Once() + col.On("List", mock.Anything, spaceID, envAlias, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "nameUPD"}}, nil).Once() + + v4, err := svc.Get(ctx, spaceID, envAlias, colID) + require.NoError(t, err) + assert.NotSame(t, v3, v4, "Ожидает что элемент после обновления был удален РёР· кэша Рё будет запрошен заново РёР· сервиса.") + + v5, err := svc.Get(ctx, spaceID, envID, colID) + require.NoError(t, err) + assert.Same(t, v4, v5, "Ожидается получение объекта РёР· кеша РїРѕ Alias окружения.") + + vl4, err := svc.List(ctx, spaceID, envAlias, nil) + require.NoError(t, err) + assert.NotSame(t, vl1[0], vl4[0], "Ожидает что после обновления элементы Р±СѓРґСѓС‚ запрошены заново РёР· сервиса.") + + env.AssertExpectations(t) + col.AssertExpectations(t) + }) + + t.Run("After Set Schema", func(t *testing.T) { + col := &colsmocks.Collections{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Twice() + col.On("Get", mock.Anything, spaceID, envID, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}, nil).Once() + col.On("List", mock.Anything, spaceID, envID, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Twice() + col.On("List", mock.Anything, spaceID, envAlias, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, envID, colID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envID, colID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кеша РїРѕ ID окружения.") + + v3, err := svc.Get(ctx, spaceID, envAlias, colID) + require.NoError(t, err) + assert.Same(t, v2, v3, "Ожидается получение объекта РёР· кеша РїРѕ Alias окружения.") + + vl1, err := svc.List(ctx, spaceID, envID, nil) + require.NoError(t, err) + + vl2, err := svc.List(ctx, spaceID, envID, nil) + require.NoError(t, err) + assert.Len(t, vl2, 1) + assert.Same(t, vl1[0], vl2[0], "Ожидается получение объектов РёР· кеша РїРѕ ID окружения.") + + vl3, err := svc.List(ctx, spaceID, envAlias, nil) + require.NoError(t, err) + assert.Len(t, vl2, 1) + assert.Equal(t, vl2[0], vl3[0], "Ожидается получение объектов РёР· кеша РїРѕ Alias окружения.") + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + col.On("SetSchema", mock.Anything, spaceID, envID, colID, mock.Anything).Return(nil).Once() + err = svc.SetSchema(ctx, spaceID, envID, colID, &schema.Schema{}) + require.NoError(t, err) + + //env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + col.On("Get", mock.Anything, spaceID, envID, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "nameUPD"}, nil).Once() + col.On("List", mock.Anything, spaceID, envID, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "nameUPD"}}, nil).Once() + col.On("List", mock.Anything, spaceID, envAlias, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "nameUPD"}}, nil).Once() + + v4, err := svc.Get(ctx, spaceID, envID, colID) + require.NoError(t, err) + assert.NotSame(t, v3, v4, "Ожидает что элемент после обновления схемы был удален РёР· кэша Рё будет запрошен заново РёР· сервиса.") + + v5, err := svc.Get(ctx, spaceID, envAlias, colID) + require.NoError(t, err) + assert.Same(t, v4, v5, "Ожидается получение объекта РёР· кеша РїРѕ Alias окружения.") + + vl4, err := svc.List(ctx, spaceID, envID, nil) + require.NoError(t, err) + assert.NotSame(t, vl4[0], vl3[0], "Ожидает что после обновления схемы элементы Р±СѓРґСѓС‚ запрошены заново РёР· сервиса.") + + vl5, err := svc.List(ctx, spaceID, envAlias, nil) + require.NoError(t, err) + assert.Equal(t, vl4[0], vl5[0], "Ожидается получение объектов РёР· кеша РїРѕ Alias окружения..") + + env.AssertExpectations(t) + col.AssertExpectations(t) + }) + + t.Run("After Delete", func(t *testing.T) { + col := &colsmocks.Collections{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Twice() + col.On("Get", mock.Anything, spaceID, envID, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}, nil).Once() + col.On("List", mock.Anything, spaceID, envID, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Twice() + col.On("List", mock.Anything, spaceID, envAlias, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, envID, colID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envID, colID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кеша РїРѕ ID окружения.") + + v3, err := svc.Get(ctx, spaceID, envAlias, colID) + require.NoError(t, err) + assert.Same(t, v2, v3, "Ожидается получение объекта РёР· кеша РїРѕ Alias окружения.") + + vl1, err := svc.List(ctx, spaceID, envID, nil) + require.NoError(t, err) + + vl2, err := svc.List(ctx, spaceID, envID, nil) + require.NoError(t, err) + assert.Len(t, vl2, 1) + assert.Same(t, vl1[0], vl2[0], "Ожидается получение объектов РёР· кеша РїРѕ ID окружения.") + + vl3, err := svc.List(ctx, spaceID, envAlias, nil) + require.NoError(t, err) + assert.Len(t, vl2, 1) + assert.Equal(t, vl2[0], vl3[0], "Ожидается получение объектов РёР· кеша РїРѕ Alias окружения.") + + //env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + col.On("Delete", mock.Anything, spaceID, envID, colID).Return(nil).Once() + err = svc.Delete(ctx, spaceID, envID, colID) + require.NoError(t, err) + + //env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + col.On("Get", mock.Anything, spaceID, envID, colID).Return(nil, errNotFound).Once() + col.On("List", mock.Anything, spaceID, envID, mock.Anything).Return([]*collections.Collection{}, nil).Once() + + _, err = svc.Get(ctx, spaceID, envID, colID) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидает что элемент был удален РёР· кэша Рё получена ошибка РѕС‚ сервиса.") + + vl4, err := svc.List(ctx, spaceID, envID, nil) + require.NoError(t, err) + assert.Len(t, vl4, 0, "Ожидает что элементы были удалены РёР· кэша.") + + col.On("Get", mock.Anything, spaceID, envAlias, colID).Return(nil, errNotFound).Once() + + _, err = svc.Get(ctx, spaceID, envAlias, colID) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидает что элемент был удален РёР· кэша Рё получена ошибка РѕС‚ сервиса.") + + env.AssertExpectations(t) + col.AssertExpectations(t) + }) + + t.Run("After Create", func(t *testing.T) { + col := &colsmocks.Collections{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col) + + //env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + col.On("List", mock.Anything, spaceID, envID, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Twice() + col.On("List", mock.Anything, spaceID, envAlias, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Once() + + vl1, err := svc.List(ctx, spaceID, envID, nil) + require.NoError(t, err) + + vl2, err := svc.List(ctx, spaceID, envID, nil) + require.NoError(t, err) + assert.Len(t, vl2, 1) + assert.Same(t, vl1[0], vl2[0], "Ожидается получение объектов РёР· кеша РїРѕ ID окружения.") + + vl3, err := svc.List(ctx, spaceID, envAlias, nil) + require.NoError(t, err) + assert.Len(t, vl2, 1) + assert.Equal(t, vl2[0], vl3[0], "Ожидается получение объектов РёР· кеша РїРѕ Alias окружения.") + + //env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + col.On("Create", mock.Anything, mock.Anything).Return(&collections.Collection{ID: "colID2", SpaceID: spaceID, EnvID: envID, Name: "name2"}, nil).Once() + _, err = svc.Create(ctx, &collections.Collection{ID: "colID2", SpaceID: spaceID, EnvID: envID, Name: "name2"}) + require.NoError(t, err) + + //env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + col.On("List", mock.Anything, spaceID, envID, mock.Anything).Return([]*collections.Collection{ + {ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}, + {ID: "colID2", SpaceID: spaceID, EnvID: envID, Name: "name2"}, + }, nil).Once() + col.On("List", mock.Anything, spaceID, envAlias, mock.Anything).Return([]*collections.Collection{ + {ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}, + {ID: "colID2", SpaceID: spaceID, EnvID: envID, Name: "name2"}, + }, nil).Once() + + vl4, err := svc.List(ctx, spaceID, envID, nil) + require.NoError(t, err) + assert.Len(t, vl4, 2, "Ожидает что элементы были удалены РёР· кэша Рё получены заново РёР· сервиса.") + + vl5, err := svc.List(ctx, spaceID, envAlias, nil) + require.NoError(t, err) + assert.Len(t, vl5, 2) + assert.Equal(t, vl4[0], vl5[0], "Ожидается получение объектов РёР· кеша РїРѕ Alias окружения..") + + env.AssertExpectations(t) + col.AssertExpectations(t) + }) + + t.Run("After TTL expired", func(t *testing.T) { + col := &colsmocks.Collections{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil) + col.On("Get", mock.Anything, spaceID, envID, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, envID, colID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envID, colID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кеша.") + + time.Sleep(2 * ttl) + + col.On("Get", mock.Anything, spaceID, envID, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}, nil).Once() + + v3, err := svc.Get(ctx, spaceID, envID, colID) + require.NoError(t, err) + assert.NotSame(t, v3, v2, "Ожидает что элемент был удален РёР· кэша Рё будет запрошен заново РёР· сервиса.") + + env.AssertExpectations(t) + col.AssertExpectations(t) + }) + }) + +} diff --git a/pkg/collections/middleware/error_logging_middleware.go b/pkg/collections/middleware/error_logging_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..d1be7f66c8564cc162d93a926652c63d0a670e1d --- /dev/null +++ b/pkg/collections/middleware/error_logging_middleware.go @@ -0,0 +1,101 @@ +package service + +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/error_log +// gowrap: http://github.com/hexdigest/gowrap + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/collections -i Collections -t ../../../assets/templates/middleware/error_log -o error_logging_middleware.go -l "" + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/collections" + "git.perx.ru/perxis/perxis-go/pkg/schema" + "go.uber.org/zap" +) + +// errorLoggingMiddleware implements collections.Collections that is instrumented with logging +type errorLoggingMiddleware struct { + logger *zap.Logger + next collections.Collections +} + +// ErrorLoggingMiddleware instruments an implementation of the collections.Collections with simple logging +func ErrorLoggingMiddleware(logger *zap.Logger) Middleware { + return func(next collections.Collections) collections.Collections { + return &errorLoggingMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *errorLoggingMiddleware) Create(ctx context.Context, collection *collections.Collection) (created *collections.Collection, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Create(ctx, collection) +} + +func (m *errorLoggingMiddleware) Delete(ctx context.Context, spaceId string, envId string, collectionId string) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Delete(ctx, spaceId, envId, collectionId) +} + +func (m *errorLoggingMiddleware) Get(ctx context.Context, spaceId string, envId string, collectionId string, options ...*collections.GetOptions) (collection *collections.Collection, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Get(ctx, spaceId, envId, collectionId, options...) +} + +func (m *errorLoggingMiddleware) List(ctx context.Context, spaceId string, envId string, filter *collections.Filter) (collections []*collections.Collection, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.List(ctx, spaceId, envId, filter) +} + +func (m *errorLoggingMiddleware) SetSchema(ctx context.Context, spaceId string, envId string, collectionId string, schema *schema.Schema) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.SetSchema(ctx, spaceId, envId, collectionId, schema) +} + +func (m *errorLoggingMiddleware) SetState(ctx context.Context, spaceId string, envId string, collectionId string, state *collections.StateInfo) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.SetState(ctx, spaceId, envId, collectionId, state) +} + +func (m *errorLoggingMiddleware) Update(ctx context.Context, coll *collections.Collection) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Update(ctx, coll) +} diff --git a/pkg/collections/middleware/logging_middleware.go b/pkg/collections/middleware/logging_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..dd43cb9ffd3e662241a1bb08e7174cfd1c5677df --- /dev/null +++ b/pkg/collections/middleware/logging_middleware.go @@ -0,0 +1,296 @@ +package service + +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/access_log +// gowrap: http://github.com/hexdigest/gowrap + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/collections -i Collections -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/collections" + "git.perx.ru/perxis/perxis-go/pkg/schema" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// loggingMiddleware implements collections.Collections that is instrumented with logging +type loggingMiddleware struct { + logger *zap.Logger + next collections.Collections +} + +// LoggingMiddleware instruments an implementation of the collections.Collections with simple logging +func LoggingMiddleware(logger *zap.Logger) Middleware { + return func(next collections.Collections) collections.Collections { + return &loggingMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *loggingMiddleware) Create(ctx context.Context, collection *collections.Collection) (created *collections.Collection, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "collection": collection} { + 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, collection) + + 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, envId string, collectionId string) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "envId": envId, + "collectionId": collectionId} { + 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, envId, collectionId) + + 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) Get(ctx context.Context, spaceId string, envId string, collectionId string, options ...*collections.GetOptions) (collection *collections.Collection, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "envId": envId, + "collectionId": collectionId, + "options": options} { + 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...) + + collection, err = m.next.Get(ctx, spaceId, envId, collectionId, options...) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "collection": collection, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Get.Response", fields...) + + return collection, err +} + +func (m *loggingMiddleware) List(ctx context.Context, spaceId string, envId string, filter *collections.Filter) (collections []*collections.Collection, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "envId": envId, + "filter": filter} { + 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...) + + collections, err = m.next.List(ctx, spaceId, envId, filter) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "collections": collections, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("List.Response", fields...) + + return collections, err +} + +func (m *loggingMiddleware) SetSchema(ctx context.Context, spaceId string, envId string, collectionId string, schema *schema.Schema) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "envId": envId, + "collectionId": collectionId, + "schema": schema} { + 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("SetSchema.Request", fields...) + + err = m.next.SetSchema(ctx, spaceId, envId, collectionId, schema) + + 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("SetSchema.Response", fields...) + + return err +} + +func (m *loggingMiddleware) SetState(ctx context.Context, spaceId string, envId string, collectionId string, state *collections.StateInfo) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "envId": envId, + "collectionId": collectionId, + "state": state} { + 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("SetState.Request", fields...) + + err = m.next.SetState(ctx, spaceId, envId, collectionId, state) + + 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("SetState.Response", fields...) + + return err +} + +func (m *loggingMiddleware) Update(ctx context.Context, coll *collections.Collection) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "coll": coll} { + 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, coll) + + 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/collections/middleware/middleware.go b/pkg/collections/middleware/middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..911368f0f1df019e0e9b4b22afac9863cd4e523b --- /dev/null +++ b/pkg/collections/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/collections -i Collections -t ../../../assets/templates/middleware/middleware -o middleware.go -l "" + +import ( + "git.perx.ru/perxis/perxis-go/pkg/collections" + "go.uber.org/zap" +) + +type Middleware func(collections.Collections) collections.Collections + +func WithLog(s collections.Collections, logger *zap.Logger, log_access bool) collections.Collections { + if logger == nil { + logger = zap.NewNop() + } + + logger = logger.Named("Collections") + s = ErrorLoggingMiddleware(logger)(s) + if log_access { + s = LoggingMiddleware(logger)(s) + } + s = RecoveringMiddleware(logger)(s) + return s +} diff --git a/pkg/collections/middleware/recovering_middleware.go b/pkg/collections/middleware/recovering_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..fb61326fd05df2473219bccacea776246a68376e --- /dev/null +++ b/pkg/collections/middleware/recovering_middleware.go @@ -0,0 +1,116 @@ +// 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/collections -i Collections -t ../../../assets/templates/middleware/recovery -o recovering_middleware.go -l "" + +import ( + "context" + "fmt" + + "git.perx.ru/perxis/perxis-go/pkg/collections" + "git.perx.ru/perxis/perxis-go/pkg/schema" + "go.uber.org/zap" +) + +// recoveringMiddleware implements collections.Collections that is instrumented with logging +type recoveringMiddleware struct { + logger *zap.Logger + next collections.Collections +} + +// RecoveringMiddleware instruments an implementation of the collections.Collections with simple logging +func RecoveringMiddleware(logger *zap.Logger) Middleware { + return func(next collections.Collections) collections.Collections { + return &recoveringMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *recoveringMiddleware) Create(ctx context.Context, collection *collections.Collection) (created *collections.Collection, 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, collection) +} + +func (m *recoveringMiddleware) Delete(ctx context.Context, spaceId string, envId string, collectionId 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, envId, collectionId) +} + +func (m *recoveringMiddleware) Get(ctx context.Context, spaceId string, envId string, collectionId string, options ...*collections.GetOptions) (collection *collections.Collection, 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, envId, collectionId, options...) +} + +func (m *recoveringMiddleware) List(ctx context.Context, spaceId string, envId string, filter *collections.Filter) (collections []*collections.Collection, 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, envId, filter) +} + +func (m *recoveringMiddleware) SetSchema(ctx context.Context, spaceId string, envId string, collectionId string, schema *schema.Schema) (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.SetSchema(ctx, spaceId, envId, collectionId, schema) +} + +func (m *recoveringMiddleware) SetState(ctx context.Context, spaceId string, envId string, collectionId string, state *collections.StateInfo) (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.SetState(ctx, spaceId, envId, collectionId, state) +} + +func (m *recoveringMiddleware) Update(ctx context.Context, coll *collections.Collection) (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, coll) +} diff --git a/pkg/environments/middleware/caching_middleware.go b/pkg/environments/middleware/caching_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..18b594bc3a193d962d63ef3d5eae6217408d61bd --- /dev/null +++ b/pkg/environments/middleware/caching_middleware.go @@ -0,0 +1,167 @@ +package service + +import ( + "context" + "strings" + + "git.perx.ru/perxis/perxis-go/pkg/cache" + service "git.perx.ru/perxis/perxis-go/pkg/environments" +) + +func makeKey(ss ...string) string { + return strings.Join(ss, "-") +} + +func CachingMiddleware(cache *cache.Cache) Middleware { + return func(next service.Environments) service.Environments { + return &cachingMiddleware{ + cache: cache, + next: next, + } + } +} + +type cachingMiddleware struct { + cache *cache.Cache + next service.Environments +} + +func (m cachingMiddleware) Create(ctx context.Context, env *service.Environment) (environment *service.Environment, err error) { + + environment, err = m.next.Create(ctx, env) + if err == nil { + m.cache.Remove(environment.SpaceID) + } + return environment, err +} + +func (m cachingMiddleware) Get(ctx context.Context, spaceId string, envId string) (environment *service.Environment, err error) { + + value, e := m.cache.Get(makeKey(spaceId, envId)) + if e == nil { + return value.(*service.Environment), err + } + environment, err = m.next.Get(ctx, spaceId, envId) + if err == nil { + m.cache.Set(makeKey(spaceId, environment.ID), environment) + for _, a := range environment.Aliases { + m.cache.Set(makeKey(spaceId, a), environment) + } + } + return environment, err +} + +func (m cachingMiddleware) List(ctx context.Context, spaceId string) (environments []*service.Environment, err error) { + + value, e := m.cache.Get(spaceId) + if e == nil { + return value.([]*service.Environment), err + } + environments, err = m.next.List(ctx, spaceId) + if err == nil { + m.cache.Set(spaceId, environments) + } + return environments, err +} + +func (m cachingMiddleware) Update(ctx context.Context, env *service.Environment) (err error) { + + err = m.next.Update(ctx, env) + if err == nil { + value, e := m.cache.Get(makeKey(env.SpaceID, env.ID)) + if e == nil { + env := value.(*service.Environment) + m.cache.Remove(makeKey(env.SpaceID, env.ID)) + for _, a := range env.Aliases { + m.cache.Remove(makeKey(env.SpaceID, a)) + } + } + m.cache.Remove(env.SpaceID) + } + return err +} + +func (m cachingMiddleware) Delete(ctx context.Context, spaceId string, envId string) (err error) { + + err = m.next.Delete(ctx, spaceId, envId) + if err == nil { + value, e := m.cache.Get(makeKey(spaceId, envId)) + if e == nil { + env := value.(*service.Environment) + m.cache.Remove(makeKey(env.SpaceID, env.ID)) + for _, a := range env.Aliases { + m.cache.Remove(makeKey(env.SpaceID, a)) + } + } + m.cache.Remove(spaceId) + } + return err +} + +func (m cachingMiddleware) SetAlias(ctx context.Context, spaceId string, envId string, alias string) (err error) { + + err = m.next.SetAlias(ctx, spaceId, envId, alias) + if err == nil { + value, e := m.cache.Get(makeKey(spaceId, alias)) + if e == nil { + env := value.(*service.Environment) + m.cache.Remove(makeKey(env.SpaceID, env.ID)) + for _, a := range env.Aliases { + m.cache.Remove(makeKey(env.SpaceID, a)) + } + } + + value, e = m.cache.Get(makeKey(spaceId, envId)) + if e == nil { + env := value.(*service.Environment) + m.cache.Remove(makeKey(env.SpaceID, env.ID)) + for _, a := range env.Aliases { + m.cache.Remove(makeKey(env.SpaceID, a)) + } + } + m.cache.Remove(spaceId) + } + return err +} + +func (m cachingMiddleware) RemoveAlias(ctx context.Context, spaceId string, envId string, alias string) (err error) { + + err = m.next.RemoveAlias(ctx, spaceId, envId, alias) + if err == nil { + m.cache.Remove(spaceId) + value, e := m.cache.Get(makeKey(spaceId, alias)) + if e == nil { + env := value.(*service.Environment) + m.cache.Remove(makeKey(env.SpaceID, env.ID)) + for _, a := range env.Aliases { + m.cache.Remove(makeKey(env.SpaceID, a)) + } + } + + value, e = m.cache.Get(makeKey(spaceId, envId)) + if e == nil { + env := value.(*service.Environment) + m.cache.Remove(makeKey(env.SpaceID, env.ID)) + for _, a := range env.Aliases { + m.cache.Remove(makeKey(env.SpaceID, a)) + } + } + } + return err +} + +func (m cachingMiddleware) Migrate(ctx context.Context, spaceId, envId string, options ...*service.MigrateOptions) (err error) { + err = m.next.Migrate(ctx, spaceId, envId, options...) + + // значение РёР· кэша удалить РІРЅРµ зависимости РѕС‚ наличия ошибки, поскольку состояние окружения могло измениться + value, e := m.cache.Get(makeKey(spaceId, envId)) + if e == nil { + env := value.(*service.Environment) + m.cache.Remove(makeKey(env.SpaceID, env.ID)) + for _, a := range env.Aliases { + m.cache.Remove(makeKey(env.SpaceID, a)) + } + } + m.cache.Remove(spaceId) + return err +} diff --git a/pkg/environments/middleware/caching_middleware_test.go b/pkg/environments/middleware/caching_middleware_test.go new file mode 100644 index 0000000000000000000000000000000000000000..f5bfa7a9b5d207ed0c00d60cd126950a6da5aba0 --- /dev/null +++ b/pkg/environments/middleware/caching_middleware_test.go @@ -0,0 +1,387 @@ +package service + +import ( + "context" + "testing" + "time" + + "git.perx.ru/perxis/perxis-go/pkg/cache" + "git.perx.ru/perxis/perxis-go/pkg/environments" + mocksenvironments "git.perx.ru/perxis/perxis-go/pkg/environments/mocks" + "git.perx.ru/perxis/perxis-go/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestEnvironmentsCache(t *testing.T) { + + const ( + envID = "envID" + spaceID = "spaceID" + envAlias = "envAlias" + size = 5 + ttl = 20 * time.Millisecond + ) + + errNotFound := errors.NotFound(errors.New("not found")) + + ctx := context.Background() + + t.Run("Get from cache", func(t *testing.T) { + envs := &mocksenvironments.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(envs) + + envs.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Description: "Environment", Aliases: []string{envAlias}}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, envID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кэша РїСЂРё повторном запросе РїРѕ ID.") + + v3, err := svc.Get(ctx, spaceID, envAlias) + require.NoError(t, err) + assert.Same(t, v3, v2, "Ожидается получение объекта РёР· кеша, РїСЂРё запросе того Р¶Рµ объекта РїРѕ alias окружения.") + + envs.AssertExpectations(t) + }) + + t.Run("Get from cache(by Alias)", func(t *testing.T) { + envs := &mocksenvironments.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(envs) + + envs.On("Get", mock.Anything, spaceID, envAlias).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Description: "Environment", Aliases: []string{envAlias}}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, envAlias) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envAlias) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кэша РїРѕ alias.") + + v3, err := svc.Get(ctx, spaceID, envID) + require.NoError(t, err) + assert.Same(t, v3, v2, "Ожидается получение объекта РёР· кеша, РїСЂРё запросе того Р¶Рµ объекта РїРѕ ID окружения.") + + envs.AssertExpectations(t) + }) + + t.Run("List from cache", func(t *testing.T) { + envs := &mocksenvironments.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(envs) + + envs.On("List", mock.Anything, spaceID).Return([]*environments.Environment{{ID: envID, SpaceID: spaceID, Description: "Environment"}}, 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], "Ожидается получение объектов РёР· кэша.") + + envs.AssertExpectations(t) + }) + + t.Run("Invalidate cache", func(t *testing.T) { + t.Run("After SetAlias", func(t *testing.T) { + envs := &mocksenvironments.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(envs) + + envs.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Description: "Environment", Aliases: []string{"envID2"}}, nil).Once() + envs.On("List", mock.Anything, spaceID).Return([]*environments.Environment{{ID: envID, SpaceID: spaceID, Description: "Environment", Aliases: []string{"envID2"}}}, nil).Once() + envs.On("SetAlias", mock.Anything, spaceID, envID, envAlias).Return(nil).Once() + + v1, err := svc.Get(ctx, spaceID, envID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кэша.") + + 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], "Ожидается получение объектов РёР· кэша.") + + err = svc.SetAlias(ctx, spaceID, envID, envAlias) + require.NoError(t, err) + + envs.On("Get", mock.Anything, spaceID, envAlias).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Description: "Environment", Aliases: []string{"envID2", envAlias}}, nil).Once() + envs.On("List", mock.Anything, spaceID).Return([]*environments.Environment{{ID: envID, SpaceID: spaceID, Description: "Environment", Aliases: []string{"envID2", envAlias}}}, nil).Once() + + v4, err := svc.Get(ctx, spaceID, envAlias) + require.NoError(t, err) + assert.Contains(t, v4.Aliases, envAlias, "Ожидает что элемент будет запрошен РёР· сервиса РїРѕ Alias.") + + v5, err := svc.Get(ctx, spaceID, envID) + require.NoError(t, err) + assert.Same(t, v4, v5, "Ожидается получение объекта РёР· кэша РїРѕ ID.") + + vl3, err := svc.List(ctx, spaceID) + require.NoError(t, err) + assert.NotSame(t, vl2[0], vl3[0], "Ожидает что объекты Р±СѓРґСѓС‚ удалены РёР· кэша Рё запрошены РёР· сервиса.") + + envs.AssertExpectations(t) + }) + + t.Run("After RemoveAlias", func(t *testing.T) { + envs := &mocksenvironments.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(envs) + + envs.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Description: "Environment", Aliases: []string{"envID2", envAlias}}, nil).Once() + envs.On("List", mock.Anything, spaceID).Return([]*environments.Environment{{ID: envID, SpaceID: spaceID, Description: "Environment", Aliases: []string{"envID2", envAlias}}}, nil).Once() + envs.On("RemoveAlias", mock.Anything, spaceID, envID, envAlias).Return(nil).Once() + + v1, err := svc.Get(ctx, spaceID, envID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кэша РїРѕ ID.") + + v3, err := svc.Get(ctx, spaceID, envAlias) + require.NoError(t, err) + assert.Same(t, v2, v3, "Ожидается получение объекта РёР· кэша РїРѕ Alias.") + + 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], "Ожидается получение объектов РёР· кэша.") + + err = svc.RemoveAlias(ctx, spaceID, envID, envAlias) + require.NoError(t, err) + + envs.On("Get", mock.Anything, spaceID, envAlias).Return(nil, errNotFound).Once() + envs.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Description: "Environment", Aliases: []string{"envID2"}}, nil).Once() + envs.On("List", mock.Anything, spaceID).Return([]*environments.Environment{{ID: envID, SpaceID: spaceID, Description: "Environment", Aliases: []string{"envID2"}}}, nil).Once() + + _, err = svc.Get(ctx, spaceID, envAlias) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидает что элемент был удален РёР· кеша Рё сервис вернул ошибку РЅР° запрос РїРѕ несуществующему Alias.") + + v4, err := svc.Get(ctx, spaceID, envID) + require.NoError(t, err) + assert.NotSame(t, v3, v4, "Ожидает что элемент был удален РёР· кеша Рё получен РёР· сервиса РїРѕ ID.") + + vl3, err := svc.List(ctx, spaceID) + require.NoError(t, err) + assert.NotSame(t, vl2[0], vl3[0], "Ожидает что объекты Р±СѓРґСѓС‚ удалены РёР· кэша Рё запрошены РёР· сервиса.") + + envs.AssertExpectations(t) + }) + + t.Run("After Update", func(t *testing.T) { + envs := &mocksenvironments.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(envs) + + envs.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Description: "Environment", Aliases: []string{envAlias}}, nil).Once() + envs.On("List", mock.Anything, spaceID).Return([]*environments.Environment{{ID: envID, SpaceID: spaceID, Description: "Environment", Aliases: []string{envAlias}}}, nil).Once() + envs.On("Update", mock.Anything, mock.Anything).Return(nil).Once() + + v1, err := svc.Get(ctx, spaceID, envID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кэша.") + + v3, err := svc.Get(ctx, spaceID, envAlias) + require.NoError(t, err) + assert.Same(t, v2, v3, "Ожидается получение объекта РёР· кэша РїРѕ Alias.") + + 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], "Ожидается получение объектов РёР· кэша.") + + err = svc.Update(ctx, &environments.Environment{ID: envID, SpaceID: spaceID, Description: "EnvironmentUPD", Aliases: []string{envAlias}}) + require.NoError(t, err) + + envs.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Description: "EnvironmentUPD", Aliases: []string{envAlias}}, nil).Once() + envs.On("List", mock.Anything, spaceID).Return([]*environments.Environment{{ID: envID, SpaceID: spaceID, Description: "EnvironmentUPD", Aliases: []string{envAlias}}}, nil).Once() + + _, err = svc.Get(ctx, spaceID, envID) + require.NoError(t, err) + + v4, err := svc.Get(ctx, spaceID, envID) + require.NoError(t, err) + assert.NotSame(t, v2, v4, "Ожидает что элемент был удален РёР· кэша Рё будет запрошен заново РёР· сервиса.") + + v5, err := svc.Get(ctx, spaceID, envAlias) + require.NoError(t, err) + assert.Same(t, v4, v5, "Ожидается получение объекта РёР· кэша РїРѕ Alias после обновления объекта Рё получения РїРѕ ID.") + + vl3, err := svc.List(ctx, spaceID) + require.NoError(t, err) + assert.NotSame(t, vl2[0], vl3[0], "Ожидает что объекты Р±СѓРґСѓС‚ удалены РёР· кэша Рё запрошены РёР· сервиса.") + + envs.AssertExpectations(t) + }) + + t.Run("After Update(List)", func(t *testing.T) { + envs := &mocksenvironments.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(envs) + + envs.On("List", mock.Anything, spaceID).Return([]*environments.Environment{{ID: envID, SpaceID: spaceID, Description: "Environment", Aliases: []string{envAlias}}}, nil).Once() + envs.On("Update", mock.Anything, mock.Anything).Return(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], "Ожидается получение объектов РёР· кэша.") + + err = svc.Update(ctx, &environments.Environment{ID: envID, SpaceID: spaceID, Description: "EnvironmentUPD", Aliases: []string{envAlias}}) + require.NoError(t, err) + + envs.On("List", mock.Anything, spaceID).Return([]*environments.Environment{{ID: envID, SpaceID: spaceID, Description: "EnvironmentUPD", Aliases: []string{envAlias}}}, nil).Once() + + vl3, err := svc.List(ctx, spaceID) + require.NoError(t, err) + assert.NotSame(t, vl2[0], vl3[0], "Ожидает что объекты Р±СѓРґСѓС‚ удалены РёР· кэша Рё запрошены РёР· сервиса.") + + envs.AssertExpectations(t) + }) + + t.Run("After Delete", func(t *testing.T) { + envs := &mocksenvironments.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(envs) + + envs.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Description: "Environment", Aliases: []string{envAlias}}, nil).Once() + envs.On("List", mock.Anything, spaceID).Return([]*environments.Environment{{ID: envID, SpaceID: spaceID, Description: "Environment", Aliases: []string{envAlias}}}, nil).Once() + envs.On("Delete", mock.Anything, spaceID, envID).Return(nil).Once() + + v1, err := svc.Get(ctx, spaceID, envID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кэша.") + + v3, err := svc.Get(ctx, spaceID, envAlias) + require.NoError(t, err) + assert.Same(t, v2, v3, "Ожидается получение объекта РёР· кэша РїРѕ Alias.") + + 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], "Ожидается получение объектов РёР· кэша.") + + err = svc.Delete(ctx, spaceID, envID) + require.NoError(t, err) + + envs.On("Get", mock.Anything, spaceID, envID).Return(nil, errNotFound).Once() + envs.On("Get", mock.Anything, spaceID, envAlias).Return(nil, errNotFound).Once() + envs.On("List", mock.Anything, spaceID).Return([]*environments.Environment{}, nil).Once() + + _, err = svc.Get(ctx, spaceID, envID) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидает что элемент был удален РёР· кэша РїРѕ ID Рё получена ошибка РѕС‚ сервиса.") + + _, err = svc.Get(ctx, spaceID, envAlias) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидает что элемент был удален РёР· кэша РїРѕ Alias Рё получена ошибка РѕС‚ сервиса.") + + vl3, err := svc.List(ctx, spaceID) + require.NoError(t, err) + assert.Len(t, vl3, 0, "Ожидает что объекты Р±СѓРґСѓС‚ удалены РёР· кэша Рё запрошены РёР· сервиса.") + + envs.AssertExpectations(t) + }) + + t.Run("After Create", func(t *testing.T) { + envs := &mocksenvironments.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(envs) + + envs.On("List", mock.Anything, spaceID).Return([]*environments.Environment{{ID: envID, SpaceID: spaceID, Description: "Environment"}}, 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], "Ожидается получение объектов РёР· кэша.") + + envs.On("Create", mock.Anything, mock.Anything).Return(&environments.Environment{ID: "envID2", SpaceID: spaceID, Description: "Environment2"}, nil).Once() + _, err = svc.Create(ctx, &environments.Environment{ID: "envID2", SpaceID: spaceID, Description: "Environment2"}) + require.NoError(t, err) + + envs.On("List", mock.Anything, spaceID).Return([]*environments.Environment{{ID: envID, SpaceID: spaceID, Description: "Environment"}, {ID: "envID2", SpaceID: spaceID, Description: "Environment2"}}, nil).Once() + + vl3, err := svc.List(ctx, spaceID) + require.NoError(t, err) + assert.Len(t, vl3, 2, "Ожидает что объекты были удалены РёР· кэша Рё запрошены заново РёР· сервиса.") + + envs.AssertExpectations(t) + }) + + t.Run("After size exceeded", func(t *testing.T) { + envs := &mocksenvironments.Environments{} + + svc := CachingMiddleware(cache.NewCache(1, ttl))(envs) + + envs.On("Get", mock.Anything, spaceID, "envID2").Return(&environments.Environment{ID: "envID2", SpaceID: spaceID, Description: "Environment2"}, nil).Once() + envs.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Description: "Environment"}, nil).Once() + envs.On("Get", mock.Anything, spaceID, "envID2").Return(&environments.Environment{ID: "envID2", SpaceID: spaceID, Description: "Environment2"}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, "envID2") + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, "envID2") + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кэша.") + + _, err = svc.Get(ctx, spaceID, envID) + require.NoError(t, err) + + v5, err := svc.Get(ctx, spaceID, "envID2") + require.NoError(t, err) + assert.NotSame(t, v2, v5, "Ожидает что объект был удален РёР· кэша Рё будет запрошен заново РёР· сервиса.") + + envs.AssertExpectations(t) + }) + + t.Run("After TTL expired", func(t *testing.T) { + envs := &mocksenvironments.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(envs) + + envs.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Description: "Environment"}, nil).Once() + v1, err := svc.Get(ctx, spaceID, envID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кэша.") + + time.Sleep(2 * ttl) + + envs.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Description: "Environment"}, nil).Once() + v3, err := svc.Get(ctx, spaceID, envID) + require.NoError(t, err) + assert.NotSame(t, v2, v3, "Ожидает что объект был удален РёР· кэша Рё будет запрошен заново РёР· сервиса.") + + envs.AssertExpectations(t) + }) + }) +} diff --git a/pkg/environments/middleware/error_logging_middleware.go b/pkg/environments/middleware/error_logging_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..6d6cb544f129f2925d33be91f6666dcf8ebb8936 --- /dev/null +++ b/pkg/environments/middleware/error_logging_middleware.go @@ -0,0 +1,110 @@ +package service + +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/error_log +// gowrap: http://github.com/hexdigest/gowrap + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/environments -i Environments -t ../../../assets/templates/middleware/error_log -o error_logging_middleware.go -l "" + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/environments" + "go.uber.org/zap" +) + +// errorLoggingMiddleware implements environments.Environments that is instrumented with logging +type errorLoggingMiddleware struct { + logger *zap.Logger + next environments.Environments +} + +// ErrorLoggingMiddleware instruments an implementation of the environments.Environments with simple logging +func ErrorLoggingMiddleware(logger *zap.Logger) Middleware { + return func(next environments.Environments) environments.Environments { + return &errorLoggingMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *errorLoggingMiddleware) Create(ctx context.Context, env *environments.Environment) (created *environments.Environment, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Create(ctx, env) +} + +func (m *errorLoggingMiddleware) Delete(ctx context.Context, spaceId string, envId string) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Delete(ctx, spaceId, envId) +} + +func (m *errorLoggingMiddleware) Get(ctx context.Context, spaceId string, envId string) (env *environments.Environment, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Get(ctx, spaceId, envId) +} + +func (m *errorLoggingMiddleware) List(ctx context.Context, spaceId string) (envs []*environments.Environment, 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) Migrate(ctx context.Context, spaceId string, envId string, options ...*environments.MigrateOptions) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Migrate(ctx, spaceId, envId, options...) +} + +func (m *errorLoggingMiddleware) RemoveAlias(ctx context.Context, spaceId string, envId string, alias string) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.RemoveAlias(ctx, spaceId, envId, alias) +} + +func (m *errorLoggingMiddleware) SetAlias(ctx context.Context, spaceId string, envId string, alias string) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.SetAlias(ctx, spaceId, envId, alias) +} + +func (m *errorLoggingMiddleware) Update(ctx context.Context, env *environments.Environment) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Update(ctx, env) +} diff --git a/pkg/environments/middleware/logging_middleware.go b/pkg/environments/middleware/logging_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..26f4eb279126e6a008db8998b83012c8a316ca2f --- /dev/null +++ b/pkg/environments/middleware/logging_middleware.go @@ -0,0 +1,325 @@ +package service + +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/access_log +// gowrap: http://github.com/hexdigest/gowrap + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/environments -i Environments -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/environments" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// loggingMiddleware implements environments.Environments that is instrumented with logging +type loggingMiddleware struct { + logger *zap.Logger + next environments.Environments +} + +// LoggingMiddleware instruments an implementation of the environments.Environments with simple logging +func LoggingMiddleware(logger *zap.Logger) Middleware { + return func(next environments.Environments) environments.Environments { + return &loggingMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *loggingMiddleware) Create(ctx context.Context, env *environments.Environment) (created *environments.Environment, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "env": env} { + 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, env) + + 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, envId string) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "envId": envId} { + 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, envId) + + 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) Get(ctx context.Context, spaceId string, envId string) (env *environments.Environment, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "envId": envId} { + 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...) + + env, err = m.next.Get(ctx, spaceId, envId) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "env": env, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Get.Response", fields...) + + return env, err +} + +func (m *loggingMiddleware) List(ctx context.Context, spaceId string) (envs []*environments.Environment, 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...) + + envs, 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{}{ + "envs": envs, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("List.Response", fields...) + + return envs, err +} + +func (m *loggingMiddleware) Migrate(ctx context.Context, spaceId string, envId string, options ...*environments.MigrateOptions) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "envId": envId, + "options": options} { + 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("Migrate.Request", fields...) + + err = m.next.Migrate(ctx, spaceId, envId, options...) + + 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("Migrate.Response", fields...) + + return err +} + +func (m *loggingMiddleware) RemoveAlias(ctx context.Context, spaceId string, envId string, alias string) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "envId": envId, + "alias": alias} { + 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("RemoveAlias.Request", fields...) + + err = m.next.RemoveAlias(ctx, spaceId, envId, alias) + + 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("RemoveAlias.Response", fields...) + + return err +} + +func (m *loggingMiddleware) SetAlias(ctx context.Context, spaceId string, envId string, alias string) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "envId": envId, + "alias": alias} { + 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("SetAlias.Request", fields...) + + err = m.next.SetAlias(ctx, spaceId, envId, alias) + + 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("SetAlias.Response", fields...) + + return err +} + +func (m *loggingMiddleware) Update(ctx context.Context, env *environments.Environment) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "env": env} { + 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, env) + + 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/environments/middleware/middleware.go b/pkg/environments/middleware/middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..b5e29a99db170df7d2cc5003b2d9a206a423fef2 --- /dev/null +++ b/pkg/environments/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/environments -i Environments -t ../../../assets/templates/middleware/middleware -o middleware.go -l "" + +import ( + "git.perx.ru/perxis/perxis-go/pkg/environments" + "go.uber.org/zap" +) + +type Middleware func(environments.Environments) environments.Environments + +func WithLog(s environments.Environments, logger *zap.Logger, log_access bool) environments.Environments { + if logger == nil { + logger = zap.NewNop() + } + + logger = logger.Named("Environments") + s = ErrorLoggingMiddleware(logger)(s) + if log_access { + s = LoggingMiddleware(logger)(s) + } + s = RecoveringMiddleware(logger)(s) + return s +} diff --git a/pkg/environments/middleware/recovering_middleware.go b/pkg/environments/middleware/recovering_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..bf4bec7d46fa8b3e963c65330c6883c9c7fb0c20 --- /dev/null +++ b/pkg/environments/middleware/recovering_middleware.go @@ -0,0 +1,127 @@ +// 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/environments -i Environments -t ../../../assets/templates/middleware/recovery -o recovering_middleware.go -l "" + +import ( + "context" + "fmt" + + "git.perx.ru/perxis/perxis-go/pkg/environments" + "go.uber.org/zap" +) + +// recoveringMiddleware implements environments.Environments that is instrumented with logging +type recoveringMiddleware struct { + logger *zap.Logger + next environments.Environments +} + +// RecoveringMiddleware instruments an implementation of the environments.Environments with simple logging +func RecoveringMiddleware(logger *zap.Logger) Middleware { + return func(next environments.Environments) environments.Environments { + return &recoveringMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *recoveringMiddleware) Create(ctx context.Context, env *environments.Environment) (created *environments.Environment, 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, env) +} + +func (m *recoveringMiddleware) Delete(ctx context.Context, spaceId string, envId 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, envId) +} + +func (m *recoveringMiddleware) Get(ctx context.Context, spaceId string, envId string) (env *environments.Environment, 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, envId) +} + +func (m *recoveringMiddleware) List(ctx context.Context, spaceId string) (envs []*environments.Environment, 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) Migrate(ctx context.Context, spaceId string, envId string, options ...*environments.MigrateOptions) (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.Migrate(ctx, spaceId, envId, options...) +} + +func (m *recoveringMiddleware) RemoveAlias(ctx context.Context, spaceId string, envId string, alias 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.RemoveAlias(ctx, spaceId, envId, alias) +} + +func (m *recoveringMiddleware) SetAlias(ctx context.Context, spaceId string, envId string, alias 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.SetAlias(ctx, spaceId, envId, alias) +} + +func (m *recoveringMiddleware) Update(ctx context.Context, env *environments.Environment) (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, env) +} 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..b635ee0cd09b28245a8f29c67d7902a08392eba7 --- /dev/null +++ b/pkg/invitations/middleware/caching_middleware_test.go @@ -0,0 +1,129 @@ +package service + +import ( + "context" + "testing" + "time" + + "git.perx.ru/perxis/perxis-go/pkg/cache" + "git.perx.ru/perxis/perxis-go/pkg/errors" + "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 + ) + + errNotFound := errors.NotFound(errors.New("not found")) + + 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, 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, 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..39c823ae6fdf7fb7cb5c181c42099ca16c1a2bad --- /dev/null +++ b/pkg/invitations/middleware/error_logging_middleware.go @@ -0,0 +1,81 @@ +package service + +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/error_log +// gowrap: http://github.com/hexdigest/gowrap + +//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..8f1ceb9959497794cc4bc7a2d6a963a949d9b1a3 --- /dev/null +++ b/pkg/invitations/middleware/logging_middleware.go @@ -0,0 +1,216 @@ +package service + +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/access_log +// gowrap: http://github.com/hexdigest/gowrap + +//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) +} diff --git a/pkg/items/middleware/caching_middleware.go b/pkg/items/middleware/caching_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..0455cb276c5ccae86aeb27619fe55369a735550a --- /dev/null +++ b/pkg/items/middleware/caching_middleware.go @@ -0,0 +1,176 @@ +package service + +import ( + "context" + "strings" + + "git.perx.ru/perxis/perxis-go/pkg/cache" + envService "git.perx.ru/perxis/perxis-go/pkg/environments" + service "git.perx.ru/perxis/perxis-go/pkg/items" +) + +func makeKey(ss ...string) string { + return strings.Join(ss, "-") +} + +func CachingMiddleware(cache, cachePublished *cache.Cache, envs envService.Environments) Middleware { + return func(next service.Items) service.Items { + return &cachingMiddleware{ + cache: cache, + cachePublished: cachePublished, + Items: next, + envs: envs, + } + } +} + +type cachingMiddleware struct { + cache *cache.Cache + cachePublished *cache.Cache + envs envService.Environments + service.Items +} + +func (m cachingMiddleware) Get(ctx context.Context, spaceId, envId, collectionId, itemId string, options ...*service.GetOptions) (itm *service.Item, err error) { + + value, e := m.cache.Get(makeKey(spaceId, envId, collectionId, itemId)) + if e == nil { + return value.(*service.Item), err + } + itm, err = m.Items.Get(ctx, spaceId, envId, collectionId, itemId, options...) + if err == nil { + env, err := m.envs.Get(ctx, itm.SpaceID, itm.EnvID) + if err != nil { + return nil, err + } + m.cache.Set(makeKey(itm.SpaceID, env.ID, itm.CollectionID, itm.ID), itm) + for _, al := range env.Aliases { + m.cache.Set(makeKey(itm.SpaceID, al, itm.CollectionID, itm.ID), itm) + } + } + return itm, err +} + +func (m cachingMiddleware) Update(ctx context.Context, item *service.Item, options ...*service.UpdateOptions) (err error) { + + err = m.Items.Update(ctx, item, options...) + if err == nil { + env, err := m.envs.Get(ctx, item.SpaceID, item.EnvID) + if err != nil { + return err + } + m.cache.Remove(makeKey(item.SpaceID, env.ID, item.CollectionID, item.ID)) + m.cachePublished.Remove(makeKey(item.SpaceID, env.ID, item.CollectionID, item.ID)) + for _, al := range env.Aliases { + m.cache.Remove(makeKey(item.SpaceID, al, item.CollectionID, item.ID)) + m.cachePublished.Remove(makeKey(item.SpaceID, al, item.CollectionID, item.ID)) + } + } + return err +} + +func (m cachingMiddleware) Delete(ctx context.Context, spaceId, envId, collectionId, itemId string, options ...*service.DeleteOptions) (err error) { + + err = m.Items.Delete(ctx, spaceId, envId, collectionId, itemId, options...) + if err == nil { + env, err := m.envs.Get(ctx, spaceId, envId) + if err != nil { + return err + } + m.cache.Remove(makeKey(spaceId, env.ID, collectionId, itemId)) + m.cachePublished.Remove(makeKey(spaceId, env.ID, collectionId, itemId)) + for _, al := range env.Aliases { + m.cache.Remove(makeKey(spaceId, al, collectionId, itemId)) + m.cachePublished.Remove(makeKey(spaceId, al, collectionId, itemId)) + } + + } + return err +} + +func (m cachingMiddleware) Publish(ctx context.Context, item *service.Item, options ...*service.PublishOptions) (err error) { + + err = m.Items.Publish(ctx, item, options...) + if err == nil { + env, err := m.envs.Get(ctx, item.SpaceID, item.EnvID) + if err != nil { + return err + } + m.cache.Remove(makeKey(item.SpaceID, env.ID, item.CollectionID, item.ID)) + m.cachePublished.Remove(makeKey(item.SpaceID, env.ID, item.CollectionID, item.ID)) + for _, al := range env.Aliases { + m.cache.Remove(makeKey(item.SpaceID, al, item.CollectionID, item.ID)) + m.cachePublished.Remove(makeKey(item.SpaceID, al, item.CollectionID, item.ID)) + } + } + return err +} + +func (m cachingMiddleware) Unpublish(ctx context.Context, item *service.Item, options ...*service.UnpublishOptions) (err error) { + + err = m.Items.Unpublish(ctx, item, options...) + if err == nil { + env, err := m.envs.Get(ctx, item.SpaceID, item.EnvID) + if err != nil { + return err + } + m.cache.Remove(makeKey(item.SpaceID, env.ID, item.CollectionID, item.ID)) + m.cachePublished.Remove(makeKey(item.SpaceID, env.ID, item.CollectionID, item.ID)) + for _, al := range env.Aliases { + m.cache.Remove(makeKey(item.SpaceID, al, item.CollectionID, item.ID)) + m.cachePublished.Remove(makeKey(item.SpaceID, al, item.CollectionID, item.ID)) + } + } + return err +} + +func (m cachingMiddleware) GetPublished(ctx context.Context, spaceId, envId, collectionId, itemId string, options ...*service.GetPublishedOptions) (itm *service.Item, err error) { + + opts := service.MergeGetPublishedOptions(options...) + + val, e := m.cachePublished.Get(makeKey(spaceId, envId, collectionId, itemId)) + if e == nil { + value := val.(map[string]*service.Item) + if i, ok := value[opts.LocaleID]; ok { + return i, nil + } + } + + itm, err = m.Items.GetPublished(ctx, spaceId, envId, collectionId, itemId, opts) + + if err == nil { + env, err := m.envs.Get(ctx, itm.SpaceID, itm.EnvID) + if err != nil { + return nil, err + } + var value = make(map[string]*service.Item) + if val != nil { + value = val.(map[string]*service.Item) + } + value[opts.LocaleID] = itm + m.cachePublished.Set(makeKey(itm.SpaceID, env.ID, itm.CollectionID, itm.ID), value) + for _, al := range env.Aliases { + m.cachePublished.Set(makeKey(itm.SpaceID, al, itm.CollectionID, itm.ID), value) + } + } + + return itm, err +} + +func (m cachingMiddleware) Archive(ctx context.Context, item *service.Item, options ...*service.ArchiveOptions) (err error) { + + err = m.Items.Archive(ctx, item, options...) + if err == nil { + env, err := m.envs.Get(ctx, item.SpaceID, item.EnvID) + if err != nil { + return err + } + m.cache.Remove(makeKey(item.SpaceID, env.ID, item.CollectionID, item.ID)) + m.cachePublished.Remove(makeKey(item.SpaceID, env.ID, item.CollectionID, item.ID)) + for _, al := range env.Aliases { + m.cache.Remove(makeKey(item.SpaceID, al, item.CollectionID, item.ID)) + m.cachePublished.Remove(makeKey(item.SpaceID, al, item.CollectionID, item.ID)) + } + } + return err +} diff --git a/pkg/items/middleware/caching_middleware_test.go b/pkg/items/middleware/caching_middleware_test.go new file mode 100644 index 0000000000000000000000000000000000000000..9e02b49beb80507ef7fbf25d3c67d5c8a022ce1b --- /dev/null +++ b/pkg/items/middleware/caching_middleware_test.go @@ -0,0 +1,685 @@ +package service + +import ( + "context" + "testing" + "time" + + "git.perx.ru/perxis/perxis-go/pkg/cache" + "git.perx.ru/perxis/perxis-go/pkg/environments" + envmocks "git.perx.ru/perxis/perxis-go/pkg/environments/mocks" + "git.perx.ru/perxis/perxis-go/pkg/errors" + "git.perx.ru/perxis/perxis-go/pkg/items" + itmsmocks "git.perx.ru/perxis/perxis-go/pkg/items/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestItemsCache(t *testing.T) { + + const ( + colID = "colID" + spaceID = "spaceID" + envID = "envID" + envAlias = "envAlias" + itemID = "itemID" + locID = "locID" + size = 5 + ttl = 20 * time.Millisecond + ) + + errNotFound := errors.NotFound(errors.New("not found")) + + ctx := context.Background() + + t.Run("Get from cache", func(t *testing.T) { + itms := &itmsmocks.Items{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, envID, colID, itemID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envID, colID, itemID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кеша, РїСЂРё повторном запросе.") + + v3, err := svc.Get(ctx, spaceID, envAlias, colID, itemID) + assert.Same(t, v3, v2, "Ожидается получение объекта РёР· кеша, РїСЂРё запросе того Р¶Рµ объекта РїРѕ alias окружения.") + require.NoError(t, err) + + env.AssertExpectations(t) + itms.AssertExpectations(t) + }) + + t.Run("Get from cache(by Alias)", func(t *testing.T) { + itms := &itmsmocks.Items{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + itms.On("Get", mock.Anything, spaceID, envAlias, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, envAlias, colID, itemID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envAlias, colID, itemID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кеша, РїСЂРё повторном запросе.") + + v3, err := svc.Get(ctx, spaceID, envID, colID, itemID) + assert.Same(t, v3, v2, "Ожидается получение объекта РёР· кеша, РїСЂРё запросе того Р¶Рµ объекта РїРѕ ID окружения.") + require.NoError(t, err) + + env.AssertExpectations(t) + itms.AssertExpectations(t) + }) + + t.Run("GetPublished from cache", func(t *testing.T) { + itms := &itmsmocks.Items{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + itms.On("GetPublished", mock.Anything, spaceID, envID, colID, itemID, mock.Anything).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}, nil).Once() + + v1, err := svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID}) + require.NoError(t, err) + + v2, err := svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID}) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кеша.") + + v3, err := svc.GetPublished(ctx, spaceID, envAlias, colID, itemID, &items.GetPublishedOptions{LocaleID: locID}) + require.NoError(t, err) + assert.Same(t, v2, v3, "Ожидается получение объекта РёР· кеша, РїСЂРё запросе того Р¶Рµ объекта РїРѕ alias окружения.") + + env.AssertExpectations(t) + itms.AssertExpectations(t) + }) + + t.Run("GetPublished from cache(by Alias)", func(t *testing.T) { + itms := &itmsmocks.Items{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + itms.On("GetPublished", mock.Anything, spaceID, envAlias, colID, itemID, mock.Anything).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}, nil).Once() + + v1, err := svc.GetPublished(ctx, spaceID, envAlias, colID, itemID, &items.GetPublishedOptions{LocaleID: locID}) + require.NoError(t, err) + + v2, err := svc.GetPublished(ctx, spaceID, envAlias, colID, itemID, &items.GetPublishedOptions{LocaleID: locID}) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кеша.") + + v3, err := svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID}) + require.NoError(t, err) + assert.Same(t, v2, v3, "Ожидается получение объекта РёР· кеша, РїСЂРё запросе того Р¶Рµ объекта РїРѕ ID окружения.") + + env.AssertExpectations(t) + itms.AssertExpectations(t) + }) + + t.Run("GetPublished from cache (with different locales)", func(t *testing.T) { + const ( + loc1 = "loc1" + loc2 = "loc2" + ) + + itms := &itmsmocks.Items{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Twice() + itms.On("GetPublished", mock.Anything, spaceID, envAlias, colID, itemID, mock.Anything).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}, nil).Once() + itms.On("GetPublished", mock.Anything, spaceID, envAlias, colID, itemID, mock.Anything).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}, nil).Once() + + v1loc1, err := svc.GetPublished(ctx, spaceID, envAlias, colID, itemID, &items.GetPublishedOptions{LocaleID: loc1}) + require.NoError(t, err, "Ожидается получение объекта РёР· сервиса Рё добавление его РІ кеш СЃ loc1.") + + v1loc2, err := svc.GetPublished(ctx, spaceID, envAlias, colID, itemID, &items.GetPublishedOptions{LocaleID: loc2}) + require.NoError(t, err, "Ожидается получение объекта РёР· сервиса Рё добавление его РІ кеш СЃ loc2 вместе СЃ loc1.") + + v2loc1, err := svc.GetPublished(ctx, spaceID, envAlias, colID, itemID, &items.GetPublishedOptions{LocaleID: loc1}) + require.NoError(t, err) + assert.Same(t, v1loc1, v2loc1, "Ожидается получение объекта c локализацией loc1 РёР· кеша.") + + v2loc2, err := svc.GetPublished(ctx, spaceID, envAlias, colID, itemID, &items.GetPublishedOptions{LocaleID: loc2}) + require.NoError(t, err) + assert.Same(t, v1loc2, v2loc2, "Ожидается получение объекта c локализацией loc2 РёР· кеша.") + + v3loc1, err := svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: loc1}) + require.NoError(t, err) + assert.Same(t, v2loc1, v3loc1, "Ожидается получение объекта c локализацией loc1 РёР· кеша, РїСЂРё запросе того Р¶Рµ объекта РїРѕ ID окружения.") + + v3loc2, err := svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: loc2}) + require.NoError(t, err) + assert.Same(t, v2loc2, v3loc2, "Ожидается получение объекта c локализацией loc2 РёР· кеша, РїСЂРё запросе того Р¶Рµ объекта РїРѕ ID окружения.") + + env.AssertExpectations(t) + itms.AssertExpectations(t) + }) + + t.Run("Invalidate cache", func(t *testing.T) { + t.Run("After Update(Get)", func(t *testing.T) { + itms := &itmsmocks.Items{} + env := &envmocks.Environments{} + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms) + + itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, Data: map[string]interface{}{"f1": "d1"}}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, envID, colID, itemID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envID, colID, itemID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кеша.") + + itms.On("Update", mock.Anything, mock.Anything).Return(nil).Once() + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + + err = svc.Update(ctx, &items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, Data: map[string]interface{}{"f1": "d2"}}) + require.NoError(t, err) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, Data: map[string]interface{}{"f1": "d2"}}, nil).Once() + + v3, err := svc.Get(ctx, spaceID, envID, colID, itemID) + require.NoError(t, err) + assert.NotSame(t, v3, v2, "Ожидается удаление объекта РёР· кэша после обновления Рё получение его заново РёР· сервиса.") + + env.AssertExpectations(t) + itms.AssertExpectations(t) + }) + + t.Run("After Archive(Get)", func(t *testing.T) { + itms := &itmsmocks.Items{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, envID, colID, itemID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envID, colID, itemID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кеша.") + + itms.On("Archive", mock.Anything, mock.Anything).Return(nil).Once() + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + + err = svc.Archive(ctx, &items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}) + require.NoError(t, err) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateArchived}, nil).Once() + + v3, err := svc.Get(ctx, spaceID, envID, colID, itemID) + require.NoError(t, err) + assert.NotSame(t, v3, v2, "Ожидается удаление объекта РёР· кэша после архивации Рё получение РёР· сервиса.") + + env.AssertExpectations(t) + itms.AssertExpectations(t) + }) + + t.Run("After Publish(Get)", func(t *testing.T) { + itms := &itmsmocks.Items{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, envID, colID, itemID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envID, colID, itemID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кеша.") + + itms.On("Publish", mock.Anything, mock.Anything).Return(nil).Once() + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + err = svc.Publish(ctx, &items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}) + require.NoError(t, err) + + itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StatePublished}, nil).Once() + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + + v3, err := svc.Get(ctx, spaceID, envID, colID, itemID) + require.NoError(t, err) + assert.NotSame(t, v3, v2, "Ожидается удаление объекта РёР· кэша после публикации Рё получение заново РёР· сервиса.") + + env.AssertExpectations(t) + itms.AssertExpectations(t) + }) + + t.Run("After Delete", func(t *testing.T) { + itms := &itmsmocks.Items{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StatePublished}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, envID, colID, itemID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envID, colID, itemID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кеша РїСЂРё повторном запросе.") + + v3, err := svc.Get(ctx, spaceID, envAlias, colID, itemID) + require.NoError(t, err) + assert.Same(t, v2, v3, "Ожидается получение объекта РёР· кеша РїРѕ alias окружения.") + + itms.On("Delete", mock.Anything, spaceID, envID, colID, itemID).Return(nil).Once() + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + + err = svc.Delete(ctx, spaceID, envID, colID, itemID) + require.NoError(t, err) + + itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(nil, errNotFound).Once() + itms.On("Get", mock.Anything, spaceID, envAlias, colID, itemID).Return(nil, errNotFound).Once() + _, err = svc.Get(ctx, spaceID, envID, colID, itemID) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидается удаление РёР· кэша после удаления объекта Рё получение ошибки РѕС‚ сервиса.") + + _, err = svc.Get(ctx, spaceID, envAlias, colID, itemID) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидается удаление РёР· кэша после удаления объекта Рё получение ошибки РѕС‚ сервиса.") + + env.AssertExpectations(t) + itms.AssertExpectations(t) + }) + + t.Run("After Unpublish(Get)", func(t *testing.T) { + itms := &itmsmocks.Items{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StatePublished}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, envID, colID, itemID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envID, colID, itemID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кеша.") + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + itms.On("Unpublish", mock.Anything, mock.Anything).Return(nil).Once() + + err = svc.Unpublish(ctx, &items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StatePublished}) + require.NoError(t, err) + + itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}, nil).Once() + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + + v3, err := svc.Get(ctx, spaceID, envID, colID, itemID) + require.NoError(t, err) + assert.NotSame(t, v3, v2, "Ожидается удаление объекта РёР· кэша после снятия СЃ публикации Рё получение заново РёР· сервиса.") + + env.AssertExpectations(t) + itms.AssertExpectations(t) + }) + + t.Run("After Publish(Get by Alias)", func(t *testing.T) { + itms := &itmsmocks.Items{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, envID, colID, itemID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envAlias, colID, itemID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кеша РїРѕ alias окружения.") + + itms.On("Publish", mock.Anything, mock.Anything).Return(nil).Once() + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + + err = svc.Publish(ctx, &items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}) + require.NoError(t, err) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + itms.On("Get", mock.Anything, spaceID, envAlias, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StatePublished}, nil).Once() + + v3, err := svc.Get(ctx, spaceID, envAlias, colID, itemID) + require.NoError(t, err) + assert.NotSame(t, v3, v2, "Ожидается удаление объекта РёР· кэша после публикации Рё получение РёР· сервиса РїРѕ alias окружения.") + + env.AssertExpectations(t) + itms.AssertExpectations(t) + }) + + t.Run("After Update(Get by Alias)", func(t *testing.T) { + itms := &itmsmocks.Items{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, Data: map[string]interface{}{"f1": "d1"}}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, envID, colID, itemID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envAlias, colID, itemID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кеша РїРѕ alias окружения.") + + itms.On("Update", mock.Anything, mock.Anything).Return(nil).Once() + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + + err = svc.Update(ctx, &items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, Data: map[string]interface{}{"f1": "d2"}}) + require.NoError(t, err) + + itms.On("Get", mock.Anything, spaceID, envAlias, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, Data: map[string]interface{}{"f1": "d2"}}, nil).Once() + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + + v3, err := svc.Get(ctx, spaceID, envAlias, colID, itemID) + require.NoError(t, err) + assert.NotSame(t, v3, v2, "Ожидается удаление объекта РёР· кэша РїСЂРё обновлении Рё получение РёР· сервиса РїРѕ alias окружения.") + + env.AssertExpectations(t) + itms.AssertExpectations(t) + }) + + t.Run("After Unpublish(Get by Alias)", func(t *testing.T) { + itms := &itmsmocks.Items{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StatePublished}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, envID, colID, itemID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envAlias, colID, itemID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кеша РїРѕ alias окружения.") + + itms.On("Unpublish", mock.Anything, mock.Anything).Return(nil).Once() + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + + err = svc.Unpublish(ctx, &items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StatePublished}) + require.NoError(t, err) + + itms.On("Get", mock.Anything, spaceID, envAlias, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}, nil).Once() + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + + v3, err := svc.Get(ctx, spaceID, envAlias, colID, itemID) + require.NoError(t, err) + assert.NotSame(t, v3, v2, "Ожидается удаление объекта РёР· кэша после снятия СЃ публикации Рё получение РёР· сервиса РїРѕ alias окружения.") + + env.AssertExpectations(t) + itms.AssertExpectations(t) + }) + + t.Run("After Update(GetPublished)", func(t *testing.T) { + itms := &itmsmocks.Items{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + itms.On("GetPublished", mock.Anything, spaceID, envID, colID, itemID, mock.Anything).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StatePublished, Data: map[string]interface{}{"f1": "d1"}}, nil).Once() + + v1, err := svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID}) + require.NoError(t, err) + + v2, err := svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID}) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кеша.") + + v3, err := svc.GetPublished(ctx, spaceID, envAlias, colID, itemID, &items.GetPublishedOptions{LocaleID: locID}) + require.NoError(t, err) + assert.Same(t, v2, v3, "Ожидается получение объекта РёР· кеша РїРѕ Рѕ alias окружения.") + + itms.On("Update", mock.Anything, mock.Anything).Return(nil).Once() + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + + err = svc.Update(ctx, &items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StatePublished, Data: map[string]interface{}{"f1": "d2"}}) + require.NoError(t, err) + + itms.On("GetPublished", mock.Anything, spaceID, envID, colID, itemID, mock.Anything).Return(nil, errNotFound).Once() + itms.On("GetPublished", mock.Anything, spaceID, envAlias, colID, itemID, mock.Anything).Return(nil, errNotFound).Once() + + _, err = svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID}) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидается удаление объекта РёР· кэша РїРѕ ID окружения после его обновления Рё получение ошибки РѕС‚ сервиса.") + + _, err = svc.GetPublished(ctx, spaceID, envAlias, colID, itemID, &items.GetPublishedOptions{LocaleID: locID}) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидается удаление объекта РёР· кэша РїРѕ alias окружения после его обновления Рё получение ошибки РѕС‚ сервиса.") + + env.AssertExpectations(t) + itms.AssertExpectations(t) + }) + + t.Run("After Archive(GetPublished)", func(t *testing.T) { + itms := &itmsmocks.Items{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + itms.On("GetPublished", mock.Anything, spaceID, envID, colID, itemID, mock.Anything).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}, nil).Once() + + v1, err := svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID}) + require.NoError(t, err) + + v2, err := svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID}) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кеша.") + + v3, err := svc.GetPublished(ctx, spaceID, envAlias, colID, itemID, &items.GetPublishedOptions{LocaleID: locID}) + require.NoError(t, err) + assert.Same(t, v2, v3, "Ожидается получение объекта РёР· кеша РїРѕ Рѕ alias окружения.") + + itms.On("Archive", mock.Anything, mock.Anything).Return(nil).Once() + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + + err = svc.Archive(ctx, &items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}) + require.NoError(t, err) + + itms.On("GetPublished", mock.Anything, spaceID, envID, colID, itemID, mock.Anything).Return(nil, errNotFound).Once() + itms.On("GetPublished", mock.Anything, spaceID, envAlias, colID, itemID, mock.Anything).Return(nil, errNotFound).Once() + + _, err = svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID}) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидается удаление объекта РёР· кэша РїРѕ ID окружения после его архивации Рё получение ошибки РѕС‚ сервиса.") + + _, err = svc.GetPublished(ctx, spaceID, envAlias, colID, itemID, &items.GetPublishedOptions{LocaleID: locID}) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидается удаление объекта РёР· кэша РїРѕ alias окружения после его архивации Рё получение ошибки РѕС‚ сервиса.") + + env.AssertExpectations(t) + itms.AssertExpectations(t) + }) + + t.Run("After Delete(GetPublished)", func(t *testing.T) { + itms := &itmsmocks.Items{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + itms.On("GetPublished", mock.Anything, spaceID, envID, colID, itemID, mock.Anything).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StatePublished}, nil).Once() + + v1, err := svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID}) + require.NoError(t, err) + + v2, err := svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID}) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кеша.") + + v3, err := svc.GetPublished(ctx, spaceID, envAlias, colID, itemID, &items.GetPublishedOptions{LocaleID: locID}) + require.NoError(t, err) + assert.Same(t, v2, v3, "Ожидается получение объекта РёР· кеша РїРѕ Рѕ alias окружения.") + + itms.On("Delete", mock.Anything, spaceID, envID, colID, itemID).Return(nil).Once() + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + + err = svc.Delete(ctx, spaceID, envID, colID, itemID) + require.NoError(t, err) + + itms.On("GetPublished", mock.Anything, spaceID, envID, colID, itemID, mock.Anything).Return(nil, errNotFound).Once() + itms.On("GetPublished", mock.Anything, spaceID, envAlias, colID, itemID, mock.Anything).Return(nil, errNotFound).Once() + + _, err = svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID}) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидается удаление объекта РёР· кэша после удаления РёР· хранилища Рё получение ошибки РѕС‚ сервиса.") + + _, err = svc.GetPublished(ctx, spaceID, envAlias, colID, itemID, &items.GetPublishedOptions{LocaleID: locID}) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидается очистка кеша РїРѕ alias окружения после удаления объекта Рё получение ошибки РѕС‚ сервиса.") + + env.AssertExpectations(t) + itms.AssertExpectations(t) + }) + + t.Run("After Unpublish(GetPublished)", func(t *testing.T) { + itms := &itmsmocks.Items{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + itms.On("GetPublished", mock.Anything, spaceID, envID, colID, itemID, mock.Anything).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StatePublished}, nil).Once() + + v1, err := svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID}) + require.NoError(t, err) + + v2, err := svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID}) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кеша.") + + v3, err := svc.GetPublished(ctx, spaceID, envAlias, colID, itemID, &items.GetPublishedOptions{LocaleID: locID}) + require.NoError(t, err) + assert.Same(t, v2, v3, "Ожидается получение объекта РёР· кеша РїРѕ Рѕ alias окружения.") + + itms.On("Unpublish", mock.Anything, mock.Anything).Return(nil).Once() + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + + err = svc.Unpublish(ctx, &items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StatePublished}) + require.NoError(t, err) + + itms.On("GetPublished", mock.Anything, spaceID, envID, colID, itemID, mock.Anything).Return(nil, errNotFound).Once() + itms.On("GetPublished", mock.Anything, spaceID, envAlias, colID, itemID, mock.Anything).Return(nil, errNotFound).Once() + + _, err = svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID}) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидается удаление объекта РёР· кэша РїРѕ ID окружения после снятия СЃ публикации Рё получение ошибки РѕС‚ сервиса.") + + _, err = svc.GetPublished(ctx, spaceID, envAlias, colID, itemID, &items.GetPublishedOptions{LocaleID: locID}) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидается удаление объекта РёР· кэша РїРѕ alias окружения после снятия СЃ публикации Рё получение ошибки РѕС‚ сервиса.") + + env.AssertExpectations(t) + itms.AssertExpectations(t) + }) + + t.Run("After Unpublish by Alias", func(t *testing.T) { + itms := &itmsmocks.Items{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + env.On("Get", mock.Anything, spaceID, envAlias).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + + itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StatePublished}, nil).Once() + itms.On("GetPublished", mock.Anything, spaceID, envID, colID, itemID, mock.Anything).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StatePublished}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, envID, colID, itemID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envID, colID, itemID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кеша.") + + v3, err := svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID}) + require.NoError(t, err) + + v4, err := svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID}) + require.NoError(t, err) + assert.Same(t, v3, v4, "Ожидается получение опубликованного объекта РёР· кеша.") + + itms.On("Unpublish", mock.Anything, mock.Anything).Return(nil).Once() + err = svc.Unpublish(ctx, &items.Item{ID: itemID, SpaceID: spaceID, EnvID: envAlias, CollectionID: colID, State: items.StatePublished}) + require.NoError(t, err) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + + itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}, nil).Once() + itms.On("GetPublished", mock.Anything, spaceID, envAlias, colID, itemID, mock.Anything).Return(nil, errNotFound).Once() + + v5, err := svc.Get(ctx, spaceID, envID, colID, itemID) + require.NoError(t, err) + assert.NotSame(t, v5, v2, "Ожидается удаление объекта РёР· кэша Рё получение заново РёР· сервиса.") + + _, err = svc.GetPublished(ctx, spaceID, envAlias, colID, itemID, &items.GetPublishedOptions{LocaleID: locID}) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидается удаление объекта РёР· кэша Рё получение ошибки РѕС‚ сервиса РёР· сервиса.") + + env.AssertExpectations(t) + itms.AssertExpectations(t) + }) + + t.Run("After TTL expired", func(t *testing.T) { + itms := &itmsmocks.Items{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, envID, colID, itemID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envID, colID, itemID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кеша.") + + time.Sleep(2 * ttl) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}, nil).Once() + + v3, err := svc.Get(ctx, spaceID, envID, colID, itemID) + require.NoError(t, err) + assert.NotSame(t, v2, v3, "Ожидается удаление объекта РёР· кэша Рё получение РёР· сервиса.") + + env.AssertExpectations(t) + itms.AssertExpectations(t) + }) + }) +} diff --git a/pkg/items/middleware/client_encode_middleware.go b/pkg/items/middleware/client_encode_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..3aaa6b94badd9726e74d1cb3b0abbd02893f5838 --- /dev/null +++ b/pkg/items/middleware/client_encode_middleware.go @@ -0,0 +1,317 @@ +package service + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/collections" + "git.perx.ru/perxis/perxis-go/pkg/errors" + "git.perx.ru/perxis/perxis-go/pkg/items" + "git.perx.ru/perxis/perxis-go/pkg/schema" +) + +// ClientEncodeMiddleware выполняет операции encode/decode для передаваемых данных +func ClientEncodeMiddleware(colls collections.Collections) Middleware { + return func(items items.Items) items.Items { + return &encodeDecodeMiddleware{ + next: items, + colls: colls, + } + + } +} + +type encodeDecodeMiddleware struct { + next items.Items + colls collections.Collections +} + +func (m *encodeDecodeMiddleware) Introspect(ctx context.Context, item *items.Item, opts ...*items.IntrospectOptions) (itm *items.Item, sch *schema.Schema, err error) { + coll, err := m.colls.Get(ctx, item.SpaceID, item.EnvID, item.CollectionID) + if err != nil { + return nil, nil, err + } + + if item, err = item.Encode(ctx, coll.Schema); err != nil { + return + } + + itm, sch, err = m.next.Introspect(ctx, item, opts...) + if itm != nil && sch != nil { + var err error + if itm, err = itm.Decode(ctx, sch); err != nil { + return nil, nil, err + } + } + return itm, sch, err + +} + +func (m *encodeDecodeMiddleware) Create(ctx context.Context, item *items.Item, opts ...*items.CreateOptions) (created *items.Item, err error) { + + var col *collections.Collection + + if item != nil && (item.Data != nil || item.Translations != nil) { + + col, err = m.colls.Get(ctx, item.SpaceID, item.EnvID, item.CollectionID) + if err != nil { + return nil, err + } + + if item, err = item.Encode(ctx, col.Schema); err != nil { + return nil, err + } + } + + res, err := m.next.Create(ctx, item, opts...) + if err == nil && (res.Data != nil || res.Translations != nil) { + + if col == nil { + col, err = m.colls.Get(ctx, item.SpaceID, item.EnvID, item.CollectionID) + if err != nil { + return nil, err + } + } + + res, err = res.Decode(ctx, col.Schema) + } + + return res, err +} + +func (m *encodeDecodeMiddleware) Update(ctx context.Context, upd *items.Item, options ...*items.UpdateOptions) (err error) { + var col *collections.Collection + if upd != nil && (upd.Data != nil || upd.Translations != nil) { + col, err = m.colls.Get(ctx, upd.SpaceID, upd.EnvID, upd.CollectionID) + if err != nil { + return err + } + if upd, err = upd.Encode(ctx, col.Schema); err != nil { + return err + } + } + return m.next.Update(ctx, upd, options...) +} + +func (m *encodeDecodeMiddleware) Find(ctx context.Context, spaceId, envId, collectionId string, filter *items.Filter, options ...*items.FindOptions) (items []*items.Item, total int, err error) { + items, total, err = m.next.Find(ctx, spaceId, envId, collectionId, filter, options...) + if err == nil && total > 0 { + col, err := m.colls.Get(ctx, spaceId, envId, collectionId) + if err != nil { + return nil, 0, err + } + for i, itm := range items { + itm, err = itm.Decode(ctx, col.Schema) + if err != nil { + return nil, 0, err + } + + items[i] = itm + } + } + return +} + +func (m *encodeDecodeMiddleware) Get(ctx context.Context, spaceId, envId, collectionId, itemId string, options ...*items.GetOptions) (item *items.Item, err error) { + item, err = m.next.Get(ctx, spaceId, envId, collectionId, itemId, options...) + if err == nil && item != nil { + col, err := m.colls.Get(ctx, spaceId, envId, collectionId) + if err != nil { + return nil, err + } + item, err = item.Decode(ctx, col.Schema) + if err != nil { + return nil, err + + } + } + return +} + +func (m *encodeDecodeMiddleware) Publish(ctx context.Context, item *items.Item, opts ...*items.PublishOptions) (err error) { + if item != nil && (item.Data != nil || item.Translations != nil) { + col, err := m.colls.Get(ctx, item.SpaceID, item.EnvID, item.CollectionID) + if err != nil { + return err + } + + if item, err = item.Encode(ctx, col.Schema); err != nil { + return err + } + } + + return m.next.Publish(ctx, item, opts...) +} + +func (m *encodeDecodeMiddleware) Unpublish(ctx context.Context, item *items.Item, opts ...*items.UnpublishOptions) (err error) { + if item != nil && (item.Data != nil || item.Translations != nil) { + col, err := m.colls.Get(ctx, item.SpaceID, item.EnvID, item.CollectionID) + if err != nil { + return err + } + + if item, err = item.Encode(ctx, col.Schema); err != nil { + return err + } + } + + return m.next.Unpublish(ctx, item, opts...) +} + +func (m *encodeDecodeMiddleware) GetPublished(ctx context.Context, spaceId, envId, collectionId, itemId string, options ...*items.GetPublishedOptions) (item *items.Item, err error) { + item, err = m.next.GetPublished(ctx, spaceId, envId, collectionId, itemId, options...) + if err == nil && item != nil { + col, err := m.colls.Get(ctx, spaceId, envId, collectionId) + if err != nil { + return nil, err + } + item, err = item.Decode(ctx, col.Schema) + if err != nil { + return nil, err + + } + } + return +} + +func (m *encodeDecodeMiddleware) FindPublished(ctx context.Context, spaceId, envId, collectionId string, filter *items.Filter, options ...*items.FindPublishedOptions) (items []*items.Item, total int, err error) { + items, total, err = m.next.FindPublished(ctx, spaceId, envId, collectionId, filter, options...) + if err == nil && total > 0 { + col, err := m.colls.Get(ctx, spaceId, envId, collectionId) + if err != nil { + return nil, 0, err + } + for i, itm := range items { + itm, err = itm.Decode(ctx, col.Schema) + if err != nil { + return nil, 0, err + } + + items[i] = itm + } + } + return +} + +func (m *encodeDecodeMiddleware) GetRevision(ctx context.Context, spaceId, envId, collectionId, itemId, revisionId string, options ...*items.GetRevisionOptions) (item *items.Item, err error) { + item, err = m.next.GetRevision(ctx, spaceId, envId, collectionId, itemId, revisionId, options...) + if err == nil && item != nil { + col, err := m.colls.Get(ctx, spaceId, envId, collectionId) + if err != nil { + return nil, err + } + item, err = item.Decode(ctx, col.Schema) + if err != nil { + return nil, err + + } + } + return +} + +func (m *encodeDecodeMiddleware) ListRevisions(ctx context.Context, spaceId, envId, collectionId, itemId string, options ...*items.ListRevisionsOptions) (items []*items.Item, err error) { + items, err = m.next.ListRevisions(ctx, spaceId, envId, collectionId, itemId, options...) + if err == nil && len(items) > 0 { + col, err := m.colls.Get(ctx, spaceId, envId, collectionId) + if err != nil { + return nil, err + } + for i, itm := range items { + itm, err = itm.Decode(ctx, col.Schema) + if err != nil { + return nil, err + } + + items[i] = itm + } + } + return +} + +func (m *encodeDecodeMiddleware) FindArchived(ctx context.Context, spaceId, envId, collectionId string, filter *items.Filter, options ...*items.FindArchivedOptions) (items []*items.Item, total int, err error) { + items, total, err = m.next.FindArchived(ctx, spaceId, envId, collectionId, filter, options...) + if err == nil && total > 0 { + col, err := m.colls.Get(ctx, spaceId, envId, collectionId) + if err != nil { + return nil, 0, err + } + for i, itm := range items { + itm, err = itm.Decode(ctx, col.Schema) + if err != nil { + return nil, 0, err + } + + items[i] = itm + } + } + return +} + +func (m *encodeDecodeMiddleware) Archive(ctx context.Context, item *items.Item, opts ...*items.ArchiveOptions) (err error) { + if item != nil && (item.Data != nil || item.Translations != nil) { + col, err := m.colls.Get(ctx, item.SpaceID, item.EnvID, item.CollectionID) + if err != nil { + return err + } + + if item, err = item.Encode(ctx, col.Schema); err != nil { + return err + } + } + + return m.next.Archive(ctx, item, opts...) +} + +func (m *encodeDecodeMiddleware) Unarchive(ctx context.Context, item *items.Item, opts ...*items.UnarchiveOptions) (err error) { + if item != nil && (item.Data != nil || item.Translations != nil) { + col, err := m.colls.Get(ctx, item.SpaceID, item.EnvID, item.CollectionID) + if err != nil { + return err + } + + if item, err = item.Encode(ctx, col.Schema); err != nil { + return err + } + } + + return m.next.Unarchive(ctx, item, opts...) +} + +func (m *encodeDecodeMiddleware) Delete(ctx context.Context, spaceId, envId, collectionId, itemId string, options ...*items.DeleteOptions) (err error) { + return m.next.Delete(ctx, spaceId, envId, collectionId, itemId, options...) +} + +func (m *encodeDecodeMiddleware) Undelete(ctx context.Context, spaceId, envId, collectionId, itemId string, options ...*items.UndeleteOptions) (err error) { + return m.next.Undelete(ctx, spaceId, envId, collectionId, itemId, options...) +} + +func (m *encodeDecodeMiddleware) Aggregate(ctx context.Context, spaceId, envId, collectionId string, filter *items.Filter, options ...*items.AggregateOptions) (result map[string]interface{}, err error) { + res, err := m.next.Aggregate(ctx, spaceId, envId, collectionId, filter, options...) + if len(res) > 0 && len(options) > 0 { + col, err := m.colls.Get(ctx, spaceId, envId, collectionId) + if err != nil { + return nil, errors.Wrap(err, "encode aggregate result") + } + o := items.MergeAggregateOptions(options...) + res, err = items.DecodeAggregateResult(ctx, o.Fields, res, col.Schema) + if err != nil { + return nil, errors.Wrap(err, "encode aggregate result") + } + } + return res, err +} + +func (m *encodeDecodeMiddleware) AggregatePublished(ctx context.Context, spaceId, envId, collectionId string, filter *items.Filter, options ...*items.AggregatePublishedOptions) (result map[string]interface{}, err error) { + res, err := m.next.AggregatePublished(ctx, spaceId, envId, collectionId, filter, options...) + if len(res) > 0 && len(options) > 0 { + col, err := m.colls.Get(ctx, spaceId, envId, collectionId) + if err != nil { + return nil, errors.Wrap(err, "get collection") + } + o := items.MergeAggregatePublishedOptions(options...) + res, err = items.DecodeAggregateResult(ctx, o.Fields, res, col.Schema) + if err != nil { + return nil, err + } + } + return res, err +} diff --git a/pkg/items/middleware/error_logging_middleware.go b/pkg/items/middleware/error_logging_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..97967808d150cf951f3fb22b16b2836765fc611f --- /dev/null +++ b/pkg/items/middleware/error_logging_middleware.go @@ -0,0 +1,211 @@ +package service + +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/error_log +// gowrap: http://github.com/hexdigest/gowrap + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/items -i Items -t ../../../assets/templates/middleware/error_log -o error_logging_middleware.go -l "" + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/items" + "git.perx.ru/perxis/perxis-go/pkg/schema" + "go.uber.org/zap" +) + +// errorLoggingMiddleware implements items.Items that is instrumented with logging +type errorLoggingMiddleware struct { + logger *zap.Logger + next items.Items +} + +// ErrorLoggingMiddleware instruments an implementation of the items.Items with simple logging +func ErrorLoggingMiddleware(logger *zap.Logger) Middleware { + return func(next items.Items) items.Items { + return &errorLoggingMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *errorLoggingMiddleware) Aggregate(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.AggregateOptions) (result map[string]interface{}, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Aggregate(ctx, spaceId, envId, collectionId, filter, options...) +} + +func (m *errorLoggingMiddleware) AggregatePublished(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.AggregatePublishedOptions) (result map[string]interface{}, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.AggregatePublished(ctx, spaceId, envId, collectionId, filter, options...) +} + +func (m *errorLoggingMiddleware) Archive(ctx context.Context, item *items.Item, options ...*items.ArchiveOptions) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Archive(ctx, item, options...) +} + +func (m *errorLoggingMiddleware) Create(ctx context.Context, item *items.Item, opts ...*items.CreateOptions) (created *items.Item, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Create(ctx, item, opts...) +} + +func (m *errorLoggingMiddleware) Delete(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.DeleteOptions) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Delete(ctx, spaceId, envId, collectionId, itemId, options...) +} + +func (m *errorLoggingMiddleware) Find(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.FindOptions) (items []*items.Item, total int, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Find(ctx, spaceId, envId, collectionId, filter, options...) +} + +func (m *errorLoggingMiddleware) FindArchived(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.FindArchivedOptions) (items []*items.Item, total int, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.FindArchived(ctx, spaceId, envId, collectionId, filter, options...) +} + +func (m *errorLoggingMiddleware) FindPublished(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.FindPublishedOptions) (items []*items.Item, total int, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.FindPublished(ctx, spaceId, envId, collectionId, filter, options...) +} + +func (m *errorLoggingMiddleware) Get(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.GetOptions) (item *items.Item, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Get(ctx, spaceId, envId, collectionId, itemId, options...) +} + +func (m *errorLoggingMiddleware) GetPublished(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.GetPublishedOptions) (item *items.Item, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.GetPublished(ctx, spaceId, envId, collectionId, itemId, options...) +} + +func (m *errorLoggingMiddleware) GetRevision(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, revisionId string, options ...*items.GetRevisionOptions) (item *items.Item, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.GetRevision(ctx, spaceId, envId, collectionId, itemId, revisionId, options...) +} + +func (m *errorLoggingMiddleware) Introspect(ctx context.Context, item *items.Item, opts ...*items.IntrospectOptions) (itm *items.Item, sch *schema.Schema, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Introspect(ctx, item, opts...) +} + +func (m *errorLoggingMiddleware) ListRevisions(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.ListRevisionsOptions) (items []*items.Item, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.ListRevisions(ctx, spaceId, envId, collectionId, itemId, options...) +} + +func (m *errorLoggingMiddleware) Publish(ctx context.Context, item *items.Item, options ...*items.PublishOptions) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Publish(ctx, item, options...) +} + +func (m *errorLoggingMiddleware) Unarchive(ctx context.Context, item *items.Item, options ...*items.UnarchiveOptions) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Unarchive(ctx, item, options...) +} + +func (m *errorLoggingMiddleware) Undelete(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.UndeleteOptions) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Undelete(ctx, spaceId, envId, collectionId, itemId, options...) +} + +func (m *errorLoggingMiddleware) Unpublish(ctx context.Context, item *items.Item, options ...*items.UnpublishOptions) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Unpublish(ctx, item, options...) +} + +func (m *errorLoggingMiddleware) Update(ctx context.Context, item *items.Item, options ...*items.UpdateOptions) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Update(ctx, item, options...) +} diff --git a/pkg/items/middleware/logging_middleware.go b/pkg/items/middleware/logging_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..102b91874c63655c169e5c101b10490e375833b9 --- /dev/null +++ b/pkg/items/middleware/logging_middleware.go @@ -0,0 +1,732 @@ +package service + +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/access_log +// gowrap: http://github.com/hexdigest/gowrap + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/items -i Items -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/items" + "git.perx.ru/perxis/perxis-go/pkg/schema" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// loggingMiddleware implements items.Items that is instrumented with logging +type loggingMiddleware struct { + logger *zap.Logger + next items.Items +} + +// LoggingMiddleware instruments an implementation of the items.Items with simple logging +func LoggingMiddleware(logger *zap.Logger) Middleware { + return func(next items.Items) items.Items { + return &loggingMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *loggingMiddleware) Aggregate(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.AggregateOptions) (result map[string]interface{}, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "envId": envId, + "collectionId": collectionId, + "filter": filter, + "options": options} { + 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("Aggregate.Request", fields...) + + result, err = m.next.Aggregate(ctx, spaceId, envId, collectionId, filter, options...) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "result": result, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Aggregate.Response", fields...) + + return result, err +} + +func (m *loggingMiddleware) AggregatePublished(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.AggregatePublishedOptions) (result map[string]interface{}, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "envId": envId, + "collectionId": collectionId, + "filter": filter, + "options": options} { + 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("AggregatePublished.Request", fields...) + + result, err = m.next.AggregatePublished(ctx, spaceId, envId, collectionId, filter, options...) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "result": result, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("AggregatePublished.Response", fields...) + + return result, err +} + +func (m *loggingMiddleware) Archive(ctx context.Context, item *items.Item, options ...*items.ArchiveOptions) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "item": item, + "options": options} { + 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("Archive.Request", fields...) + + err = m.next.Archive(ctx, item, options...) + + 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("Archive.Response", fields...) + + return err +} + +func (m *loggingMiddleware) Create(ctx context.Context, item *items.Item, opts ...*items.CreateOptions) (created *items.Item, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "item": item, + "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("Create.Request", fields...) + + created, err = m.next.Create(ctx, item, opts...) + + 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, envId string, collectionId string, itemId string, options ...*items.DeleteOptions) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "envId": envId, + "collectionId": collectionId, + "itemId": itemId, + "options": options} { + 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, envId, collectionId, itemId, options...) + + 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, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.FindOptions) (items []*items.Item, total int, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "envId": envId, + "collectionId": collectionId, + "filter": filter, + "options": options} { + 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...) + + items, total, err = m.next.Find(ctx, spaceId, envId, collectionId, filter, options...) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "items": items, + "total": total, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Find.Response", fields...) + + return items, total, err +} + +func (m *loggingMiddleware) FindArchived(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.FindArchivedOptions) (items []*items.Item, total int, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "envId": envId, + "collectionId": collectionId, + "filter": filter, + "options": options} { + 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("FindArchived.Request", fields...) + + items, total, err = m.next.FindArchived(ctx, spaceId, envId, collectionId, filter, options...) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "items": items, + "total": total, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("FindArchived.Response", fields...) + + return items, total, err +} + +func (m *loggingMiddleware) FindPublished(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.FindPublishedOptions) (items []*items.Item, total int, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "envId": envId, + "collectionId": collectionId, + "filter": filter, + "options": options} { + 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("FindPublished.Request", fields...) + + items, total, err = m.next.FindPublished(ctx, spaceId, envId, collectionId, filter, options...) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "items": items, + "total": total, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("FindPublished.Response", fields...) + + return items, total, err +} + +func (m *loggingMiddleware) Get(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.GetOptions) (item *items.Item, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "envId": envId, + "collectionId": collectionId, + "itemId": itemId, + "options": options} { + 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...) + + item, err = m.next.Get(ctx, spaceId, envId, collectionId, itemId, options...) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "item": item, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Get.Response", fields...) + + return item, err +} + +func (m *loggingMiddleware) GetPublished(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.GetPublishedOptions) (item *items.Item, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "envId": envId, + "collectionId": collectionId, + "itemId": itemId, + "options": options} { + 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("GetPublished.Request", fields...) + + item, err = m.next.GetPublished(ctx, spaceId, envId, collectionId, itemId, options...) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "item": item, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("GetPublished.Response", fields...) + + return item, err +} + +func (m *loggingMiddleware) GetRevision(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, revisionId string, options ...*items.GetRevisionOptions) (item *items.Item, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "envId": envId, + "collectionId": collectionId, + "itemId": itemId, + "revisionId": revisionId, + "options": options} { + 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("GetRevision.Request", fields...) + + item, err = m.next.GetRevision(ctx, spaceId, envId, collectionId, itemId, revisionId, options...) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "item": item, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("GetRevision.Response", fields...) + + return item, err +} + +func (m *loggingMiddleware) Introspect(ctx context.Context, item *items.Item, opts ...*items.IntrospectOptions) (itm *items.Item, sch *schema.Schema, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "item": item, + "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("Introspect.Request", fields...) + + itm, sch, err = m.next.Introspect(ctx, item, opts...) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "itm": itm, + "sch": sch, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Introspect.Response", fields...) + + return itm, sch, err +} + +func (m *loggingMiddleware) ListRevisions(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.ListRevisionsOptions) (items []*items.Item, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "envId": envId, + "collectionId": collectionId, + "itemId": itemId, + "options": options} { + 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("ListRevisions.Request", fields...) + + items, err = m.next.ListRevisions(ctx, spaceId, envId, collectionId, itemId, options...) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "items": items, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("ListRevisions.Response", fields...) + + return items, err +} + +func (m *loggingMiddleware) Publish(ctx context.Context, item *items.Item, options ...*items.PublishOptions) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "item": item, + "options": options} { + 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("Publish.Request", fields...) + + err = m.next.Publish(ctx, item, options...) + + 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("Publish.Response", fields...) + + return err +} + +func (m *loggingMiddleware) Unarchive(ctx context.Context, item *items.Item, options ...*items.UnarchiveOptions) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "item": item, + "options": options} { + 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("Unarchive.Request", fields...) + + err = m.next.Unarchive(ctx, item, options...) + + 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("Unarchive.Response", fields...) + + return err +} + +func (m *loggingMiddleware) Undelete(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.UndeleteOptions) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "envId": envId, + "collectionId": collectionId, + "itemId": itemId, + "options": options} { + 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("Undelete.Request", fields...) + + err = m.next.Undelete(ctx, spaceId, envId, collectionId, itemId, options...) + + 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("Undelete.Response", fields...) + + return err +} + +func (m *loggingMiddleware) Unpublish(ctx context.Context, item *items.Item, options ...*items.UnpublishOptions) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "item": item, + "options": options} { + 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("Unpublish.Request", fields...) + + err = m.next.Unpublish(ctx, item, options...) + + 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("Unpublish.Response", fields...) + + return err +} + +func (m *loggingMiddleware) Update(ctx context.Context, item *items.Item, options ...*items.UpdateOptions) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "item": item, + "options": options} { + 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, item, options...) + + 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/items/middleware/middleware.go b/pkg/items/middleware/middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..a1090fe5ad072cd42682c1a4fad8504f22136926 --- /dev/null +++ b/pkg/items/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/items -i Items -t ../../../assets/templates/middleware/middleware -o middleware.go -l "" + +import ( + "git.perx.ru/perxis/perxis-go/pkg/items" + "go.uber.org/zap" +) + +type Middleware func(items.Items) items.Items + +func WithLog(s items.Items, logger *zap.Logger, log_access bool) items.Items { + if logger == nil { + logger = zap.NewNop() + } + + logger = logger.Named("Items") + s = ErrorLoggingMiddleware(logger)(s) + if log_access { + s = LoggingMiddleware(logger)(s) + } + s = RecoveringMiddleware(logger)(s) + return s +} diff --git a/pkg/items/middleware/recovering_middleware.go b/pkg/items/middleware/recovering_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..244fc8a0638fa9ad812afeb3f919907171fb9b1e --- /dev/null +++ b/pkg/items/middleware/recovering_middleware.go @@ -0,0 +1,248 @@ +// 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/items -i Items -t ../../../assets/templates/middleware/recovery -o recovering_middleware.go -l "" + +import ( + "context" + "fmt" + + "git.perx.ru/perxis/perxis-go/pkg/items" + "git.perx.ru/perxis/perxis-go/pkg/schema" + "go.uber.org/zap" +) + +// recoveringMiddleware implements items.Items that is instrumented with logging +type recoveringMiddleware struct { + logger *zap.Logger + next items.Items +} + +// RecoveringMiddleware instruments an implementation of the items.Items with simple logging +func RecoveringMiddleware(logger *zap.Logger) Middleware { + return func(next items.Items) items.Items { + return &recoveringMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *recoveringMiddleware) Aggregate(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.AggregateOptions) (result map[string]interface{}, 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.Aggregate(ctx, spaceId, envId, collectionId, filter, options...) +} + +func (m *recoveringMiddleware) AggregatePublished(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.AggregatePublishedOptions) (result map[string]interface{}, 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.AggregatePublished(ctx, spaceId, envId, collectionId, filter, options...) +} + +func (m *recoveringMiddleware) Archive(ctx context.Context, item *items.Item, options ...*items.ArchiveOptions) (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.Archive(ctx, item, options...) +} + +func (m *recoveringMiddleware) Create(ctx context.Context, item *items.Item, opts ...*items.CreateOptions) (created *items.Item, 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, item, opts...) +} + +func (m *recoveringMiddleware) Delete(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.DeleteOptions) (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, envId, collectionId, itemId, options...) +} + +func (m *recoveringMiddleware) Find(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.FindOptions) (items []*items.Item, 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, spaceId, envId, collectionId, filter, options...) +} + +func (m *recoveringMiddleware) FindArchived(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.FindArchivedOptions) (items []*items.Item, 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.FindArchived(ctx, spaceId, envId, collectionId, filter, options...) +} + +func (m *recoveringMiddleware) FindPublished(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.FindPublishedOptions) (items []*items.Item, 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.FindPublished(ctx, spaceId, envId, collectionId, filter, options...) +} + +func (m *recoveringMiddleware) Get(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.GetOptions) (item *items.Item, 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, envId, collectionId, itemId, options...) +} + +func (m *recoveringMiddleware) GetPublished(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.GetPublishedOptions) (item *items.Item, 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.GetPublished(ctx, spaceId, envId, collectionId, itemId, options...) +} + +func (m *recoveringMiddleware) GetRevision(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, revisionId string, options ...*items.GetRevisionOptions) (item *items.Item, 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.GetRevision(ctx, spaceId, envId, collectionId, itemId, revisionId, options...) +} + +func (m *recoveringMiddleware) Introspect(ctx context.Context, item *items.Item, opts ...*items.IntrospectOptions) (itm *items.Item, sch *schema.Schema, 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.Introspect(ctx, item, opts...) +} + +func (m *recoveringMiddleware) ListRevisions(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.ListRevisionsOptions) (items []*items.Item, 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.ListRevisions(ctx, spaceId, envId, collectionId, itemId, options...) +} + +func (m *recoveringMiddleware) Publish(ctx context.Context, item *items.Item, options ...*items.PublishOptions) (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.Publish(ctx, item, options...) +} + +func (m *recoveringMiddleware) Unarchive(ctx context.Context, item *items.Item, options ...*items.UnarchiveOptions) (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.Unarchive(ctx, item, options...) +} + +func (m *recoveringMiddleware) Undelete(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.UndeleteOptions) (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.Undelete(ctx, spaceId, envId, collectionId, itemId, options...) +} + +func (m *recoveringMiddleware) Unpublish(ctx context.Context, item *items.Item, options ...*items.UnpublishOptions) (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.Unpublish(ctx, item, options...) +} + +func (m *recoveringMiddleware) Update(ctx context.Context, item *items.Item, options ...*items.UpdateOptions) (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, item, options...) +} diff --git a/pkg/items/pagination.go b/pkg/items/pagination.go new file mode 100644 index 0000000000000000000000000000000000000000..7f990dc6c6e8d0684553e4039b86580236cc2ef0 --- /dev/null +++ b/pkg/items/pagination.go @@ -0,0 +1,137 @@ +package items + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/content" + "git.perx.ru/perxis/perxis-go/pkg/data" + "git.perx.ru/perxis/perxis-go/pkg/errors" + "git.perx.ru/perxis/perxis-go/pkg/options" + "google.golang.org/grpc/codes" +) + +type BatchProcessor struct { + Content *content.Content + SpaceID, EnvID, CollectionID string + FindOptions *FindOptions + FindPublishedOptions *FindPublishedOptions + Filter *Filter + + pageSize, pageNum int + sort []string + processed int +} + +func (b *BatchProcessor) getBatch(ctx context.Context) ([]*Item, bool, error) { + var res []*Item + var err error + var total int + + if b.FindPublishedOptions != nil { + res, total, err = b.Content.Items.FindPublished( + ctx, + b.SpaceID, + b.EnvID, + b.CollectionID, + b.Filter, + &FindPublishedOptions{ + Regular: b.FindPublishedOptions.Regular, + Hidden: b.FindPublishedOptions.Hidden, + Templates: b.FindPublishedOptions.Templates, + FindOptions: *options.NewFindOptions(b.pageNum, b.pageSize, b.sort...), + }, + ) + } else { + res, total, err = b.Content.Items.Find( + ctx, + b.SpaceID, + b.EnvID, + b.CollectionID, + b.Filter, + &FindOptions{ + Deleted: b.FindOptions.Deleted, + Regular: b.FindOptions.Regular, + Hidden: b.FindOptions.Hidden, + Templates: b.FindOptions.Templates, + FindOptions: *options.NewFindOptions(b.pageNum, b.pageSize, b.sort...), + }, + ) + } + + if err == nil { + b.processed += len(res) + b.pageNum++ + } + + return res, b.processed != total, err +} + +func (b *BatchProcessor) next(ctx context.Context) (res []*Item, next bool, err error) { + + for { + res, next, err = b.getBatch(ctx) + if err != nil { + if errors.GetStatusCode(err) == codes.ResourceExhausted && b.reducePageSize() { + continue + } + + return nil, false, err + } + + break + } + + return res, next, nil +} + +func (b *BatchProcessor) reducePageSize() bool { + if b.pageSize == 1 { + return false + } + + b.pageNum = 2 * b.pageNum + b.pageSize = b.pageSize / 2 + + return true +} + +func (b *BatchProcessor) Do(ctx context.Context, f func(batch []*Item) error) (int, error) { + + if b.FindOptions == nil && b.FindPublishedOptions == nil { + b.FindOptions = new(FindOptions) + } + if b.FindOptions != nil { + b.pageSize = b.FindOptions.PageSize + b.sort = b.FindOptions.Sort + } + if b.FindPublishedOptions != nil { + b.pageSize = b.FindPublishedOptions.PageSize + b.sort = b.FindPublishedOptions.Sort + } + + if b.pageSize == 0 { + b.pageSize = 128 + } + + if b.Filter != nil && (len(b.Filter.ID) > 0 || len(b.Filter.Q) > 0) && !data.Contains("_id", b.sort) { + b.sort = append(b.sort, "_id") + } + + var err error + + next := true + for next { + + var batch []*Item + + batch, next, err = b.next(ctx) + if err != nil { + return 0, err + } + + if err = f(batch); err != nil { + return 0, err + } + } + return b.processed, nil +} 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..695c91128d6f093d93f022468d464bedbd571e04 --- /dev/null +++ b/pkg/locales/middleware/error_logging_middleware.go @@ -0,0 +1,60 @@ +package service + +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/error_log +// gowrap: http://github.com/hexdigest/gowrap + +//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..fb98d25759e87d199e8c65a204f30b3acb48c1f4 --- /dev/null +++ b/pkg/locales/middleware/logging_middleware.go @@ -0,0 +1,142 @@ +package service + +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/access_log +// gowrap: http://github.com/hexdigest/gowrap + +//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) +} diff --git a/pkg/members/middleware/caching_middleware.go b/pkg/members/middleware/caching_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..2faa5ce433281d8d396bb5c912e4e91cfaff727f --- /dev/null +++ b/pkg/members/middleware/caching_middleware.go @@ -0,0 +1,102 @@ +package service + +import ( + "context" + "strings" + + "git.perx.ru/perxis/perxis-go/pkg/cache" + service "git.perx.ru/perxis/perxis-go/pkg/members" +) + +func makeKey(ss ...string) string { + return strings.Join(ss, "-") +} + +func CachingMiddleware(cache *cache.Cache) Middleware { + return func(next service.Members) service.Members { + return &cachingMiddleware{ + cache: cache, + next: next, + } + } +} + +type cachingMiddleware struct { + cache *cache.Cache + next service.Members +} + +func (m cachingMiddleware) Set(ctx context.Context, orgId string, userId string, role service.Role) (err error) { + + err = m.next.Set(ctx, orgId, userId, role) + if err == nil { + m.cache.Remove(makeKey(orgId, userId)) + m.cache.Remove(makeKey(orgId)) + m.cache.Remove(makeKey(userId)) + } + return err +} + +func (m cachingMiddleware) Get(ctx context.Context, orgId string, userId string) (role service.Role, err error) { + + key := makeKey(orgId, userId) + value, e := m.cache.Get(key) + if e == nil { + return value.(service.Role), err + } + role, err = m.next.Get(ctx, orgId, userId) + if err == nil { + m.cache.Set(key, role) + } + return role, err +} + +func (m cachingMiddleware) Remove(ctx context.Context, orgId string, userId string) (err error) { + + err = m.next.Remove(ctx, orgId, userId) + if err == nil { + m.cache.Remove(makeKey(orgId, userId)) + m.cache.Remove(makeKey(orgId)) + m.cache.Remove(makeKey(userId)) + } + return err +} + +func (m cachingMiddleware) RemoveAll(ctx context.Context, orgId string) (err error) { + + err = m.next.RemoveAll(ctx, orgId) + if err == nil { + members, _ := m.ListMembers(ctx, orgId) + for _, member := range members { + m.cache.Remove(member.UserId) + m.cache.Remove(makeKey(orgId, member.UserId)) + } + } + return err +} + +func (m cachingMiddleware) ListMembers(ctx context.Context, orgId string) (members []*service.Member, err error) { + + value, e := m.cache.Get(makeKey(orgId)) + if e == nil { + return value.([]*service.Member), err + } + members, err = m.next.ListMembers(ctx, orgId) + if err == nil { + m.cache.Set(makeKey(orgId), members) + } + return members, err +} + +func (m cachingMiddleware) ListOrganizations(ctx context.Context, userId string) (members []*service.Member, err error) { + + value, e := m.cache.Get(makeKey(userId)) + if e == nil { + return value.([]*service.Member), err + } + members, err = m.next.ListOrganizations(ctx, userId) + if err == nil { + m.cache.Set(makeKey(userId), members) + } + return members, err +} diff --git a/pkg/members/middleware/caching_middleware_test.go b/pkg/members/middleware/caching_middleware_test.go new file mode 100644 index 0000000000000000000000000000000000000000..1844dc58ebb97b228800a95f9c9cb8b142407fb4 --- /dev/null +++ b/pkg/members/middleware/caching_middleware_test.go @@ -0,0 +1,147 @@ +package service + +import ( + "context" + "testing" + "time" + + "git.perx.ru/perxis/perxis-go/pkg/cache" + "git.perx.ru/perxis/perxis-go/pkg/members" + mocksmembers "git.perx.ru/perxis/perxis-go/pkg/members/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestMembersCache(t *testing.T) { + + const ( + orgId = "orgId" + userId = "userId" + size = 5 + ttl = 20 * time.Millisecond + ) + + ctx := context.Background() + + t.Run("Get from cache", func(t *testing.T) { + mbrs := &mocksmembers.Members{} + svc := CachingMiddleware(cache.NewCache(size, ttl))(mbrs) + + mbrs.On("Get", mock.Anything, orgId, userId).Return(members.RoleOwner, nil).Once() + + v1, err := svc.Get(ctx, orgId, userId) + require.NoError(t, err) + + v2, err := svc.Get(ctx, orgId, userId) + require.NoError(t, err) + assert.Equal(t, v1, v2, "Ожидается получение объекта РёР· кэша, после повторного запроса get.") + + mbrs.AssertExpectations(t) + }) + + t.Run("Invalidate cache", func(t *testing.T) { + t.Run("After Set", func(t *testing.T) { + mbrs := &mocksmembers.Members{} + svc := CachingMiddleware(cache.NewCache(size, ttl))(mbrs) + + mbrs.On("Get", mock.Anything, orgId, userId).Return(members.RoleOwner, nil).Once() + + v1, err := svc.Get(ctx, orgId, userId) + require.NoError(t, err) + + v2, err := svc.Get(ctx, orgId, userId) + require.NoError(t, err) + assert.Equal(t, v1, v2, "Ожидается получение объекта РёР· кэша.") + + mbrs.On("Set", mock.Anything, orgId, userId, members.RoleMember).Return(nil).Once() + + err = svc.Set(ctx, orgId, userId, members.RoleMember) + require.NoError(t, err) + + mbrs.On("Get", mock.Anything, orgId, userId).Return(members.RoleMember, nil).Once() + + v3, err := svc.Get(ctx, orgId, userId) + require.NoError(t, err) + assert.NotEqual(t, v2, v3, "Ожидается удаление объекта РёР· кэша Рё получение заново РёР· сервиса.") + mbrs.AssertExpectations(t) + }) + + t.Run("After Remove", func(t *testing.T) { + mbrs := &mocksmembers.Members{} + svc := CachingMiddleware(cache.NewCache(size, ttl))(mbrs) + + mbrs.On("Get", mock.Anything, orgId, userId).Return(members.RoleOwner, nil).Once() + + v1, err := svc.Get(ctx, orgId, userId) + require.NoError(t, err) + + v2, err := svc.Get(ctx, orgId, userId) + require.NoError(t, err) + assert.Equal(t, v1, v2, "Ожидается получение объекта РёР· кэша.") + + mbrs.On("Remove", mock.Anything, orgId, userId).Return(nil).Once() + + err = svc.Remove(ctx, orgId, userId) + require.NoError(t, err) + + mbrs.On("Get", mock.Anything, orgId, userId).Return(members.NotMember, nil).Once() + + v3, err := svc.Get(ctx, orgId, userId) + require.NoError(t, err) + assert.NotEqual(t, v2, v3, "Ожидается удаление объекта РёР· кэша после удаления РёР· хранилища Рё получение заново РёР· сервиса.") + + mbrs.AssertExpectations(t) + }) + + t.Run("After RemoveAll", func(t *testing.T) { + mbrs := &mocksmembers.Members{} + svc := CachingMiddleware(cache.NewCache(size, ttl))(mbrs) + + mbrs.On("Get", mock.Anything, orgId, userId).Return(members.RoleOwner, nil).Once() + mbrs.On("ListMembers", mock.Anything, orgId).Return([]*members.Member{{OrgId: orgId, UserId: userId, Role: members.RoleOwner}}, nil) + + v1, err := svc.Get(ctx, orgId, userId) + require.NoError(t, err) + + v2, err := svc.Get(ctx, orgId, userId) + require.NoError(t, err) + assert.Equal(t, v1, v2, "Ожидается получение объекта РёР· кэша.") + + mbrs.On("RemoveAll", mock.Anything, orgId).Return(nil).Once() + + err = svc.RemoveAll(ctx, orgId) + require.NoError(t, err) + + mbrs.On("Get", mock.Anything, orgId, userId).Return(members.NotMember, nil).Once() + + v3, err := svc.Get(ctx, orgId, userId) + require.NoError(t, err) + assert.NotEqual(t, v2, v3, "Ожидается удаление объекта РёР· кэша после удаления РёР· хранилища Рё получение заново РёР· сервиса.") + + mbrs.AssertExpectations(t) + }) + + t.Run("After TTL expired", func(t *testing.T) { + mbrs := &mocksmembers.Members{} + svc := CachingMiddleware(cache.NewCache(size, ttl))(mbrs) + + mbrs.On("Get", mock.Anything, orgId, userId).Return(members.RoleOwner, nil).Once() + + v1, err := svc.Get(ctx, orgId, userId) + + v2, err := svc.Get(ctx, orgId, userId) + require.NoError(t, err) + assert.Equal(t, v1, v2, "Ожидается получение объекта РёР· кэша.") + + time.Sleep(2 * ttl) + + mbrs.On("Get", mock.Anything, orgId, userId).Return(members.RoleMember, nil).Once() + + v3, err := svc.Get(ctx, orgId, userId) + require.NoError(t, err) + assert.NotEqual(t, v2, v3, "Ожидается удаление объекта РёР· кэша после истечения ttl Рё получение заново РёР· сервиса.") + mbrs.AssertExpectations(t) + }) + }) +} diff --git a/pkg/members/middleware/error_logging_middleware.go b/pkg/members/middleware/error_logging_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..08d2814bf4fc8f16f0df57462770342a93e191c8 --- /dev/null +++ b/pkg/members/middleware/error_logging_middleware.go @@ -0,0 +1,90 @@ +package service + +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/error_log +// gowrap: http://github.com/hexdigest/gowrap + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/members -i Members -t ../../../assets/templates/middleware/error_log -o error_logging_middleware.go -l "" + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/members" + "go.uber.org/zap" +) + +// errorLoggingMiddleware implements members.Members that is instrumented with logging +type errorLoggingMiddleware struct { + logger *zap.Logger + next members.Members +} + +// ErrorLoggingMiddleware instruments an implementation of the members.Members with simple logging +func ErrorLoggingMiddleware(logger *zap.Logger) Middleware { + return func(next members.Members) members.Members { + return &errorLoggingMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *errorLoggingMiddleware) Get(ctx context.Context, orgId string, userId string) (role members.Role, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Get(ctx, orgId, userId) +} + +func (m *errorLoggingMiddleware) ListMembers(ctx context.Context, orgId string) (members []*members.Member, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.ListMembers(ctx, orgId) +} + +func (m *errorLoggingMiddleware) ListOrganizations(ctx context.Context, userId string) (organizations []*members.Member, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.ListOrganizations(ctx, userId) +} + +func (m *errorLoggingMiddleware) Remove(ctx context.Context, orgId string, userId string) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Remove(ctx, orgId, userId) +} + +func (m *errorLoggingMiddleware) RemoveAll(ctx context.Context, orgId string) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.RemoveAll(ctx, orgId) +} + +func (m *errorLoggingMiddleware) Set(ctx context.Context, orgId string, userId string, role members.Role) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Set(ctx, orgId, userId, role) +} diff --git a/pkg/members/middleware/logging_middleware.go b/pkg/members/middleware/logging_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..1b9ead6d06d694945c89638c52d4bce07e4ee938 --- /dev/null +++ b/pkg/members/middleware/logging_middleware.go @@ -0,0 +1,251 @@ +package service + +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/access_log +// gowrap: http://github.com/hexdigest/gowrap + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/members -i Members -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/members" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// loggingMiddleware implements members.Members that is instrumented with logging +type loggingMiddleware struct { + logger *zap.Logger + next members.Members +} + +// LoggingMiddleware instruments an implementation of the members.Members with simple logging +func LoggingMiddleware(logger *zap.Logger) Middleware { + return func(next members.Members) members.Members { + return &loggingMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *loggingMiddleware) Get(ctx context.Context, orgId string, userId string) (role members.Role, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "orgId": orgId, + "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("Get.Request", fields...) + + role, err = m.next.Get(ctx, orgId, userId) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "role": role, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Get.Response", fields...) + + return role, err +} + +func (m *loggingMiddleware) ListMembers(ctx context.Context, orgId string) (members []*members.Member, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "orgId": orgId} { + 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("ListMembers.Request", fields...) + + members, err = m.next.ListMembers(ctx, orgId) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "members": members, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("ListMembers.Response", fields...) + + return members, err +} + +func (m *loggingMiddleware) ListOrganizations(ctx context.Context, userId string) (organizations []*members.Member, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "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("ListOrganizations.Request", fields...) + + organizations, err = m.next.ListOrganizations(ctx, userId) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "organizations": organizations, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("ListOrganizations.Response", fields...) + + return organizations, err +} + +func (m *loggingMiddleware) Remove(ctx context.Context, orgId string, userId string) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "orgId": orgId, + "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("Remove.Request", fields...) + + err = m.next.Remove(ctx, orgId, 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("Remove.Response", fields...) + + return err +} + +func (m *loggingMiddleware) RemoveAll(ctx context.Context, orgId string) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "orgId": orgId} { + 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("RemoveAll.Request", fields...) + + err = m.next.RemoveAll(ctx, orgId) + + 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("RemoveAll.Response", fields...) + + return err +} + +func (m *loggingMiddleware) Set(ctx context.Context, orgId string, userId string, role members.Role) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "orgId": orgId, + "userId": userId, + "role": role} { + 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("Set.Request", fields...) + + err = m.next.Set(ctx, orgId, userId, role) + + 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("Set.Response", fields...) + + return err +} diff --git a/pkg/members/middleware/middleware.go b/pkg/members/middleware/middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..1aa0cfbe798f5688587ab8e4f5d4e166383d8d92 --- /dev/null +++ b/pkg/members/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/members -i Members -t ../../../assets/templates/middleware/middleware -o middleware.go -l "" + +import ( + "git.perx.ru/perxis/perxis-go/pkg/members" + "go.uber.org/zap" +) + +type Middleware func(members.Members) members.Members + +func WithLog(s members.Members, logger *zap.Logger, log_access bool) members.Members { + if logger == nil { + logger = zap.NewNop() + } + + logger = logger.Named("Members") + s = ErrorLoggingMiddleware(logger)(s) + if log_access { + s = LoggingMiddleware(logger)(s) + } + s = RecoveringMiddleware(logger)(s) + return s +} diff --git a/pkg/members/middleware/recovering_middleware.go b/pkg/members/middleware/recovering_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..ec6db9f480d35215c47bff6fceef8c0e01a77447 --- /dev/null +++ b/pkg/members/middleware/recovering_middleware.go @@ -0,0 +1,103 @@ +// 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/members -i Members -t ../../../assets/templates/middleware/recovery -o recovering_middleware.go -l "" + +import ( + "context" + "fmt" + + "git.perx.ru/perxis/perxis-go/pkg/members" + "go.uber.org/zap" +) + +// recoveringMiddleware implements members.Members that is instrumented with logging +type recoveringMiddleware struct { + logger *zap.Logger + next members.Members +} + +// RecoveringMiddleware instruments an implementation of the members.Members with simple logging +func RecoveringMiddleware(logger *zap.Logger) Middleware { + return func(next members.Members) members.Members { + return &recoveringMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *recoveringMiddleware) Get(ctx context.Context, orgId string, userId string) (role members.Role, 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, orgId, userId) +} + +func (m *recoveringMiddleware) ListMembers(ctx context.Context, orgId string) (members []*members.Member, 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.ListMembers(ctx, orgId) +} + +func (m *recoveringMiddleware) ListOrganizations(ctx context.Context, userId string) (organizations []*members.Member, 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.ListOrganizations(ctx, userId) +} + +func (m *recoveringMiddleware) Remove(ctx context.Context, orgId 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.Remove(ctx, orgId, userId) +} + +func (m *recoveringMiddleware) RemoveAll(ctx context.Context, orgId 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.RemoveAll(ctx, orgId) +} + +func (m *recoveringMiddleware) Set(ctx context.Context, orgId string, userId string, role members.Role) (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.Set(ctx, orgId, userId, role) +} diff --git a/pkg/organizations/middleware/caching_middleware.go b/pkg/organizations/middleware/caching_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..2017c9913b41ae8fbeb05e116abdda037fb5667e --- /dev/null +++ b/pkg/organizations/middleware/caching_middleware.go @@ -0,0 +1,62 @@ +package service + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/cache" + "git.perx.ru/perxis/perxis-go/pkg/options" + service "git.perx.ru/perxis/perxis-go/pkg/organizations" +) + +func CachingMiddleware(cache *cache.Cache) Middleware { + return func(next service.Organizations) service.Organizations { + return &cachingMiddleware{ + cache: cache, + next: next, + } + } +} + +type cachingMiddleware struct { + cache *cache.Cache + next service.Organizations +} + +func (m cachingMiddleware) Create(ctx context.Context, org *service.Organization) (organization *service.Organization, err error) { + return m.next.Create(ctx, org) +} + +func (m cachingMiddleware) Get(ctx context.Context, orgId string) (organization *service.Organization, err error) { + + value, e := m.cache.Get(orgId) + if e == nil { + return value.(*service.Organization), err + } + organization, err = m.next.Get(ctx, orgId) + if err == nil { + m.cache.Set(orgId, organization) + } + return organization, err +} + +func (m cachingMiddleware) Update(ctx context.Context, org *service.Organization) (err error) { + + err = m.next.Update(ctx, org) + if err == nil { + m.cache.Remove(org.ID) + } + return err +} + +func (m cachingMiddleware) Delete(ctx context.Context, orgId string) (err error) { + + err = m.next.Delete(ctx, orgId) + if err == nil { + m.cache.Remove(orgId) + } + return err +} + +func (m cachingMiddleware) Find(ctx context.Context, filter *service.Filter, opts *options.FindOptions) (organizations []*service.Organization, total int, err error) { + return m.next.Find(ctx, filter, opts) +} diff --git a/pkg/organizations/middleware/caching_middleware_test.go b/pkg/organizations/middleware/caching_middleware_test.go new file mode 100644 index 0000000000000000000000000000000000000000..59248ded4d85f904bf67e21956ad2df355706cdf --- /dev/null +++ b/pkg/organizations/middleware/caching_middleware_test.go @@ -0,0 +1,119 @@ +package service + +import ( + "context" + "testing" + "time" + + "git.perx.ru/perxis/perxis-go/pkg/cache" + "git.perx.ru/perxis/perxis-go/pkg/errors" + "git.perx.ru/perxis/perxis-go/pkg/organizations" + mocksorgs "git.perx.ru/perxis/perxis-go/pkg/organizations/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestOrganizationsCache(t *testing.T) { + + const ( + orgId = "orgId" + size = 5 + ttl = 20 * time.Millisecond + ) + + errNotFound := errors.NotFound(errors.New("not found")) + + ctx := context.Background() + + t.Run("Get from cache", func(t *testing.T) { + orgs := &mocksorgs.Organizations{} + svc := CachingMiddleware(cache.NewCache(size, ttl))(orgs) + + orgs.On("Get", mock.Anything, orgId).Return(&organizations.Organization{ID: orgId, Name: "Organization"}, nil).Once() + + v1, err := svc.Get(ctx, orgId) + require.NoError(t, err) + + v2, err := svc.Get(ctx, orgId) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кэша.") + + orgs.AssertExpectations(t) + }) + + t.Run("Invalidate cache", func(t *testing.T) { + t.Run("After Update", func(t *testing.T) { + orgs := &mocksorgs.Organizations{} + svc := CachingMiddleware(cache.NewCache(size, ttl))(orgs) + + orgs.On("Get", mock.Anything, orgId).Return(&organizations.Organization{ID: orgId, Name: "Organization"}, nil).Once() + + v1, err := svc.Get(ctx, orgId) + require.NoError(t, err) + + v2, err := svc.Get(ctx, orgId) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кэша.") + + orgs.On("Update", mock.Anything, mock.Anything).Return(nil).Once() + err = svc.Update(ctx, &organizations.Organization{ID: orgId, Name: "OrganizationUPD"}) + + orgs.On("Get", mock.Anything, orgId).Return(&organizations.Organization{ID: orgId, Name: "OrganizationUPD"}, nil).Once() + + v3, err := svc.Get(ctx, orgId) + require.NoError(t, err) + assert.NotSame(t, v2, v3, "Ожидается удаление объекта РёР· кэша после обновления Рё получение заново РёР· сервиса.") + + orgs.AssertExpectations(t) + }) + + t.Run("After Delete", func(t *testing.T) { + orgs := &mocksorgs.Organizations{} + svc := CachingMiddleware(cache.NewCache(size, ttl))(orgs) + + orgs.On("Get", mock.Anything, orgId).Return(&organizations.Organization{ID: orgId, Name: "Organization"}, nil).Once() + + v1, err := svc.Get(ctx, orgId) + require.NoError(t, err) + + v2, err := svc.Get(ctx, orgId) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кэша.") + + orgs.On("Delete", mock.Anything, mock.Anything).Return(nil).Once() + err = svc.Delete(ctx, orgId) + + orgs.On("Get", mock.Anything, orgId).Return(nil, errNotFound).Once() + + _, err = svc.Get(ctx, orgId) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидается удаление объекта РёР· кэша после удаления РёР· хранилища Рё получение ошибки РѕС‚ сервиса.") + + }) + + t.Run("After TTL expired", func(t *testing.T) { + orgs := &mocksorgs.Organizations{} + svc := CachingMiddleware(cache.NewCache(size, ttl))(orgs) + + orgs.On("Get", mock.Anything, orgId).Return(&organizations.Organization{ID: orgId, Name: "Organization"}, nil).Once() + + v1, err := svc.Get(ctx, orgId) + require.NoError(t, err) + + v2, err := svc.Get(ctx, orgId) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кэша.") + + time.Sleep(2 * ttl) + + orgs.On("Get", mock.Anything, orgId).Return(&organizations.Organization{ID: orgId, Name: "Organization"}, nil).Once() + + v3, err := svc.Get(ctx, orgId) + require.NoError(t, err) + assert.NotSame(t, v2, v3, "Ожидается удаление объекта РёР· кэша Рё получение заново РёР· сервиса.") + + orgs.AssertExpectations(t) + }) + }) +} diff --git a/pkg/organizations/middleware/error_logging_middleware.go b/pkg/organizations/middleware/error_logging_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..c9631f9144db6244044379219250ca334893d0d6 --- /dev/null +++ b/pkg/organizations/middleware/error_logging_middleware.go @@ -0,0 +1,81 @@ +package service + +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/error_log +// gowrap: http://github.com/hexdigest/gowrap + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/organizations -i Organizations -t ../../../assets/templates/middleware/error_log -o error_logging_middleware.go -l "" + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/options" + "git.perx.ru/perxis/perxis-go/pkg/organizations" + "go.uber.org/zap" +) + +// errorLoggingMiddleware implements organizations.Organizations that is instrumented with logging +type errorLoggingMiddleware struct { + logger *zap.Logger + next organizations.Organizations +} + +// ErrorLoggingMiddleware instruments an implementation of the organizations.Organizations with simple logging +func ErrorLoggingMiddleware(logger *zap.Logger) Middleware { + return func(next organizations.Organizations) organizations.Organizations { + return &errorLoggingMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *errorLoggingMiddleware) Create(ctx context.Context, org *organizations.Organization) (created *organizations.Organization, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Create(ctx, org) +} + +func (m *errorLoggingMiddleware) Delete(ctx context.Context, orgId string) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Delete(ctx, orgId) +} + +func (m *errorLoggingMiddleware) Find(ctx context.Context, filter *organizations.Filter, opts *options.FindOptions) (orgs []*organizations.Organization, 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, orgId string) (org *organizations.Organization, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Get(ctx, orgId) +} + +func (m *errorLoggingMiddleware) Update(ctx context.Context, org *organizations.Organization) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Update(ctx, org) +} diff --git a/pkg/organizations/middleware/logging_middleware.go b/pkg/organizations/middleware/logging_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..6f33296d5cecbee19be4bff65da4280486cd1958 --- /dev/null +++ b/pkg/organizations/middleware/logging_middleware.go @@ -0,0 +1,215 @@ +package service + +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/access_log +// gowrap: http://github.com/hexdigest/gowrap + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/organizations -i Organizations -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/options" + "git.perx.ru/perxis/perxis-go/pkg/organizations" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// loggingMiddleware implements organizations.Organizations that is instrumented with logging +type loggingMiddleware struct { + logger *zap.Logger + next organizations.Organizations +} + +// LoggingMiddleware instruments an implementation of the organizations.Organizations with simple logging +func LoggingMiddleware(logger *zap.Logger) Middleware { + return func(next organizations.Organizations) organizations.Organizations { + return &loggingMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *loggingMiddleware) Create(ctx context.Context, org *organizations.Organization) (created *organizations.Organization, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "org": org} { + 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, org) + + 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, orgId string) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "orgId": orgId} { + 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, orgId) + + 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 *organizations.Filter, opts *options.FindOptions) (orgs []*organizations.Organization, 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...) + + orgs, 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{}{ + "orgs": orgs, + "total": total, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Find.Response", fields...) + + return orgs, total, err +} + +func (m *loggingMiddleware) Get(ctx context.Context, orgId string) (org *organizations.Organization, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "orgId": orgId} { + 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...) + + org, err = m.next.Get(ctx, orgId) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "org": org, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Get.Response", fields...) + + return org, err +} + +func (m *loggingMiddleware) Update(ctx context.Context, org *organizations.Organization) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "org": org} { + 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, org) + + 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/organizations/middleware/middleware.go b/pkg/organizations/middleware/middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..fe3c3d645e19cc9a0c46742be61edec6bda3882e --- /dev/null +++ b/pkg/organizations/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/organizations -i Organizations -t ../../../assets/templates/middleware/middleware -o middleware.go -l "" + +import ( + "git.perx.ru/perxis/perxis-go/pkg/organizations" + "go.uber.org/zap" +) + +type Middleware func(organizations.Organizations) organizations.Organizations + +func WithLog(s organizations.Organizations, logger *zap.Logger, log_access bool) organizations.Organizations { + if logger == nil { + logger = zap.NewNop() + } + + logger = logger.Named("Organizations") + s = ErrorLoggingMiddleware(logger)(s) + if log_access { + s = LoggingMiddleware(logger)(s) + } + s = RecoveringMiddleware(logger)(s) + return s +} diff --git a/pkg/organizations/middleware/recovering_middleware.go b/pkg/organizations/middleware/recovering_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..35f3a6c5f7333f7fa3664c7101d31b72be270d33 --- /dev/null +++ b/pkg/organizations/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/organizations -i Organizations -t ../../../assets/templates/middleware/recovery -o recovering_middleware.go -l "" + +import ( + "context" + "fmt" + + "git.perx.ru/perxis/perxis-go/pkg/options" + "git.perx.ru/perxis/perxis-go/pkg/organizations" + "go.uber.org/zap" +) + +// recoveringMiddleware implements organizations.Organizations that is instrumented with logging +type recoveringMiddleware struct { + logger *zap.Logger + next organizations.Organizations +} + +// RecoveringMiddleware instruments an implementation of the organizations.Organizations with simple logging +func RecoveringMiddleware(logger *zap.Logger) Middleware { + return func(next organizations.Organizations) organizations.Organizations { + return &recoveringMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *recoveringMiddleware) Create(ctx context.Context, org *organizations.Organization) (created *organizations.Organization, 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, org) +} + +func (m *recoveringMiddleware) Delete(ctx context.Context, orgId 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, orgId) +} + +func (m *recoveringMiddleware) Find(ctx context.Context, filter *organizations.Filter, opts *options.FindOptions) (orgs []*organizations.Organization, 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, orgId string) (org *organizations.Organization, 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, orgId) +} + +func (m *recoveringMiddleware) Update(ctx context.Context, org *organizations.Organization) (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, org) +} diff --git a/pkg/references/middleware/client_encode_middleware.go b/pkg/references/middleware/client_encode_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..1cd9cb2f22340a5d90feb95bd00accb1bae39e0f --- /dev/null +++ b/pkg/references/middleware/client_encode_middleware.go @@ -0,0 +1,42 @@ +package service + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/collections" + "git.perx.ru/perxis/perxis-go/pkg/items" + "git.perx.ru/perxis/perxis-go/pkg/references" +) + +// ClientEncodeMiddleware выполняет операции encode/decode для передаваемых данных +func ClientEncodeMiddleware(colls collections.Collections) Middleware { + return func(refs references.References) references.References { + return &encodeDecodeMiddleware{ + next: refs, + colls: colls, + } + } +} + +type encodeDecodeMiddleware struct { + next references.References + colls collections.Collections +} + +func (m *encodeDecodeMiddleware) Get(ctx context.Context, spaceId, envId string, refs []*references.Reference) (items []*items.Item, notfound []*references.Reference, err error) { + items, notfound, err = m.next.Get(ctx, spaceId, envId, refs) + if err == nil && len(items) > 0 { + for i, item := range items { + col, err := m.colls.Get(ctx, item.SpaceID, item.EnvID, item.CollectionID) + if err != nil { + return nil, nil, err + } + + if item, err = item.Decode(ctx, col.Schema); err != nil { + return nil, nil, err + } + items[i] = item + } + } + return +} diff --git a/pkg/references/middleware/error_logging_middleware.go b/pkg/references/middleware/error_logging_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..9a62947f240842c6afb099a9b03cc966d9bd99b5 --- /dev/null +++ b/pkg/references/middleware/error_logging_middleware.go @@ -0,0 +1,41 @@ +package service + +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/error_log +// gowrap: http://github.com/hexdigest/gowrap + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/references -i References -t ../../../assets/templates/middleware/error_log -o error_logging_middleware.go -l "" + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/items" + "git.perx.ru/perxis/perxis-go/pkg/references" + "go.uber.org/zap" +) + +// errorLoggingMiddleware implements references.References that is instrumented with logging +type errorLoggingMiddleware struct { + logger *zap.Logger + next references.References +} + +// ErrorLoggingMiddleware instruments an implementation of the references.References with simple logging +func ErrorLoggingMiddleware(logger *zap.Logger) Middleware { + return func(next references.References) references.References { + return &errorLoggingMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *errorLoggingMiddleware) Get(ctx context.Context, spaceId string, envId string, references []*references.Reference) (items []*items.Item, notfound []*references.Reference, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Get(ctx, spaceId, envId, references) +} diff --git a/pkg/references/middleware/logging_middleware.go b/pkg/references/middleware/logging_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..62a0e9d5cf26c6ab0d131fff2fd443cffd8fb8f3 --- /dev/null +++ b/pkg/references/middleware/logging_middleware.go @@ -0,0 +1,74 @@ +package service + +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/access_log +// gowrap: http://github.com/hexdigest/gowrap + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/references -i References -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/items" + "git.perx.ru/perxis/perxis-go/pkg/references" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// loggingMiddleware implements references.References that is instrumented with logging +type loggingMiddleware struct { + logger *zap.Logger + next references.References +} + +// LoggingMiddleware instruments an implementation of the references.References with simple logging +func LoggingMiddleware(logger *zap.Logger) Middleware { + return func(next references.References) references.References { + return &loggingMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *loggingMiddleware) Get(ctx context.Context, spaceId string, envId string, references []*references.Reference) (items []*items.Item, notfound []*references.Reference, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "envId": envId, + "references": references} { + 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...) + + items, notfound, err = m.next.Get(ctx, spaceId, envId, references) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "items": items, + "notfound": notfound, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Get.Response", fields...) + + return items, notfound, err +} diff --git a/pkg/references/middleware/middleware.go b/pkg/references/middleware/middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..dfed8dc0c821998df97d414876b6839a9961f6b0 --- /dev/null +++ b/pkg/references/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/references -i References -t ../../../assets/templates/middleware/middleware -o middleware.go -l "" + +import ( + "git.perx.ru/perxis/perxis-go/pkg/references" + "go.uber.org/zap" +) + +type Middleware func(references.References) references.References + +func WithLog(s references.References, logger *zap.Logger, log_access bool) references.References { + if logger == nil { + logger = zap.NewNop() + } + + logger = logger.Named("References") + s = ErrorLoggingMiddleware(logger)(s) + if log_access { + s = LoggingMiddleware(logger)(s) + } + s = RecoveringMiddleware(logger)(s) + return s +} diff --git a/pkg/references/middleware/recovering_middleware.go b/pkg/references/middleware/recovering_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..b1a4eb6b629e5a48f7508edbe34f5450d5f52b10 --- /dev/null +++ b/pkg/references/middleware/recovering_middleware.go @@ -0,0 +1,44 @@ +// 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/references -i References -t ../../../assets/templates/middleware/recovery -o recovering_middleware.go -l "" + +import ( + "context" + "fmt" + + "git.perx.ru/perxis/perxis-go/pkg/items" + "git.perx.ru/perxis/perxis-go/pkg/references" + "go.uber.org/zap" +) + +// recoveringMiddleware implements references.References that is instrumented with logging +type recoveringMiddleware struct { + logger *zap.Logger + next references.References +} + +// RecoveringMiddleware instruments an implementation of the references.References with simple logging +func RecoveringMiddleware(logger *zap.Logger) Middleware { + return func(next references.References) references.References { + return &recoveringMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *recoveringMiddleware) Get(ctx context.Context, spaceId string, envId string, references []*references.Reference) (items []*items.Item, notfound []*references.Reference, 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, envId, references) +} diff --git a/pkg/roles/middleware/caching_middleware.go b/pkg/roles/middleware/caching_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..19bdfe6b890806a98ac4aed81485f2f5314020b6 --- /dev/null +++ b/pkg/roles/middleware/caching_middleware.go @@ -0,0 +1,80 @@ +package service + +import ( + "context" + "strings" + + "git.perx.ru/perxis/perxis-go/pkg/cache" + service "git.perx.ru/perxis/perxis-go/pkg/roles" +) + +func makeKey(ss ...string) string { + return strings.Join(ss, "-") +} + +func CachingMiddleware(cache *cache.Cache) Middleware { + return func(next service.Roles) service.Roles { + return &cachingMiddleware{ + cache: cache, + next: next, + } + } +} + +type cachingMiddleware struct { + cache *cache.Cache + next service.Roles +} + +func (m cachingMiddleware) Create(ctx context.Context, role *service.Role) (rl *service.Role, err error) { + rl, err = m.next.Create(ctx, role) + if err == nil { + m.cache.Remove(rl.SpaceID) + } + return rl, err +} + +func (m cachingMiddleware) Get(ctx context.Context, spaceId string, roleId string) (rl *service.Role, err error) { + key := makeKey(spaceId, roleId) + value, e := m.cache.Get(key) + if e == nil { + return value.(*service.Role), err + } + rl, err = m.next.Get(ctx, spaceId, roleId) + if err == nil { + m.cache.Set(key, rl) + } + return rl, err +} + +func (m cachingMiddleware) List(ctx context.Context, spaceId string) (roles []*service.Role, err error) { + value, e := m.cache.Get(spaceId) + if e == nil { + return value.([]*service.Role), err + } + roles, err = m.next.List(ctx, spaceId) + if err == nil { + m.cache.Set(spaceId, roles) + } + return roles, err +} + +func (m cachingMiddleware) Update(ctx context.Context, role *service.Role) (err error) { + err = m.next.Update(ctx, role) + if err == nil { + key := makeKey(role.SpaceID, role.ID) + m.cache.Remove(key) + m.cache.Remove(role.SpaceID) + } + return err +} + +func (m cachingMiddleware) Delete(ctx context.Context, spaceId string, roleId string) (err error) { + err = m.next.Delete(ctx, spaceId, roleId) + if err == nil { + key := makeKey(spaceId, roleId) + m.cache.Remove(key) + m.cache.Remove(spaceId) + } + return err +} diff --git a/pkg/roles/middleware/caching_middleware_test.go b/pkg/roles/middleware/caching_middleware_test.go new file mode 100644 index 0000000000000000000000000000000000000000..bb5496b55fae164ea3681e644c7625ab9a2b9407 --- /dev/null +++ b/pkg/roles/middleware/caching_middleware_test.go @@ -0,0 +1,201 @@ +package service + +import ( + "context" + "testing" + "time" + + "git.perx.ru/perxis/perxis-go/pkg/cache" + "git.perx.ru/perxis/perxis-go/pkg/errors" + "git.perx.ru/perxis/perxis-go/pkg/roles" + rsmocks "git.perx.ru/perxis/perxis-go/pkg/roles/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestRolesCache(t *testing.T) { + + const ( + roleID = "roleID" + spaceID = "spaceID" + size = 5 + ttl = 20 * time.Millisecond + ) + + errNotFound := errors.NotFound(errors.New("not found")) + + ctx := context.Background() + + t.Run("Get from cache", func(t *testing.T) { + rl := &rsmocks.Roles{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(rl) + + rl.On("Get", mock.Anything, spaceID, roleID).Return(&roles.Role{ID: roleID, SpaceID: spaceID, Description: "Role"}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, roleID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, roleID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается РїСЂРё повторном запросе получение объекта РёР· кэша.") + + rl.AssertExpectations(t) + }) + + t.Run("List from cache", func(t *testing.T) { + rl := &rsmocks.Roles{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(rl) + + rl.On("List", mock.Anything, spaceID).Return([]*roles.Role{{ID: roleID, SpaceID: spaceID, Description: "Role"}}, 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], "Ожидается РїСЂРё повторном запросе получение объектов РёР· кэша.") + + rl.AssertExpectations(t) + }) + + t.Run("Invalidate cache", func(t *testing.T) { + t.Run("After Update", func(t *testing.T) { + rl := &rsmocks.Roles{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(rl) + + rl.On("Get", mock.Anything, spaceID, roleID).Return(&roles.Role{ID: roleID, SpaceID: spaceID, Description: "Role"}, nil).Once() + rl.On("List", mock.Anything, spaceID).Return([]*roles.Role{{ID: roleID, SpaceID: spaceID, Description: "Role"}}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, roleID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, roleID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается РїСЂРё повторном запросе получение объектов РёР· кэша.") + + 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], "Ожидается РїСЂРё повторном запросе получение объектов РёР· кэша.") + + rl.On("Update", mock.Anything, mock.Anything).Return(nil).Once() + + err = svc.Update(ctx, &roles.Role{ID: roleID, SpaceID: spaceID, Description: "RoleUPD"}) + require.NoError(t, err) + + rl.On("Get", mock.Anything, spaceID, roleID).Return(&roles.Role{ID: roleID, SpaceID: spaceID, Description: "RoleUPD"}, nil).Once() + rl.On("List", mock.Anything, spaceID).Return([]*roles.Role{{ID: roleID, SpaceID: spaceID, Description: "RoleUPD"}}, nil).Once() + + v3, err := svc.Get(ctx, spaceID, roleID) + require.NoError(t, err) + assert.NotSame(t, v2, v3, "Ожидается что кеш объекта был удален после его обновления Рё объект был запрошен РёР· сервиса.") + + vl3, err := svc.List(ctx, spaceID) + require.NoError(t, err) + assert.NotSame(t, vl2[0], vl3[0], "Ожидается что кеш объектов был удален после обновления объекта.") + + rl.AssertExpectations(t) + }) + + t.Run("After Delete", func(t *testing.T) { + rl := &rsmocks.Roles{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(rl) + + rl.On("Get", mock.Anything, spaceID, roleID).Return(&roles.Role{ID: roleID, SpaceID: spaceID, Description: "Role"}, nil).Once() + rl.On("List", mock.Anything, spaceID).Return([]*roles.Role{{ID: roleID, SpaceID: spaceID, Description: "Role"}}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, roleID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, roleID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается РїСЂРё повторном запросе получение объекта РёР· кэша.") + + 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], "Ожидается РїСЂРё повторном запросе получение объектов РёР· кэша.") + + rl.On("Update", mock.Anything, mock.Anything).Return(nil).Once() + + err = svc.Update(ctx, &roles.Role{ID: roleID, SpaceID: spaceID, Description: "RoleUPD"}) + require.NoError(t, err) + + rl.On("Get", mock.Anything, spaceID, roleID).Return(nil, errNotFound).Once() + rl.On("List", mock.Anything, spaceID).Return(nil, errNotFound).Once() + + v3, err := svc.Get(ctx, spaceID, roleID) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидается что после удаления кеш объекта был удален Рё получена ошибка сервиса.") + assert.Nil(t, v3) + + vl3, err := svc.List(ctx, spaceID) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидается что после удаления кеш объекта был удален Рё получена ошибка сервиса.") + assert.Nil(t, vl3) + + rl.AssertExpectations(t) + }) + + t.Run("After Create", func(t *testing.T) { + rl := &rsmocks.Roles{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(rl) + + rl.On("List", mock.Anything, spaceID).Return([]*roles.Role{{ID: roleID, SpaceID: spaceID, Description: "Role"}}, 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], "Ожидается РїСЂРё повторном запросе получение объекта РёР· кэша.") + + rl.On("Create", mock.Anything, mock.Anything).Return(&roles.Role{ID: "roleID2", SpaceID: spaceID, Description: "Role2"}, nil).Once() + + _, err = svc.Create(ctx, &roles.Role{ID: "roleID2", SpaceID: spaceID, Description: "Role2"}) + require.NoError(t, err) + + rl.On("List", mock.Anything, spaceID).Return([]*roles.Role{{ID: roleID, SpaceID: spaceID, Description: "Role"}, {ID: "roleID2", SpaceID: spaceID, Description: "Role2"}}, nil).Once() + + vl3, err := svc.List(ctx, spaceID) + require.NoError(t, err) + assert.Len(t, vl3, 2, "Ожидает что после создания РЅРѕРІРѕРіРѕ объекта, кеш будет очищен Рё объекты запрошены заново РёР· сервиса.") + + rl.AssertExpectations(t) + }) + + t.Run("After TTL expired", func(t *testing.T) { + rl := &rsmocks.Roles{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(rl) + + rl.On("Get", mock.Anything, spaceID, roleID).Return(&roles.Role{ID: roleID, SpaceID: spaceID, Description: "Role"}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, roleID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, roleID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кэша.") + + time.Sleep(2 * ttl) + rl.On("Get", mock.Anything, spaceID, roleID).Return(&roles.Role{ID: roleID, SpaceID: spaceID, Description: "Role"}, nil).Once() + + v3, err := svc.Get(ctx, spaceID, roleID) + require.NoError(t, err) + assert.NotSame(t, v2, v3, "Ожидается что объект был удален РёР· кеша Рё получен заново РёР· сервиса.") + + rl.AssertExpectations(t) + }) + }) +} diff --git a/pkg/roles/middleware/error_logging_middleware.go b/pkg/roles/middleware/error_logging_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..7afe8f1f1003031ddedb437d0422dfb4123d4526 --- /dev/null +++ b/pkg/roles/middleware/error_logging_middleware.go @@ -0,0 +1,80 @@ +package service + +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/error_log +// gowrap: http://github.com/hexdigest/gowrap + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/roles -i Roles -t ../../../assets/templates/middleware/error_log -o error_logging_middleware.go -l "" + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/roles" + "go.uber.org/zap" +) + +// errorLoggingMiddleware implements roles.Roles that is instrumented with logging +type errorLoggingMiddleware struct { + logger *zap.Logger + next roles.Roles +} + +// ErrorLoggingMiddleware instruments an implementation of the roles.Roles with simple logging +func ErrorLoggingMiddleware(logger *zap.Logger) Middleware { + return func(next roles.Roles) roles.Roles { + return &errorLoggingMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *errorLoggingMiddleware) Create(ctx context.Context, role *roles.Role) (created *roles.Role, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Create(ctx, role) +} + +func (m *errorLoggingMiddleware) Delete(ctx context.Context, spaceId string, roleId string) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Delete(ctx, spaceId, roleId) +} + +func (m *errorLoggingMiddleware) Get(ctx context.Context, spaceId string, roleId string) (role *roles.Role, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Get(ctx, spaceId, roleId) +} + +func (m *errorLoggingMiddleware) List(ctx context.Context, spaceId string) (roles []*roles.Role, 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, role *roles.Role) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Update(ctx, role) +} diff --git a/pkg/roles/middleware/logging_middleware.go b/pkg/roles/middleware/logging_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..ab536b6ad9e168adc79691a3127be8a47f9e5ba7 --- /dev/null +++ b/pkg/roles/middleware/logging_middleware.go @@ -0,0 +1,214 @@ +package service + +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/access_log +// gowrap: http://github.com/hexdigest/gowrap + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/roles -i Roles -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/roles" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// loggingMiddleware implements roles.Roles that is instrumented with logging +type loggingMiddleware struct { + logger *zap.Logger + next roles.Roles +} + +// LoggingMiddleware instruments an implementation of the roles.Roles with simple logging +func LoggingMiddleware(logger *zap.Logger) Middleware { + return func(next roles.Roles) roles.Roles { + return &loggingMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *loggingMiddleware) Create(ctx context.Context, role *roles.Role) (created *roles.Role, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "role": role} { + 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, role) + + 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, roleId string) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "roleId": roleId} { + 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, roleId) + + 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) Get(ctx context.Context, spaceId string, roleId string) (role *roles.Role, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "roleId": roleId} { + 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...) + + role, err = m.next.Get(ctx, spaceId, roleId) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "role": role, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Get.Response", fields...) + + return role, err +} + +func (m *loggingMiddleware) List(ctx context.Context, spaceId string) (roles []*roles.Role, 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...) + + roles, 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{}{ + "roles": roles, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("List.Response", fields...) + + return roles, err +} + +func (m *loggingMiddleware) Update(ctx context.Context, role *roles.Role) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "role": role} { + 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, role) + + 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/roles/middleware/middleware.go b/pkg/roles/middleware/middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..0a5198110dfc463b80274230a7d1cbc65283debd --- /dev/null +++ b/pkg/roles/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/roles -i Roles -t ../../../assets/templates/middleware/middleware -o middleware.go -l "" + +import ( + "git.perx.ru/perxis/perxis-go/pkg/roles" + "go.uber.org/zap" +) + +type Middleware func(roles.Roles) roles.Roles + +func WithLog(s roles.Roles, logger *zap.Logger, log_access bool) roles.Roles { + if logger == nil { + logger = zap.NewNop() + } + + logger = logger.Named("Roles") + s = ErrorLoggingMiddleware(logger)(s) + if log_access { + s = LoggingMiddleware(logger)(s) + } + s = RecoveringMiddleware(logger)(s) + return s +} diff --git a/pkg/roles/middleware/recovering_middleware.go b/pkg/roles/middleware/recovering_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..0c0f023b6fa4ccb6f8dc4dadbf59a58bf024a6cb --- /dev/null +++ b/pkg/roles/middleware/recovering_middleware.go @@ -0,0 +1,91 @@ +// 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/roles -i Roles -t ../../../assets/templates/middleware/recovery -o recovering_middleware.go -l "" + +import ( + "context" + "fmt" + + "git.perx.ru/perxis/perxis-go/pkg/roles" + "go.uber.org/zap" +) + +// recoveringMiddleware implements roles.Roles that is instrumented with logging +type recoveringMiddleware struct { + logger *zap.Logger + next roles.Roles +} + +// RecoveringMiddleware instruments an implementation of the roles.Roles with simple logging +func RecoveringMiddleware(logger *zap.Logger) Middleware { + return func(next roles.Roles) roles.Roles { + return &recoveringMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *recoveringMiddleware) Create(ctx context.Context, role *roles.Role) (created *roles.Role, 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, role) +} + +func (m *recoveringMiddleware) Delete(ctx context.Context, spaceId string, roleId 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, roleId) +} + +func (m *recoveringMiddleware) Get(ctx context.Context, spaceId string, roleId string) (role *roles.Role, 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, roleId) +} + +func (m *recoveringMiddleware) List(ctx context.Context, spaceId string) (roles []*roles.Role, 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, role *roles.Role) (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, role) +} diff --git a/pkg/schemaloader/context.go b/pkg/schemaloader/context.go new file mode 100644 index 0000000000000000000000000000000000000000..7407b6b30939981f917c50d734ac770ec69b9e4e --- /dev/null +++ b/pkg/schemaloader/context.go @@ -0,0 +1,30 @@ +package schemaloader + +import "context" + +type LoaderContext struct { + SpaceID string + EnvID string +} + +type loaderCtxKey struct{} + +func WithContext(ctx context.Context, loaderContext *LoaderContext) context.Context { + if ctx == nil { + ctx = context.Background() + } + p, _ := ctx.Value(loaderCtxKey{}).(*LoaderContext) + if p != nil { + *p = *loaderContext + return ctx + } + return context.WithValue(ctx, loaderCtxKey{}, loaderContext) +} + +func GetContext(ctx context.Context) *LoaderContext { + p, _ := ctx.Value(loaderCtxKey{}).(*LoaderContext) + if p == nil { + return new(LoaderContext) + } + return p +} diff --git a/pkg/schemaloader/loader.go b/pkg/schemaloader/loader.go new file mode 100644 index 0000000000000000000000000000000000000000..e27baf4ea8b509adff88f7b9c5e99a48f15960b7 --- /dev/null +++ b/pkg/schemaloader/loader.go @@ -0,0 +1,92 @@ +package schemaloader + +import ( + "context" + "net/url" + "strings" + + "git.perx.ru/perxis/perxis-go/pkg/collections" + "git.perx.ru/perxis/perxis-go/pkg/errors" + "git.perx.ru/perxis/perxis-go/pkg/schema/field" +) + +// NewLoader возвращает новый загрузчик схем РёР· коллекций +// используется только РЅР° сервере +// РЅР° клиенте РЅСѓР¶РЅРѕ использовать методы получения полностью загруженных схем, для которых разрешение РїСЂРѕРёСЃС…РѕРґРёС‚ РЅР° сервере +func NewLoader(svc collections.Collections) field.Loader { + return &loader{svc: svc} +} + +type loader struct { + svc collections.Collections +} + +// Load - возвращает поля РїРѕ референсу РёР· коллекций (РЅРµ загруженные) +func (l *loader) Load(ctx context.Context, ref string) ([]*field.Field, error) { + spaceID, envID, colID, err := parseRef(ctx, ref) + if err != nil { + return nil, err + } + + filter := &collections.Filter{ID: []string{colID}} + + collections, err := l.svc.List(ctx, spaceID, envID, filter) + if err != nil { + return nil, errors.Wrapf(err, "schemaloader: failed to get collections for \"%s\"", ref) + } + + var schemas []*field.Field + for _, s := range collections { + if s.Schema != nil { + schemas = append(schemas, &s.Schema.Field) + } + } + + if len(schemas) == 0 { + return nil, errors.Errorf("schema not found \"%s\"", ref) + } + + return schemas, nil +} + +func parseRef(ctx context.Context, ref string) (spaceID, envID, colID string, err error) { + var u *url.URL + if u, err = url.Parse(ref); err != nil { + return + } + + parts := strings.SplitN(u.Path, "/", 3) + + switch len(parts) { + case 1: + colID = parts[0] + case 2: + spaceID = parts[0] + envID = "master" + colID = parts[1] + case 3: + spaceID = parts[0] + envID = parts[1] + colID = parts[2] + } + + if colID == "" { + err = errors.Errorf("invalid schema reference \"%s\"", ref) + } + + if loaderCtx := GetContext(ctx); loaderCtx != nil { + if spaceID == "" { + spaceID = loaderCtx.SpaceID + } + + if envID == "" { + envID = loaderCtx.EnvID + } + } + + if spaceID == "" { + err = errors.Errorf("can't identify space for reference \"%s\"", ref) + } + + return +} diff --git a/pkg/schemaloader/loader_test.go b/pkg/schemaloader/loader_test.go new file mode 100644 index 0000000000000000000000000000000000000000..f796fd8f669e31140e4171955c4d7db8723cec26 --- /dev/null +++ b/pkg/schemaloader/loader_test.go @@ -0,0 +1,141 @@ +package schemaloader + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +//func Test_Load(t *testing.T) { +// +// const ( +// spaceID = "SpaceID" +// envID = "envID" +// colID = "colID" +// uri = "/colID#fieldID" +// ) +// +// t.Run("Load schema (success)", func(t *testing.T) { +// collSvs := &mocks.Collections{} +// +// sch := schema.New( +// "first_name", field.String(), +// "last_name", field.String(), +// ) +// +// cl := &collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "Collection", Schema: sch} +// collSvs.On("List", mock.Anything, spaceID, envID, mock.AnythingOfType("*collections.Filter")).Run(func(args mock.Arguments) { +// filter := args[3].(*collections.Filter) +// +// assert.Equal(t, &collections.Filter{ID: []string{"colID"}}, filter, "Фильтр должен содержать идентификатор коллекции") +// }).Return([]*collections.Collection{cl}, nil).Once() +// +// loader := NewLoader(collSvs, spaceID, envID) +// schemas, err := loader.Load(nil, uri) +// +// require.NoError(t, err, "Ожидается успешное завершение") +// require.Equal(t, []*field.Field{&sch.Field}, schemas, "Метод должен возвращать срез схем") +// collSvs.AssertExpectations(t) +// }) +// +// t.Run("Collection doesn't have schema", func(t *testing.T) { +// collSvs := &mocks.Collections{} +// +// cl := &collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "Collection"} +// collSvs.On("List", mock.Anything, spaceID, envID, mock.AnythingOfType("*collections.Filter")).Run(func(args mock.Arguments) { +// filter := args[3].(*collections.Filter) +// +// assert.Equal(t, &collections.Filter{ID: []string{"colID"}}, filter, "Фильтр должен содержать идентификатор коллекции") +// }).Return([]*collections.Collection{cl}, nil).Once() +// +// loader := NewLoader(collSvs, spaceID, envID) +// schemas, err := loader.Load(nil, uri) +// +// require.Error(t, err, "Ожидается ошибка") +// require.Contains(t, err.Error(), "schema not found") +// require.Nil(t, schemas, "Метод должен вернуть nil") +// //assert.Nil(t, schemas, "Метод должен вернуть nil") +// collSvs.AssertExpectations(t) +// }) +// +// t.Run("Loader not found collection", func(t *testing.T) { +// collSvs := &mocks.Collections{} +// +// collSvs.On("List", mock.Anything, spaceID, envID, mock.AnythingOfType("*collections.Filter")).Run(func(args mock.Arguments) { +// filter := args[3].(*collections.Filter) +// +// assert.Equal(t, &collections.Filter{ID: []string{"colID"}}, filter, "Фильтр должен содержать идентификатор коллекции") +// }).Return([]*collections.Collection{}, nil).Once() +// +// loader := NewLoader(collSvs, spaceID, envID) +// schemas, err := loader.Load(nil, uri) +// +// require.Error(t, err, "Ожидается ошибка") +// require.Contains(t, err.Error(), "schema not found") +// require.Nil(t, schemas, "Метод должен вернуть nil") +// collSvs.AssertExpectations(t) +// }) +// +// t.Run("Collection service return error", func(t *testing.T) { +// collSvs := &mocks.Collections{} +// +// collSvs.On("List", mock.Anything, spaceID, envID, mock.AnythingOfType("*collections.Filter")).Run(func(args mock.Arguments) { +// filter := args[3].(*collections.Filter) +// +// assert.Equal(t, &collections.Filter{ID: []string{"colID"}}, filter, "Фильтр должен содержать идентификатор коллекции") +// }).Return(nil, errors.New("storage error")).Once() +// +// loader := NewLoader(collSvs, spaceID, envID) +// schemas, err := loader.Load(nil, uri) +// +// require.Error(t, err, "Ожидается ошибка") +// require.Contains(t, err.Error(), "failed to get schema") +// require.Nil(t, schemas, "Метод должен вернуть nil") +// collSvs.AssertExpectations(t) +// }) +// +// t.Run("ParseMask return error", func(t *testing.T) { +// collSvs := &mocks.Collections{} +// +// loader := NewLoader(collSvs, spaceID, envID) +// schemas, err := loader.Load(nil, "") +// +// require.Error(t, err, "Ожидается ошибка") +// require.Contains(t, err.Error(), "invalid schema reference") +// require.Nil(t, schemas, "Метод должен вернуть nil") +// collSvs.AssertExpectations(t) +// }) +//} + +func Test_parseRef(t *testing.T) { + ctx := WithContext(nil, &LoaderContext{SpaceID: "spc", EnvID: "env"}) + tests := []struct { + ref string + ctx context.Context + wantSpaceID string + wantEnvId string + wantCollection string + wantErr assert.ErrorAssertionFunc + }{ + {"col", ctx, "spc", "env", "col", assert.NoError}, + {"/col", ctx, "spc", "master", "col", assert.NoError}, + {"spc1/env1/col", ctx, "spc1", "env1", "col", assert.NoError}, + {"spc1/env1/col#fld", ctx, "spc1", "env1", "col", assert.NoError}, + {"col%3f*", ctx, "spc", "env", "col?*", assert.NoError}, + {"#fld", ctx, "spc", "env", "", assert.Error}, + } + + for _, tt := range tests { + t.Run(tt.ref, func(t *testing.T) { + gotSpaceID, gotEnvId, gotCollection, err := parseRef(tt.ctx, tt.ref) + if !tt.wantErr(t, err, fmt.Sprintf("parseRef(%v)", tt.ref)) { + return + } + assert.Equalf(t, tt.wantSpaceID, gotSpaceID, "parseRef(%v)", tt.ref) + assert.Equalf(t, tt.wantEnvId, gotEnvId, "parseRef(%v)", tt.ref) + assert.Equalf(t, tt.wantCollection, gotCollection, "parseRef(%v)", tt.ref) + }) + } +} diff --git a/pkg/spaces/middleware/caching_middleware.go b/pkg/spaces/middleware/caching_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..62396fc8f1e9101f92885330a50797e26dbdebe2 --- /dev/null +++ b/pkg/spaces/middleware/caching_middleware.go @@ -0,0 +1,106 @@ +package service + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/cache" + service "git.perx.ru/perxis/perxis-go/pkg/spaces" +) + +func orgKey(orgID string) string { return "org-" + orgID } + +func CachingMiddleware(cache *cache.Cache) Middleware { + return func(next service.Spaces) service.Spaces { + m := &cachingMiddleware{ + cache: cache, + next: next, + } + + return m + } +} + +type cachingMiddleware struct { + cache *cache.Cache + next service.Spaces +} + +func (m cachingMiddleware) Create(ctx context.Context, space *service.Space) (sp *service.Space, err error) { + + sp, err = m.next.Create(ctx, space) + if err == nil { + m.cache.Remove(orgKey(sp.OrgID)) + } + return sp, err +} + +func (m cachingMiddleware) Get(ctx context.Context, spaceId string) (sp *service.Space, err error) { + + value, e := m.cache.Get(spaceId) + if e == nil { + return value.(*service.Space), err + } + sp, err = m.next.Get(ctx, spaceId) + if err == nil { + m.cache.Set(spaceId, sp) + } + return sp, err +} + +func (m cachingMiddleware) List(ctx context.Context, orgId string) (spaces []*service.Space, err error) { + + value, e := m.cache.Get(orgKey(orgId)) + if e == nil { + return value.([]*service.Space), err + } + spaces, err = m.next.List(ctx, orgId) + if err == nil { + m.cache.Set(orgKey(orgId), spaces) + for _, s := range spaces { + m.cache.Set(s.ID, s) + } + } + return spaces, err +} + +func (m cachingMiddleware) Update(ctx context.Context, space *service.Space) (err error) { + + err = m.next.Update(ctx, space) + if err == nil { + value, e := m.cache.Get(space.ID) + if e == nil { + space := value.(*service.Space) + m.cache.Remove(orgKey(space.OrgID)) + } + m.cache.Remove(space.ID) + } + return err +} + +func (m cachingMiddleware) UpdateConfig(ctx context.Context, spaceId string, config *service.Config) (err error) { + + err = m.next.UpdateConfig(ctx, spaceId, config) + if err == nil { + value, e := m.cache.Get(spaceId) + if e == nil { + space := value.(*service.Space) + m.cache.Remove(orgKey(space.OrgID)) + } + m.cache.Remove(spaceId) + } + return err +} + +func (m cachingMiddleware) Delete(ctx context.Context, spaceId string) (err error) { + + err = m.next.Delete(ctx, spaceId) + if err == nil { + value, e := m.cache.Get(spaceId) + if e == nil { + space := value.(*service.Space) + m.cache.Remove(orgKey(space.OrgID)) + } + m.cache.Remove(spaceId) + } + return err +} diff --git a/pkg/spaces/middleware/caching_middleware_test.go b/pkg/spaces/middleware/caching_middleware_test.go new file mode 100644 index 0000000000000000000000000000000000000000..2cfaf98db8713081225b3f01f39782f487292161 --- /dev/null +++ b/pkg/spaces/middleware/caching_middleware_test.go @@ -0,0 +1,241 @@ +package service + +import ( + "context" + "testing" + "time" + + "git.perx.ru/perxis/perxis-go/pkg/cache" + "git.perx.ru/perxis/perxis-go/pkg/errors" + + "git.perx.ru/perxis/perxis-go/pkg/spaces" + spmocks "git.perx.ru/perxis/perxis-go/pkg/spaces/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestRolesCache(t *testing.T) { + + const ( + spaceID = "spaceID" + orgID = "orgID" + size = 5 + ttl = 20 * time.Millisecond + ) + + errNotFound := errors.NotFound(errors.New("not found")) + + ctx := context.Background() + + t.Run("Get from cache", func(t *testing.T) { + sp := &spmocks.Spaces{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(sp) + + sp.On("Get", mock.Anything, spaceID).Return(&spaces.Space{ID: spaceID, OrgID: orgID, Name: "Space"}, nil).Once() + + v1, err := svc.Get(ctx, spaceID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается РїСЂРё повторном запросе получение объекта РёР· кэша.") + + sp.AssertExpectations(t) + }) + + t.Run("List from cache", func(t *testing.T) { + sp := &spmocks.Spaces{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(sp) + + sp.On("List", mock.Anything, orgID).Return([]*spaces.Space{{ID: spaceID, OrgID: orgID, Name: "Space"}}, nil).Once() + + vl1, err := svc.List(ctx, orgID) + require.NoError(t, err) + + vl2, err := svc.List(ctx, orgID) + require.NoError(t, err) + assert.Same(t, vl1[0], vl2[0], "Ожидается РїСЂРё повторном запросе получение объектов РёР· кэша.") + + sp.AssertExpectations(t) + }) + + t.Run("Invalidate cache", func(t *testing.T) { + t.Run("After Update", func(t *testing.T) { + sp := &spmocks.Spaces{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(sp) + + sp.On("Get", mock.Anything, spaceID).Return(&spaces.Space{ID: spaceID, OrgID: orgID, Name: "Space"}, nil).Once() + sp.On("List", mock.Anything, orgID).Return([]*spaces.Space{{ID: spaceID, OrgID: orgID, Name: "Space"}}, nil).Once() + + v1, err := svc.Get(ctx, spaceID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается РїСЂРё повторном запросе получение объекта РёР· кэша.") + + vl1, err := svc.List(ctx, orgID) + require.NoError(t, err) + + vl2, err := svc.List(ctx, orgID) + require.NoError(t, err) + assert.Same(t, vl1[0], vl2[0], "Ожидается РїСЂРё повторном запросе получение объектов РёР· кэша.") + + sp.On("Update", mock.Anything, mock.Anything).Return(nil).Once() + + err = svc.Update(ctx, &spaces.Space{ID: spaceID, OrgID: orgID, Name: "SpaceUPD"}) + require.NoError(t, err) + + sp.On("Get", mock.Anything, spaceID).Return(&spaces.Space{ID: spaceID, OrgID: orgID, Name: "SpaceUPD"}, nil).Once() + sp.On("List", mock.Anything, orgID).Return([]*spaces.Space{{ID: spaceID, OrgID: orgID, Name: "SpaceUPD"}}, nil).Once() + + v3, err := svc.Get(ctx, spaceID) + require.NoError(t, err) + assert.NotSame(t, v2, v3, "Ожидается что кеш объекта был удален после обновления объекта.") + + vl3, err := svc.List(ctx, orgID) + require.NoError(t, err) + assert.NotSame(t, vl2[0], vl3[0], "Ожидается что кеш объектов был удален после обновления объекта.") + + sp.AssertExpectations(t) + }) + + t.Run("After UpdateConfig", func(t *testing.T) { + sp := &spmocks.Spaces{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(sp) + + sp.On("Get", mock.Anything, spaceID).Return(&spaces.Space{ID: spaceID, OrgID: orgID, Name: "Space"}, nil).Once() + sp.On("List", mock.Anything, orgID).Return([]*spaces.Space{{ID: spaceID, OrgID: orgID, Name: "Space"}}, nil).Once() + + v1, err := svc.Get(ctx, spaceID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается РїСЂРё повторном запросе получение объекта РёР· кэша.") + + vl1, err := svc.List(ctx, orgID) + require.NoError(t, err) + + vl2, err := svc.List(ctx, orgID) + require.NoError(t, err) + assert.Same(t, vl1[0], vl2[0], "Ожидается РїСЂРё повторном запросе получение объектов РёР· кэша.") + + sp.On("UpdateConfig", mock.Anything, spaceID, mock.Anything).Return(nil).Once() + + err = svc.UpdateConfig(ctx, spaceID, &spaces.Config{Features: []string{"feature"}}) + require.NoError(t, err) + + sp.On("Get", mock.Anything, spaceID).Return(&spaces.Space{ID: spaceID, OrgID: orgID, Name: "SpaceUPD", Config: &spaces.Config{Features: []string{"feature"}}}, nil).Once() + sp.On("List", mock.Anything, orgID).Return([]*spaces.Space{{ID: spaceID, OrgID: orgID, Name: "SpaceUPD", Config: &spaces.Config{Features: []string{"feature"}}}}, nil).Once() + + v3, err := svc.Get(ctx, spaceID) + require.NoError(t, err) + assert.NotSame(t, v2, v3, "Ожидается что кеш объекта был удален после обновления объекта.") + + vl3, err := svc.List(ctx, orgID) + require.NoError(t, err) + assert.NotSame(t, vl2[0], vl3[0], "Ожидается что кеш объектов был удален после обновления объекта.") + + sp.AssertExpectations(t) + }) + + t.Run("After Delete", func(t *testing.T) { + sp := &spmocks.Spaces{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(sp) + + sp.On("Get", mock.Anything, spaceID).Return(&spaces.Space{ID: spaceID, OrgID: orgID, Name: "Space"}, nil).Once() + sp.On("List", mock.Anything, orgID).Return([]*spaces.Space{{ID: spaceID, OrgID: orgID, Name: "Space"}}, nil).Once() + + v1, err := svc.Get(ctx, spaceID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается РїСЂРё повторном запросе получение объекта РёР· кэша.") + + vl1, err := svc.List(ctx, orgID) + require.NoError(t, err) + + vl2, err := svc.List(ctx, orgID) + require.NoError(t, err) + assert.Same(t, vl1[0], vl2[0], "Ожидается РїСЂРё повторном запросе получение объектов РёР· кэша.") + + sp.On("Delete", mock.Anything, spaceID).Return(nil).Once() + + err = svc.Delete(ctx, spaceID) + require.NoError(t, err) + + sp.On("Get", mock.Anything, spaceID).Return(nil, errNotFound).Once() + sp.On("List", mock.Anything, orgID).Return([]*spaces.Space{}, nil).Once() + + _, err = svc.Get(ctx, spaceID) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидается что после удаления объекта кеш был удален Рё получена ошибка РѕС‚ сервиса.") + + vl3, err := svc.List(ctx, orgID) + require.NoError(t, err) + assert.Len(t, vl3, 0, "Ожидается что после удаления кеш объектов был удален.") + + sp.AssertExpectations(t) + }) + + t.Run("After Create", func(t *testing.T) { + sp := &spmocks.Spaces{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(sp) + + sp.On("List", mock.Anything, orgID).Return([]*spaces.Space{{ID: spaceID, OrgID: orgID, Name: "Space"}}, nil).Once() + + vl1, err := svc.List(ctx, orgID) + require.NoError(t, err) + + vl2, err := svc.List(ctx, orgID) + require.NoError(t, err) + assert.Same(t, vl1[0], vl2[0], "Ожидается РїСЂРё повторном запросе получение объектов РёР· кэша.") + + sp.On("Create", mock.Anything, mock.Anything).Return(&spaces.Space{ID: "spaceID2", OrgID: orgID, Name: "Space2"}, nil).Once() + + _, err = svc.Create(ctx, &spaces.Space{ID: "spaceID2", OrgID: orgID, Name: "Space2"}) + require.NoError(t, err) + + sp.On("List", mock.Anything, orgID).Return([]*spaces.Space{{ID: spaceID, OrgID: orgID, Name: "Space"}, {ID: "spaceID2", OrgID: orgID, Name: "Space2"}}, nil).Once() + + vl3, err := svc.List(ctx, orgID) + require.NoError(t, err) + assert.NotSame(t, vl2[0], vl3[0], "Ожидается что кеш объектов был удален после создания РЅРѕРІРѕРіРѕ объекта.") + + sp.AssertExpectations(t) + }) + + t.Run("After TTL expired", func(t *testing.T) { + sp := &spmocks.Spaces{} + + svc := CachingMiddleware(cache.NewCache(size, ttl))(sp) + + sp.On("Get", mock.Anything, spaceID).Return(&spaces.Space{ID: spaceID, OrgID: orgID, Name: "Space"}, nil).Once() + + v1, err := svc.Get(ctx, spaceID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается РїСЂРё повторном запросе получение объекта РёР· кэша.") + + time.Sleep(2 * ttl) + sp.On("Get", mock.Anything, spaceID).Return(&spaces.Space{ID: spaceID, OrgID: orgID, Name: "Space"}, nil).Once() + + v3, err := svc.Get(ctx, spaceID) + require.NoError(t, err) + assert.NotSame(t, v2, v3, "Ожидается удаление объекта РёР· кэша РїРѕ истечению ttl.") + + sp.AssertExpectations(t) + }) + }) +} diff --git a/pkg/spaces/middleware/error_logging_middleware.go b/pkg/spaces/middleware/error_logging_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..85b629ba32abe38e5030077e8fe81b04206650b0 --- /dev/null +++ b/pkg/spaces/middleware/error_logging_middleware.go @@ -0,0 +1,90 @@ +package service + +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/error_log +// gowrap: http://github.com/hexdigest/gowrap + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/spaces -i Spaces -t ../../../assets/templates/middleware/error_log -o error_logging_middleware.go -l "" + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/spaces" + "go.uber.org/zap" +) + +// errorLoggingMiddleware implements spaces.Spaces that is instrumented with logging +type errorLoggingMiddleware struct { + logger *zap.Logger + next spaces.Spaces +} + +// ErrorLoggingMiddleware instruments an implementation of the spaces.Spaces with simple logging +func ErrorLoggingMiddleware(logger *zap.Logger) Middleware { + return func(next spaces.Spaces) spaces.Spaces { + return &errorLoggingMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *errorLoggingMiddleware) Create(ctx context.Context, space *spaces.Space) (created *spaces.Space, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Create(ctx, space) +} + +func (m *errorLoggingMiddleware) Delete(ctx context.Context, spaceId string) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Delete(ctx, spaceId) +} + +func (m *errorLoggingMiddleware) Get(ctx context.Context, spaceId string) (space *spaces.Space, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Get(ctx, spaceId) +} + +func (m *errorLoggingMiddleware) List(ctx context.Context, orgId string) (spaces []*spaces.Space, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.List(ctx, orgId) +} + +func (m *errorLoggingMiddleware) Update(ctx context.Context, space *spaces.Space) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Update(ctx, space) +} + +func (m *errorLoggingMiddleware) UpdateConfig(ctx context.Context, spaceId string, config *spaces.Config) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.UpdateConfig(ctx, spaceId, config) +} diff --git a/pkg/spaces/middleware/logging_middleware.go b/pkg/spaces/middleware/logging_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..1471ea19535139210e79968e5a4522001a0b2ae3 --- /dev/null +++ b/pkg/spaces/middleware/logging_middleware.go @@ -0,0 +1,248 @@ +package service + +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/access_log +// gowrap: http://github.com/hexdigest/gowrap + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/spaces -i Spaces -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/spaces" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// loggingMiddleware implements spaces.Spaces that is instrumented with logging +type loggingMiddleware struct { + logger *zap.Logger + next spaces.Spaces +} + +// LoggingMiddleware instruments an implementation of the spaces.Spaces with simple logging +func LoggingMiddleware(logger *zap.Logger) Middleware { + return func(next spaces.Spaces) spaces.Spaces { + return &loggingMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *loggingMiddleware) Create(ctx context.Context, space *spaces.Space) (created *spaces.Space, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "space": space} { + 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, space) + + 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) (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("Delete.Request", fields...) + + err = m.next.Delete(ctx, spaceId) + + 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) Get(ctx context.Context, spaceId string) (space *spaces.Space, 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("Get.Request", fields...) + + space, err = m.next.Get(ctx, spaceId) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "space": space, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Get.Response", fields...) + + return space, err +} + +func (m *loggingMiddleware) List(ctx context.Context, orgId string) (spaces []*spaces.Space, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "orgId": orgId} { + 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...) + + spaces, err = m.next.List(ctx, orgId) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "spaces": spaces, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("List.Response", fields...) + + return spaces, err +} + +func (m *loggingMiddleware) Update(ctx context.Context, space *spaces.Space) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "space": space} { + 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, space) + + 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 +} + +func (m *loggingMiddleware) UpdateConfig(ctx context.Context, spaceId string, config *spaces.Config) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "config": config} { + 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("UpdateConfig.Request", fields...) + + err = m.next.UpdateConfig(ctx, spaceId, config) + + 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("UpdateConfig.Response", fields...) + + return err +} diff --git a/pkg/spaces/middleware/middleware.go b/pkg/spaces/middleware/middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..9d9d2243026f6c1152f7625e4c15bd26b5b7b5fc --- /dev/null +++ b/pkg/spaces/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/spaces -i Spaces -t ../../../assets/templates/middleware/middleware -o middleware.go -l "" + +import ( + "git.perx.ru/perxis/perxis-go/pkg/spaces" + "go.uber.org/zap" +) + +type Middleware func(spaces.Spaces) spaces.Spaces + +func WithLog(s spaces.Spaces, logger *zap.Logger, log_access bool) spaces.Spaces { + if logger == nil { + logger = zap.NewNop() + } + + logger = logger.Named("Spaces") + s = ErrorLoggingMiddleware(logger)(s) + if log_access { + s = LoggingMiddleware(logger)(s) + } + s = RecoveringMiddleware(logger)(s) + return s +} diff --git a/pkg/spaces/middleware/recovering_middleware.go b/pkg/spaces/middleware/recovering_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..5ca795bf5c188603794603ec11fc0c8a79479524 --- /dev/null +++ b/pkg/spaces/middleware/recovering_middleware.go @@ -0,0 +1,103 @@ +// 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/spaces -i Spaces -t ../../../assets/templates/middleware/recovery -o recovering_middleware.go -l "" + +import ( + "context" + "fmt" + + "git.perx.ru/perxis/perxis-go/pkg/spaces" + "go.uber.org/zap" +) + +// recoveringMiddleware implements spaces.Spaces that is instrumented with logging +type recoveringMiddleware struct { + logger *zap.Logger + next spaces.Spaces +} + +// RecoveringMiddleware instruments an implementation of the spaces.Spaces with simple logging +func RecoveringMiddleware(logger *zap.Logger) Middleware { + return func(next spaces.Spaces) spaces.Spaces { + return &recoveringMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *recoveringMiddleware) Create(ctx context.Context, space *spaces.Space) (created *spaces.Space, 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, space) +} + +func (m *recoveringMiddleware) Delete(ctx context.Context, spaceId 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) +} + +func (m *recoveringMiddleware) Get(ctx context.Context, spaceId string) (space *spaces.Space, 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) +} + +func (m *recoveringMiddleware) List(ctx context.Context, orgId string) (spaces []*spaces.Space, 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, orgId) +} + +func (m *recoveringMiddleware) Update(ctx context.Context, space *spaces.Space) (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, space) +} + +func (m *recoveringMiddleware) UpdateConfig(ctx context.Context, spaceId string, config *spaces.Config) (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.UpdateConfig(ctx, spaceId, config) +} diff --git a/pkg/template/builder.go b/pkg/template/builder.go new file mode 100644 index 0000000000000000000000000000000000000000..9a4ddcdb73e6b5c585d77088630096351d19b2bd --- /dev/null +++ b/pkg/template/builder.go @@ -0,0 +1,171 @@ +package template + +import ( + "bytes" + "context" + "text/template" + + "git.perx.ru/perxis/perxis-go/pkg/content" +) + +type Builder struct { + ctx context.Context + cnt *content.Content + SpaceID string + EnvID string + funcs template.FuncMap + data map[string]interface{} +} + +func NewBuilder(cnt *content.Content, space, env string) *Builder { + return &Builder{ + ctx: context.Background(), + cnt: cnt, + SpaceID: space, + EnvID: env, + funcs: make(template.FuncMap), + } +} + +func (b *Builder) getFuncs() template.FuncMap { + return template.FuncMap{ + "lookup": getLookup(b), + "system": getSystem(b), + } +} + +func (b *Builder) WithData(data map[string]interface{}) *Builder { + bld := *b + bld.data = data + return &bld +} + +func (b *Builder) WithKV(kv ...any) *Builder { + bld := *b + if bld.data == nil { + bld.data = make(map[string]interface{}, 10) + } + for i := 0; i < len(kv)-1; i += 2 { + k, _ := kv[i].(string) + v := kv[i+1] + if k != "" && v != nil { + bld.data[k] = v + } + } + return &bld +} + +func (b *Builder) GetData() map[string]interface{} { + return b.data +} + +func (b *Builder) WithSpace(space, env string) *Builder { + bld := *b + bld.SpaceID = space + bld.EnvID = env + return &bld +} + +func (b *Builder) WithContext(ctx context.Context) *Builder { + bld := *b + bld.ctx = ctx + return &bld +} + +func (b *Builder) Context() context.Context { + return b.ctx +} + +func (b *Builder) Template() *template.Template { + return template.New("main").Funcs(b.getFuncs()) +} + +func (b *Builder) Execute(str string, data ...any) (string, error) { + t := b.Template() + buf := new(bytes.Buffer) + t, err := t.Parse(str) + if err != nil { + return "", err + } + if err = t.Execute(buf, b.getData(data...)); err != nil { + return "", err + } + return buf.String(), nil +} + +func (b *Builder) ExecuteList(str []string, data ...any) ([]string, error) { + t := b.Template() + result := make([]string, len(str)) + buffer := new(bytes.Buffer) + for i, tmpl := range str { + if tmpl == "" { + continue + } + t, err := t.Parse(tmpl) + if err != nil { + return []string{}, err + } + if err = t.Execute(buffer, b.getData(data...)); err != nil { + return []string{}, err + } + result[i] = buffer.String() + buffer.Reset() + } + return result, nil +} + +func (b *Builder) ExecuteMap(str map[string]interface{}, data ...any) (map[string]interface{}, error) { + result := make(map[string]interface{}, len(str)) + for k, v := range str { + switch t := v.(type) { + case string: + value, err := b.Execute(t, data...) + if err != nil { + return nil, err + } + v = value + case []string: + values, err := b.ExecuteList(append([]string{k}, t...), data...) + if err != nil { + return nil, err + } + k = values[0] + vv := make([]interface{}, 0, len(t)) + for _, val := range values[1:] { + vv = append(vv, val) + } + v = vv + } + + result[k] = v + } + return result, nil +} + +func (b *Builder) getData(data ...any) any { + if len(data) == 0 { + return b.data + } + + var res map[string]interface{} + for _, v := range data { + if m, ok := v.(map[string]interface{}); ok && b.data != nil { + res = mergeMaps(b.data, m) + } + } + if res != nil { + return res + } + + return data[0] +} + +func mergeMaps(in ...map[string]interface{}) map[string]interface{} { + out := make(map[string]interface{}) + for _, i := range in { + for k, v := range i { + out[k] = v + } + } + return out +} diff --git a/pkg/template/builder_test.go b/pkg/template/builder_test.go new file mode 100644 index 0000000000000000000000000000000000000000..f8e2b34440dd73259a6ccd3ce9202a556caa5298 --- /dev/null +++ b/pkg/template/builder_test.go @@ -0,0 +1,272 @@ +package template + +import ( + "context" + "errors" + "testing" + "text/template" + + "git.perx.ru/perxis/perxis-go/pkg/content" + "git.perx.ru/perxis/perxis-go/pkg/items" + mocksitems "git.perx.ru/perxis/perxis-go/pkg/items/mocks" + "github.com/stretchr/testify/assert" +) + +func TestBuilder_Execute(t *testing.T) { + tests := []struct { + name string + ctx context.Context + cnt *content.Content + SpaceID string + EnvID string + funcs template.FuncMap + str string + data any + want any + wantErr bool + + itemsCall func(itemsSvc *mocksitems.Items) + }{ + {name: "error", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: "hello {{ .a }}", data: "world", want: "", wantErr: true}, + {name: "empty", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: "", data: "", want: "", wantErr: false}, + {name: "#1", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: "hello {{ . }}", data: "world", want: "hello world", wantErr: false}, + {name: "#2", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: "{{ . }}", data: "world", want: "world", wantErr: false}, + {name: "#3 ", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: "", data: "world", want: "", wantErr: false}, + {name: "#4 ", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: "hello", data: "world", want: "hello", wantErr: false}, + {name: "lookup", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: "hello, {{ lookup \"secrets.dev.key\" }}", data: "", want: "hello, Luk", wantErr: false, itemsCall: func(itemsSvc *mocksitems.Items) { + itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev").Return(&items.Item{ + ID: "dev", + SpaceID: "space", + EnvID: "env", + CollectionID: "secrets", + Data: map[string]interface{}{ + "id": "dev", + "key": "Luk", + }, + }, nil).Once() + }}, + {name: "lookup with slice", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: "numbers {{ lookup \"secrets.dev.slice\" }}", data: "", want: "numbers [1 2 3]", wantErr: false, itemsCall: func(itemsSvc *mocksitems.Items) { + itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev").Return(&items.Item{ + ID: "dev", + SpaceID: "space", + EnvID: "env", + CollectionID: "secrets", + Data: map[string]interface{}{ + "id": "dev", + "slice": []int{1, 2, 3}, + }, + }, nil).Once() + }}, + {name: "lookup with empty Data", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: "numbers {{ lookup \"secrets.dev.slice\" }}", data: "", want: "numbers <no value>", wantErr: false, itemsCall: func(itemsSvc *mocksitems.Items) { + itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev").Return(&items.Item{ + ID: "dev", + SpaceID: "space", + EnvID: "env", + CollectionID: "secrets", + Data: map[string]interface{}{}, + }, nil).Once() + }}, + {name: "lookup with incorrect field", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: "hello {{ lookup \"secrets.dev.incorrect\" }}", data: "", want: "hello <no value>", wantErr: false, itemsCall: func(itemsSvc *mocksitems.Items) { + itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev").Return(&items.Item{ + ID: "dev", + SpaceID: "space", + EnvID: "env", + CollectionID: "secrets", + Data: map[string]interface{}{ + "id": "dev", + "key": "1234", + }, + }, nil).Once() + }}, + {name: "lookup not found", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: "hello {{ lookup \"secrets.prod.pass\" }}", data: "", want: "", wantErr: true, itemsCall: func(itemsSvc *mocksitems.Items) { + itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "prod").Return(nil, errors.New("not found")).Once() + }}, + {name: "lookup without itemID", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: "hello {{ lookup \"secrets.pass\" }}", data: "", want: "", wantErr: true}, + {name: "system ", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: "hello {{ system.SpaceID }}", data: "", want: "hello space", wantErr: false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + itemsSvc := &mocksitems.Items{} + if tt.itemsCall != nil { + tt.itemsCall(itemsSvc) + } + tt.cnt = &content.Content{ + Items: itemsSvc, + } + b := &Builder{ + ctx: tt.ctx, + cnt: tt.cnt, + SpaceID: tt.SpaceID, + EnvID: tt.EnvID, + funcs: tt.funcs, + } + + got, err := b.Execute(tt.str, tt.data) + if tt.wantErr == true { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tt.want, got) + if tt.itemsCall != nil { + itemsSvc.AssertExpectations(t) + } + }) + } +} + +func TestBuilder_ExecuteList(t *testing.T) { + tests := []struct { + name string + ctx context.Context + cnt *content.Content + SpaceID string + EnvID string + funcs template.FuncMap + str []string + data any + want []string + wantErr bool + + itemsCall func(itemsSvc *mocksitems.Items) + }{ + {name: "error", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: []string{"hello { . }}", "go {{ . }"}, data: "world", want: []string{}, wantErr: true}, + {name: "empty", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: []string{""}, data: "world", want: []string{""}, wantErr: false}, + {name: "#1", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: []string{"hello {{ . }}", "go {{ . }}"}, data: "world", want: []string{"hello world", "go world"}, wantErr: false}, + {name: "#2", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: []string{"{{ . }}"}, data: "world", want: []string{"world"}, wantErr: false}, + {name: "#3 ", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: []string{""}, data: "world", want: []string{""}, wantErr: false}, + {name: "#4 ", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: []string{"hello"}, data: "world", want: []string{"hello"}, wantErr: false}, + {name: "lookup", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: []string{"hello {{ lookup \"secrets.dev.key\" }}"}, data: "", want: []string{"hello 1234"}, wantErr: false, itemsCall: func(itemsSvc *mocksitems.Items) { + itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev").Return(&items.Item{ + ID: "dev", + SpaceID: "space", + EnvID: "env", + CollectionID: "secrets", + Data: map[string]interface{}{ + "id": "dev", + "key": "1234", + }, + }, nil).Once() + }}, + {name: "lookup with incorrect field", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: []string{"hello {{ lookup \"secrets.dev.incorrect\" }}"}, data: "", want: []string{"hello <no value>"}, wantErr: false, itemsCall: func(itemsSvc *mocksitems.Items) { + itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev").Return(&items.Item{ + ID: "dev", + SpaceID: "space", + EnvID: "env", + CollectionID: "secrets", + Data: map[string]interface{}{ + "id": "dev", + "key": "1234", + }, + }, nil).Once() + }}, + {name: "system ", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: []string{"hello {{ system.SpaceID }}"}, data: "", want: []string{"hello space"}, wantErr: false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + itemsSvc := &mocksitems.Items{} + if tt.itemsCall != nil { + tt.itemsCall(itemsSvc) + } + tt.cnt = &content.Content{ + Items: itemsSvc, + } + b := &Builder{ + ctx: tt.ctx, + cnt: tt.cnt, + SpaceID: tt.SpaceID, + EnvID: tt.EnvID, + funcs: tt.funcs, + } + + got, err := b.ExecuteList(tt.str, tt.data) + if tt.wantErr == true { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tt.want, got) + if tt.itemsCall != nil { + itemsSvc.AssertExpectations(t) + } + }) + } +} + +func TestBuilder_ExecuteMap(t *testing.T) { + tests := []struct { + name string + ctx context.Context + cnt *content.Content + SpaceID string + EnvID string + funcs template.FuncMap + str map[string]interface{} + data any + want map[string]interface{} + wantErr bool + + itemsCall func(itemsSvc *mocksitems.Items) + }{ + {name: "error", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: map[string]interface{}{"hello": "{{ . }"}, data: "world", want: nil, wantErr: true}, + {name: "empty", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: map[string]interface{}{}, data: "", want: map[string]interface{}{}, wantErr: false}, + {name: "#1", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: map[string]interface{}{"hello": "{{ . }}"}, data: "world", want: map[string]interface{}{"hello": "world"}, wantErr: false}, + {name: "#2", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: map[string]interface{}{"hello": "{{ . }}", "go": "{{ . }}"}, data: "world", want: map[string]interface{}{"hello": "world", "go": "world"}, wantErr: false}, + {name: "#3 ", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: map[string]interface{}{}, data: "world", want: map[string]interface{}{}, wantErr: false}, + {name: "#4 ", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: map[string]interface{}{"a": "b"}, data: "world", want: map[string]interface{}{"a": "b"}, wantErr: false}, + {name: "lookup ", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: map[string]interface{}{"hello": "{{ lookup \"secrets.dev.key\" }}"}, data: "", want: map[string]interface{}{"hello": "1234"}, wantErr: false, itemsCall: func(itemsSvc *mocksitems.Items) { + itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev").Return(&items.Item{ + ID: "dev", + SpaceID: "space", + EnvID: "env", + CollectionID: "secrets", + Data: map[string]interface{}{ + "id": "dev", + "key": "1234", + }, + }, nil).Once() + }}, + {name: "lookup with incorrect field", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: map[string]interface{}{"hello": "{{ lookup \"secrets.dev.incorrect\" }}"}, data: "", want: map[string]interface{}{"hello": "<no value>"}, wantErr: false, itemsCall: func(itemsSvc *mocksitems.Items) { + itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev").Return(&items.Item{ + ID: "dev", + SpaceID: "space", + EnvID: "env", + CollectionID: "secrets", + Data: map[string]interface{}{ + "id": "dev", + "key": "1234", + }, + }, nil).Once() + }}, + {name: "system ", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: map[string]interface{}{"hello": "{{ system.SpaceID }}"}, data: "", want: map[string]interface{}{"hello": "space"}, wantErr: false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + itemsSvc := &mocksitems.Items{} + if tt.itemsCall != nil { + tt.itemsCall(itemsSvc) + } + tt.cnt = &content.Content{ + Items: itemsSvc, + } + b := &Builder{ + ctx: tt.ctx, + cnt: tt.cnt, + SpaceID: tt.SpaceID, + EnvID: tt.EnvID, + funcs: tt.funcs, + } + + got, err := b.ExecuteMap(tt.str, tt.data) + if tt.wantErr == true { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tt.want, got) + if tt.itemsCall != nil { + itemsSvc.AssertExpectations(t) + } + }) + } +} diff --git a/pkg/template/funcs.go b/pkg/template/funcs.go new file mode 100644 index 0000000000000000000000000000000000000000..0c320ad139e964f002b691ce097b24e70e6cfaf3 --- /dev/null +++ b/pkg/template/funcs.go @@ -0,0 +1,43 @@ +package template + +import ( + "strings" + + "git.perx.ru/perxis/perxis-go/pkg/errors" +) + +// getLookup возвращает функцию для шаблонизатора для получения значений РёР· записи коллекции +// name указывается РІ РІРёРґРµ "<collection id>.<item id>.<field>" +// Рспользование РІ шаблонах: {{ lookup "secrets.key.value" }} +func getLookup(b *Builder) any { + return func(name string) (any, error) { + parsedName := strings.Split(name, ".") + if len(parsedName) < 3 { + return "", errors.Errorf("incorrect parameter \"%s\"", name) + } + + collectionID := parsedName[0] + itemID := parsedName[1] + field := parsedName[2] + item, err := b.cnt.Items.Get(b.Context(), b.SpaceID, b.EnvID, collectionID, itemID) + if err != nil { + return "", errors.Wrapf(err, "failed to get \"%s\"") + } + + if len(item.Data) > 0 { + if v, ok := item.Data[field]; ok { + return v, nil + } + } + + return nil, nil + } +} + +// getSys возвращает функцию получения System +// Рспользование РІ шаблонах: {{ system.SpaceID }} +func getSystem(b *Builder) any { + return func() *System { + return &System{builder: b} + } +} diff --git a/pkg/template/system.go b/pkg/template/system.go new file mode 100644 index 0000000000000000000000000000000000000000..8f8548eb11444e72ad4f003b9225f4d4a38e4e2a --- /dev/null +++ b/pkg/template/system.go @@ -0,0 +1,13 @@ +package template + +type System struct { + builder *Builder +} + +func (s *System) SpaceID() string { + return s.builder.SpaceID +} + +func (s *System) EnvID() string { + return s.builder.EnvID +} diff --git a/pkg/users/middleware/caching_middleware.go b/pkg/users/middleware/caching_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..6dc04868c75edcd77ba9bb2b3adda3f1ff3022f0 --- /dev/null +++ b/pkg/users/middleware/caching_middleware.go @@ -0,0 +1,91 @@ +package service + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/cache" + services "git.perx.ru/perxis/perxis-go/pkg/options" + service "git.perx.ru/perxis/perxis-go/pkg/users" +) + +func CachingMiddleware(cache *cache.Cache) Middleware { + return func(next service.Users) service.Users { + return &cachingMiddleware{ + cache: cache, + next: next, + } + } +} + +type cachingMiddleware struct { + cache *cache.Cache + next service.Users +} + +func (m cachingMiddleware) Create(ctx context.Context, create *service.User) (user *service.User, err error) { + return m.next.Create(ctx, create) +} + +func (m cachingMiddleware) Get(ctx context.Context, userId string) (user *service.User, err error) { + + value, e := m.cache.Get(userId) + if e == nil { + return value.(*service.User), err + } + user, err = m.next.Get(ctx, userId) + if err == nil { + m.cache.Set(user.ID, user) + for _, i := range user.Identities { + m.cache.Set(i, user) + } + } + return user, err +} + +func (m cachingMiddleware) Find(ctx context.Context, filter *service.Filter, options *services.FindOptions) (users []*service.User, total int, err error) { + return m.next.Find(ctx, filter, options) +} + +func (m cachingMiddleware) Update(ctx context.Context, update *service.User) (err error) { + + err = m.next.Update(ctx, update) + value, e := m.cache.Get(update.ID) + if err == nil && e == nil { + usr := value.(*service.User) + m.cache.Remove(usr.ID) + for _, i := range usr.Identities { + m.cache.Remove(i) + } + } + return err +} + +func (m cachingMiddleware) Delete(ctx context.Context, userId string) (err error) { + + err = m.next.Delete(ctx, userId) + value, e := m.cache.Get(userId) + if err == nil && e == nil { + usr := value.(*service.User) + m.cache.Remove(usr.ID) + for _, i := range usr.Identities { + m.cache.Remove(i) + } + } + return err +} + +func (m cachingMiddleware) GetByIdentity(ctx context.Context, identity string) (user *service.User, err error) { + + value, e := m.cache.Get(identity) + if e == nil { + return value.(*service.User), err + } + user, err = m.next.GetByIdentity(ctx, identity) + if err == nil { + m.cache.Set(user.ID, user) + for _, i := range user.Identities { + m.cache.Set(i, user) + } + } + return user, err +} diff --git a/pkg/users/middleware/caching_middleware_test.go b/pkg/users/middleware/caching_middleware_test.go new file mode 100644 index 0000000000000000000000000000000000000000..7ad09b52fdc003eac069005a8cdb956dd58949a7 --- /dev/null +++ b/pkg/users/middleware/caching_middleware_test.go @@ -0,0 +1,165 @@ +package service + +import ( + "context" + "testing" + "time" + + "git.perx.ru/perxis/perxis-go/pkg/cache" + "git.perx.ru/perxis/perxis-go/pkg/errors" + "git.perx.ru/perxis/perxis-go/pkg/users" + "git.perx.ru/perxis/perxis-go/pkg/users/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestUsersCache(t *testing.T) { + + const ( + userID = "user_id" + identity = "user identity" + size = 5 + ttl = 20 * time.Millisecond + ) + + errNotFound := errors.NotFound(errors.New("not found")) + + t.Run("Get from cache", func(t *testing.T) { + usrs := &mocks.Users{} + ctx := context.Background() + + svc := CachingMiddleware(cache.NewCache(size, ttl))(usrs) + + usrs.On("Get", mock.Anything, userID).Return(&users.User{ID: userID, Name: "User", Identities: []string{identity}}, nil).Once() + + v1, err := svc.Get(ctx, userID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, userID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кэша.") + + v3, err := svc.GetByIdentity(ctx, identity) + require.NoError(t, err) + assert.Same(t, v2, v3, "Ожидается получение объекта РёР· кэша РїСЂРё запросе РїРѕ Identity.") + + usrs.AssertExpectations(t) + }) + + t.Run("GetByIdentity from cache", func(t *testing.T) { + usrs := &mocks.Users{} + ctx := context.Background() + + svc := CachingMiddleware(cache.NewCache(size, ttl))(usrs) + + usrs.On("GetByIdentity", mock.Anything, identity).Return(&users.User{ID: userID, Name: "User", Identities: []string{identity}}, nil).Once() + + v1, err := svc.GetByIdentity(ctx, identity) + require.NoError(t, err) + + v2, err := svc.GetByIdentity(ctx, identity) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кэша.") + + v3, err := svc.Get(ctx, userID) + require.NoError(t, err) + assert.Same(t, v2, v3, "Ожидается получение объекта РёР· кэша РїСЂРё запросе РїРѕ userID.") + + usrs.AssertExpectations(t) + }) + + t.Run("Invalidate Cache", func(t *testing.T) { + t.Run("After Update", func(t *testing.T) { + usrs := &mocks.Users{} + ctx := context.Background() + + svc := CachingMiddleware(cache.NewCache(size, ttl))(usrs) + + usrs.On("Get", mock.Anything, userID).Return(&users.User{ID: userID, Name: "User", Identities: []string{identity}}, nil).Once() + usrs.On("Update", mock.Anything, mock.Anything).Return(nil).Once() + + v1, err := svc.Get(ctx, userID) + require.NoError(t, err) + + v2, err := svc.GetByIdentity(ctx, identity) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кэша.") + + err = svc.Update(ctx, &users.User{ID: userID, Name: "New User", Identities: []string{identity}}) + require.NoError(t, err) + + usrs.On("GetByIdentity", mock.Anything, identity).Return(&users.User{ID: userID, Name: "New User", Identities: []string{identity}}, nil).Once() + + v3, err := svc.GetByIdentity(ctx, identity) + require.NoError(t, err) + assert.NotSame(t, v3, v2, "Ожидается удаление объекта РёР· кеша после обновления Рё получение его заново РёР· сервиса.") + + v4, err := svc.Get(ctx, userID) + require.NoError(t, err) + assert.NotSame(t, v4, v2) + assert.Same(t, v4, v3, "Ожидается получение РЅРѕРІРѕРіРѕ обьекта РёР· кеша.") + + usrs.AssertExpectations(t) + }) + + t.Run("After Delete", func(t *testing.T) { + usrs := &mocks.Users{} + ctx := context.Background() + + svc := CachingMiddleware(cache.NewCache(size, ttl))(usrs) + + usrs.On("Get", mock.Anything, userID).Return(&users.User{ID: userID, Name: "User", Identities: []string{identity}}, nil).Once() + usrs.On("Delete", mock.Anything, mock.Anything).Return(nil).Once() + + v1, err := svc.Get(ctx, userID) + require.NoError(t, err) + + v2, err := svc.GetByIdentity(ctx, identity) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кэша.") + + err = svc.Delete(ctx, userID) + require.NoError(t, err) + + usrs.On("GetByIdentity", mock.Anything, identity).Return(nil, errNotFound).Once() + usrs.On("Get", mock.Anything, userID).Return(nil, errNotFound).Once() + + _, err = svc.GetByIdentity(ctx, identity) + require.Error(t, err) + assert.EqualErrorf(t, err, "not found", "Ожидается удаление объекта РёР· кеша после удаления РёР· хранилища Рё получение ошибки РѕС‚ сервиса.") + + _, err = svc.Get(ctx, userID) + require.Error(t, err) + assert.EqualErrorf(t, err, "not found", "Ожидается удаление объекта РёР· кеша после удаления РёР· хранилища Рё получение ошибки РѕС‚ сервиса.") + + usrs.AssertExpectations(t) + }) + + t.Run("After TTL expired", func(t *testing.T) { + usrs := &mocks.Users{} + ctx := context.Background() + + svc := CachingMiddleware(cache.NewCache(size, ttl))(usrs) + + usrs.On("Get", mock.Anything, userID).Return(&users.User{ID: userID, Name: "User", Identities: []string{identity}}, nil).Once() + + v1, err := svc.Get(ctx, userID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, userID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта РёР· кэша.") + + time.Sleep(2 * ttl) + + usrs.On("Get", mock.Anything, userID).Return(&users.User{ID: userID, Name: "User", Identities: []string{identity}}, nil).Once() + + v3, err := svc.Get(ctx, userID) + require.NoError(t, err) + assert.NotSame(t, v2, v3, "Ожидается получение объекта РёР· кэша РїСЂРё запросе РїРѕ Identity.") + + usrs.AssertExpectations(t) + }) + }) +} diff --git a/pkg/users/middleware/error_logging_middleware.go b/pkg/users/middleware/error_logging_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..a9084fa7a05608e45f7c5436934b5738c685a0e9 --- /dev/null +++ b/pkg/users/middleware/error_logging_middleware.go @@ -0,0 +1,91 @@ +package service + +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/error_log +// gowrap: http://github.com/hexdigest/gowrap + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/users -i Users -t ../../../assets/templates/middleware/error_log -o error_logging_middleware.go -l "" + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/options" + "git.perx.ru/perxis/perxis-go/pkg/users" + "go.uber.org/zap" +) + +// errorLoggingMiddleware implements users.Users that is instrumented with logging +type errorLoggingMiddleware struct { + logger *zap.Logger + next users.Users +} + +// ErrorLoggingMiddleware instruments an implementation of the users.Users with simple logging +func ErrorLoggingMiddleware(logger *zap.Logger) Middleware { + return func(next users.Users) users.Users { + return &errorLoggingMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *errorLoggingMiddleware) Create(ctx context.Context, create *users.User) (user *users.User, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Create(ctx, create) +} + +func (m *errorLoggingMiddleware) Delete(ctx context.Context, userId string) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Delete(ctx, userId) +} + +func (m *errorLoggingMiddleware) Find(ctx context.Context, filter *users.Filter, options *options.FindOptions) (users []*users.User, 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, options) +} + +func (m *errorLoggingMiddleware) Get(ctx context.Context, userId string) (user *users.User, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Get(ctx, userId) +} + +func (m *errorLoggingMiddleware) GetByIdentity(ctx context.Context, identity string) (user *users.User, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.GetByIdentity(ctx, identity) +} + +func (m *errorLoggingMiddleware) Update(ctx context.Context, update *users.User) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Update(ctx, update) +} diff --git a/pkg/users/middleware/logging_middleware.go b/pkg/users/middleware/logging_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..1fcae0626b75992ae563ff87adfd4e33edd49af6 --- /dev/null +++ b/pkg/users/middleware/logging_middleware.go @@ -0,0 +1,251 @@ +package service + +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/access_log +// gowrap: http://github.com/hexdigest/gowrap + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/users -i Users -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/options" + "git.perx.ru/perxis/perxis-go/pkg/users" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// loggingMiddleware implements users.Users that is instrumented with logging +type loggingMiddleware struct { + logger *zap.Logger + next users.Users +} + +// LoggingMiddleware instruments an implementation of the users.Users with simple logging +func LoggingMiddleware(logger *zap.Logger) Middleware { + return func(next users.Users) users.Users { + return &loggingMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *loggingMiddleware) Create(ctx context.Context, create *users.User) (user *users.User, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "create": create} { + 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...) + + user, err = m.next.Create(ctx, create) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "user": user, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Create.Response", fields...) + + return user, err +} + +func (m *loggingMiddleware) Delete(ctx context.Context, userId string) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "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("Delete.Request", fields...) + + err = m.next.Delete(ctx, 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("Delete.Response", fields...) + + return err +} + +func (m *loggingMiddleware) Find(ctx context.Context, filter *users.Filter, options *options.FindOptions) (users []*users.User, total int, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "filter": filter, + "options": options} { + 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...) + + users, total, err = m.next.Find(ctx, filter, options) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "users": users, + "total": total, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Find.Response", fields...) + + return users, total, err +} + +func (m *loggingMiddleware) Get(ctx context.Context, userId string) (user *users.User, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "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("Get.Request", fields...) + + user, err = m.next.Get(ctx, userId) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "user": user, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Get.Response", fields...) + + return user, err +} + +func (m *loggingMiddleware) GetByIdentity(ctx context.Context, identity string) (user *users.User, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "identity": identity} { + 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("GetByIdentity.Request", fields...) + + user, err = m.next.GetByIdentity(ctx, identity) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "user": user, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("GetByIdentity.Response", fields...) + + return user, err +} + +func (m *loggingMiddleware) Update(ctx context.Context, update *users.User) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "update": update} { + 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, update) + + 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/users/middleware/middleware.go b/pkg/users/middleware/middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..2888f263ca2083bdea91e3c3e62cde40f85e974f --- /dev/null +++ b/pkg/users/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/users -i Users -t ../../../assets/templates/middleware/middleware -o middleware.go -l "" + +import ( + "git.perx.ru/perxis/perxis-go/pkg/users" + "go.uber.org/zap" +) + +type Middleware func(users.Users) users.Users + +func WithLog(s users.Users, logger *zap.Logger, log_access bool) users.Users { + if logger == nil { + logger = zap.NewNop() + } + + logger = logger.Named("Users") + s = ErrorLoggingMiddleware(logger)(s) + if log_access { + s = LoggingMiddleware(logger)(s) + } + s = RecoveringMiddleware(logger)(s) + return s +} diff --git a/pkg/users/middleware/recovering_middleware.go b/pkg/users/middleware/recovering_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..57c401b12c0b56e9f40109570c6efe59bac8d902 --- /dev/null +++ b/pkg/users/middleware/recovering_middleware.go @@ -0,0 +1,104 @@ +// 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/users -i Users -t ../../../assets/templates/middleware/recovery -o recovering_middleware.go -l "" + +import ( + "context" + "fmt" + + "git.perx.ru/perxis/perxis-go/pkg/options" + "git.perx.ru/perxis/perxis-go/pkg/users" + "go.uber.org/zap" +) + +// recoveringMiddleware implements users.Users that is instrumented with logging +type recoveringMiddleware struct { + logger *zap.Logger + next users.Users +} + +// RecoveringMiddleware instruments an implementation of the users.Users with simple logging +func RecoveringMiddleware(logger *zap.Logger) Middleware { + return func(next users.Users) users.Users { + return &recoveringMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *recoveringMiddleware) Create(ctx context.Context, create *users.User) (user *users.User, 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, create) +} + +func (m *recoveringMiddleware) Delete(ctx context.Context, 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.Delete(ctx, userId) +} + +func (m *recoveringMiddleware) Find(ctx context.Context, filter *users.Filter, options *options.FindOptions) (users []*users.User, 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, options) +} + +func (m *recoveringMiddleware) Get(ctx context.Context, userId string) (user *users.User, 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, userId) +} + +func (m *recoveringMiddleware) GetByIdentity(ctx context.Context, identity string) (user *users.User, 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.GetByIdentity(ctx, identity) +} + +func (m *recoveringMiddleware) Update(ctx context.Context, update *users.User) (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, update) +}