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