diff --git a/log/zap/channel_core.go b/log/zap/channel_core.go
new file mode 100644
index 0000000000000000000000000000000000000000..4eda68780e6460cb20201f07dfde4e2f222d9d2e
--- /dev/null
+++ b/log/zap/channel_core.go
@@ -0,0 +1,43 @@
+package zap
+
+import (
+	"strings"
+
+	"go.uber.org/zap/zapcore"
+)
+
+const channelKey = "channel"
+
+func HasChannel(channel string) FilterFunc {
+	return func(entry zapcore.Entry, fields []zapcore.Field) bool {
+		for _, f := range fields {
+			if f.Key == channelKey && f.Type == zapcore.StringType {
+				list := strings.Split(f.String, ",")
+				for _, v := range list {
+					if v == channel {
+						return true
+					}
+				}
+			}
+		}
+		return false
+	}
+}
+
+// NewDefaultChannelCore аналогичен NewChannelCore, но также устанавливает переданный канал в качестве канала по умолчанию.
+// Это означает, что если поле Channel в записи не указано, запись все равно будет передана в zapcore.Core.
+func NewDefaultChannelCore(core zapcore.Core, channel string) zapcore.Core {
+	return RegisterFilters(
+		core,
+		Or(
+			HasChannel(channel),
+			Not(HasKey(channelKey)),
+		),
+	)
+}
+
+// NewChannelCore добавляет к переданному Core фильтрацию записей по каналам.
+// Это означает, что если запись содержит поле Channel и значение соответствует переданному каналу, то запись будет передана в zapcore.Core.
+func NewChannelCore(core zapcore.Core, channel string) zapcore.Core {
+	return RegisterFilters(core, HasChannel(channel))
+}
diff --git a/log/zap/channel_core_test.go b/log/zap/channel_core_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..51fa290785a258dcb9c5faa372afa5ec1c228072
--- /dev/null
+++ b/log/zap/channel_core_test.go
@@ -0,0 +1,53 @@
+package zap
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	"go.uber.org/zap/zapcore"
+	"go.uber.org/zap/zaptest/observer"
+)
+
+func TestNewChannelCore_WriteSingleChannel(t *testing.T) {
+	core, logs := observer.New(zapcore.InfoLevel)
+	core = NewChannelCore(core, "test")
+
+	err := core.Write(zapcore.Entry{Message: "msg"}, []zapcore.Field{Channel("test")})
+	require.NoError(t, err)
+
+	err = core.Write(zapcore.Entry{Message: "msg"}, []zapcore.Field{Channel("empty")}) // запись не попадет в лог
+	require.NoError(t, err)
+
+	require.Equal(t, 1, logs.Len())
+}
+
+func TestNewChannelCore_WriteMultiplyChannels(t *testing.T) {
+	core, logs := observer.New(zapcore.InfoLevel)
+
+	core = zapcore.NewTee(
+		NewChannelCore(core, "test1"),
+		NewChannelCore(core, "test2"),
+	)
+
+	err := core.Write(zapcore.Entry{Message: "msg"}, []zapcore.Field{Channel("test1", "test2")}) // запись попадет сразу в 2 core
+	require.NoError(t, err)
+
+	require.Equal(t, 2, logs.Len())
+}
+
+func TestNewDefaultChannelCore(t *testing.T) {
+	core, logs := observer.New(zapcore.InfoLevel)
+
+	core = NewDefaultChannelCore(core, "test1")
+
+	err := core.Write(zapcore.Entry{Message: "msg"}, []zapcore.Field{Channel("test1")})
+	require.NoError(t, err)
+
+	err = core.Write(zapcore.Entry{Message: "msg"}, []zapcore.Field{Channel("test2")}) // эта запись не попадет в лог
+	require.NoError(t, err)
+
+	err = core.Write(zapcore.Entry{Message: "msg"}, []zapcore.Field{})
+	require.NoError(t, err)
+
+	require.Equal(t, 2, logs.Len())
+}
diff --git a/log/zap/field.go b/log/zap/field.go
index acc6932d5fab8b2975d09998557e6ed41197010d..b439f51b05736b77ff0a991e9b828a2116cfa0e3 100644
--- a/log/zap/field.go
+++ b/log/zap/field.go
@@ -2,6 +2,7 @@ package zap
 
 import (
 	"context"
+	"strings"
 
 	"git.perx.ru/perxis/perxis-go/id"
 	_ "git.perx.ru/perxis/perxis-go/id/system" // регистрируем обработчики для системных объектов
@@ -9,6 +10,13 @@ import (
 	"go.uber.org/zap"
 )
 
+func Channel(channels ...string) zap.Field {
+	if len(channels) == 0 {
+		return zap.Skip()
+	}
+	return zap.String(channelKey, strings.Join(channels, ","))
+}
+
 func Category(category string) zap.Field {
 	if category == "" {
 		return zap.Skip()
diff --git a/log/zap/filter_core.go b/log/zap/filter_core.go
index 1c6099e6baebacfed5d6bc21e844673d42d42534..f60ba37d7c95b153858e8f2f2c95a3ee68e5f829 100644
--- a/log/zap/filter_core.go
+++ b/log/zap/filter_core.go
@@ -24,6 +24,34 @@ func NotHasField(field zapcore.Field) FilterFunc {
 	}
 }
 
+func HasKey(key string) FilterFunc {
+	return func(entry zapcore.Entry, fields []zapcore.Field) bool {
+		for _, f := range fields {
+			if f.Key == key {
+				return true
+			}
+		}
+		return false
+	}
+}
+
+func Or(filters ...FilterFunc) FilterFunc {
+	return func(entry zapcore.Entry, fields []zapcore.Field) bool {
+		for _, f := range filters {
+			if f(entry, fields) {
+				return true
+			}
+		}
+		return false
+	}
+}
+
+func Not(filter FilterFunc) FilterFunc {
+	return func(entry zapcore.Entry, fields []zapcore.Field) bool {
+		return !filter(entry, fields)
+	}
+}
+
 type filterCore struct {
 	zapcore.Core