diff --git a/log/zap/field.go b/log/zap/field.go
new file mode 100644
index 0000000000000000000000000000000000000000..3b018c114c80200d3e413da0809e9077a5f4f676
--- /dev/null
+++ b/log/zap/field.go
@@ -0,0 +1,73 @@
+package zap
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/id"
+	_ "git.perx.ru/perxis/perxis-go/id/system" // регистрируем обработчики для системных объектов
+	"git.perx.ru/perxis/perxis-go/pkg/auth"
+	"go.uber.org/zap"
+)
+
+func Category(category string) zap.Field {
+	if category == "" {
+		return zap.Skip()
+	}
+	return zap.String("category", category)
+}
+
+func Component(component string) zap.Field {
+	if component == "" {
+		return zap.Skip()
+	}
+	return zap.String("component", component)
+}
+
+func Event(event string) zap.Field {
+	if event == "" {
+		return zap.Skip()
+	}
+	return zap.String("event", event)
+}
+
+// ObjectID возвращает поле и устанавливает передаваемый аргумент в качестве идентификатора объекта в формате ObjectId.
+// Поддерживает типы в формате ObjectId: string, map[string]any, системные объекты.
+func ObjectID(v any) zap.Field {
+	oid, err := id.NewObjectId(v)
+	if err != nil {
+		return zap.Skip()
+	}
+	return zap.Reflect("object_id", oid)
+}
+
+// CallerID возвращает поле и устанавливает передаваемый аргумент в качестве "вызывающего" в формате ObjectId.
+// Поддерживает типы в формате ObjectId: string, map[string]any, системные объекты.
+func CallerID(v any) zap.Field {
+	oid, err := id.NewObjectId(v)
+	if err != nil {
+		return zap.Skip()
+	}
+	return zap.Reflect("caller_id", oid)
+}
+
+// CallerIDFromContext извлекает auth.Principal из контекста и устанавливает его в качестве "вызывающего" в формате ObjectID.
+func CallerIDFromContext(ctx context.Context) zap.Field {
+	if ctx == nil {
+		return zap.Skip()
+	}
+	return CallerID(auth.GetPrincipal(ctx))
+}
+
+func Attr(attr any) zap.Field {
+	if attr == nil {
+		return zap.Skip()
+	}
+	return zap.Any("attr", attr)
+}
+
+func Tags(tags ...string) zap.Field {
+	if len(tags) == 0 {
+		return zap.Skip()
+	}
+	return zap.Strings("tags", tags)
+}
diff --git a/log/zap/field_test.go b/log/zap/field_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..40d6822b4dac8b0ab59371a6381f3f58ae5d439c
--- /dev/null
+++ b/log/zap/field_test.go
@@ -0,0 +1,185 @@
+package zap
+
+import (
+	"context"
+	"testing"
+
+	"git.perx.ru/perxis/perxis-go/id"
+	"git.perx.ru/perxis/perxis-go/pkg/auth"
+	"git.perx.ru/perxis/perxis-go/pkg/items"
+	"git.perx.ru/perxis/perxis-go/pkg/users"
+	"github.com/stretchr/testify/assert"
+	"go.uber.org/zap"
+)
+
+func TestCategory(t *testing.T) {
+	tests := []struct {
+		name  string
+		field zap.Field
+		want  zap.Field
+	}{
+		{name: "ok", field: Category("update"), want: zap.String("category", "update")},
+		{name: "invalid", field: Category(""), want: zap.Skip()},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			assert.True(t, tc.want.Equals(tc.field))
+		})
+	}
+}
+
+func TestComponent(t *testing.T) {
+	tests := []struct {
+		name  string
+		field zap.Field
+		want  zap.Field
+	}{
+		{name: "ok", field: Component("Items"), want: zap.String("component", "Items")},
+		{name: "invalid", field: Component(""), want: zap.Skip()},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			assert.True(t, tc.want.Equals(tc.field))
+		})
+	}
+}
+
+func TestEvent(t *testing.T) {
+	tests := []struct {
+		name  string
+		field zap.Field
+		want  zap.Field
+	}{
+		{name: "ok", field: Event("items.create"), want: zap.String("event", "items.create")},
+		{name: "invalid", field: Event(""), want: zap.Skip()},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			assert.True(t, tc.want.Equals(tc.field))
+		})
+	}
+}
+
+func TestObjectID(t *testing.T) {
+	item := &items.Item{
+		ID:           "c4ca4238a0b923820dcc509a6f75849b",
+		SpaceID:      "c81e728d9d4c2f636f067f89cc14862c",
+		EnvID:        "eccbc87e4b5ce2fe28308fd9f2a7baf3",
+		CollectionID: "a87ff679a2f3e71d9181a67b7542122c",
+	}
+
+	oid := id.MustObjectId(item)
+	itemId := id.NewItemId(*item)
+
+	tests := []struct {
+		name  string
+		field zap.Field
+		want  zap.Field
+	}{
+		{name: "system object", field: ObjectID(item), want: zap.Reflect("object_id", oid)},
+		{name: "object id", field: ObjectID(itemId), want: zap.Reflect("object_id", oid)},
+		{name: "string", field: ObjectID(oid.String()), want: zap.Reflect("object_id", oid)},
+		{name: "invalid", field: ObjectID(nil), want: zap.Skip()},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			if tc.want.Equals(zap.Skip()) {
+				assert.True(t, tc.want.Equals(tc.field))
+				return
+			}
+			assert.Equal(t, tc.want.Interface.(id.Descriptor).String(), tc.field.Interface.(id.Descriptor).String())
+		})
+	}
+}
+
+func TestCallerID(t *testing.T) {
+	user := &users.User{
+		ID: "c4ca4238a0b923820dcc509a6f75849b",
+	}
+
+	oid := id.MustObjectId(user)
+	userId := id.NewUserId(*user)
+
+	tests := []struct {
+		name  string
+		field zap.Field
+		want  zap.Field
+	}{
+		{name: "system object", field: CallerID(user), want: zap.Reflect("caller_id", oid)},
+		{name: "object id", field: CallerID(userId), want: zap.Reflect("caller_id", oid)},
+		{name: "string", field: CallerID(oid.String()), want: zap.Reflect("caller_id", oid)},
+		{name: "invalid", field: CallerID(nil), want: zap.Skip()},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			if tc.want.Equals(zap.Skip()) {
+				assert.True(t, tc.want.Equals(tc.field))
+				return
+			}
+			assert.Equal(t, tc.want.Interface.(id.Descriptor).String(), tc.field.Interface.(id.Descriptor).String())
+		})
+	}
+}
+
+func TestCallerIDFromContext(t *testing.T) {
+	ctx := auth.WithSystem(context.Background())
+	oid := id.MustObjectId(auth.GetPrincipal(ctx))
+
+	tests := []struct {
+		name  string
+		field zap.Field
+		want  zap.Field
+	}{
+		{name: "ok", field: CallerIDFromContext(ctx), want: zap.Reflect("caller_id", oid)},
+		{name: "invalid", field: CallerIDFromContext(nil), want: zap.Skip()},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			if tc.want.Equals(zap.Skip()) {
+				assert.True(t, tc.want.Equals(tc.field))
+				return
+			}
+			assert.Equal(t, tc.want.Interface.(id.Descriptor).String(), tc.field.Interface.(id.Descriptor).String())
+		})
+	}
+}
+
+func TestAttr(t *testing.T) {
+	tests := []struct {
+		name  string
+		field zap.Field
+		want  zap.Field
+	}{
+		{name: "ok", field: Attr(map[string]string{"a": "b"}), want: zap.Reflect("attr", map[string]string{"a": "b"})},
+		{name: "invalid", field: Attr(nil), want: zap.Skip()},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			assert.True(t, tc.want.Equals(tc.field))
+		})
+	}
+}
+
+func TestTags(t *testing.T) {
+	tests := []struct {
+		name  string
+		field zap.Field
+		want  zap.Field
+	}{
+		{name: "ok", field: Tags("a", "b", "c"), want: zap.Strings("tags", []string{"a", "b", "c"})},
+		{name: "invalid", field: Tags(nil...), want: zap.Skip()},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			assert.True(t, tc.want.Equals(tc.field))
+		})
+	}
+}
diff --git a/pkg/log/zap/field.go b/pkg/log/zap/field.go
deleted file mode 100644
index e0de88be61059c325821ff68afe0bcbc7b88b6ba..0000000000000000000000000000000000000000
--- a/pkg/log/zap/field.go
+++ /dev/null
@@ -1,79 +0,0 @@
-package zap
-
-import (
-	"context"
-	"fmt"
-
-	"git.perx.ru/perxis/perxis-go/id"
-	"git.perx.ru/perxis/perxis-go/pkg/auth"
-	"go.uber.org/zap"
-	"go.uber.org/zap/zapcore"
-)
-
-const (
-	unknownObject = "unknown"
-	unknownCaller = "unknown"
-)
-
-func Category(category string) zapcore.Field {
-	return zap.String("category", category)
-}
-
-func Component(component string) zapcore.Field {
-	return zap.String("component", component)
-}
-
-func Event(event string) zapcore.Field {
-	return zap.String("event", event)
-}
-
-// Object возвращает поле и устанавливает передаваемый аргумент в качестве идентификатора объекта в формате ObjectID.
-// Поддерживаемые типы: string, fmt.Stringer.
-// Если передан аргумент другого типа, будет произведена попытка привести переданное значение к ObjectID.
-func Object(v any) zapcore.Field {
-	var object = unknownObject
-	switch value := v.(type) {
-	case string:
-		object = value
-	case fmt.Stringer:
-		object = value.String()
-	default:
-		oid, err := id.FromObject(v)
-		if err == nil {
-			object = oid.String()
-		}
-	}
-	return zap.String("object", object)
-}
-
-// Caller возвращает поле и устанавливает передаваемый аргумент в качестве "вызывающего" в формате ObjectID.
-// Поддерживаемые типы: string, fmt.Stringer.
-// Если передан аргумент другого типа, будет произведена попытка привести переданное значение к ObjectID.
-func Caller(v any) zapcore.Field {
-	var caller = unknownCaller
-	switch value := v.(type) {
-	case string:
-		caller = value
-	case fmt.Stringer:
-		caller = value.String()
-	default:
-		oid, err := id.FromObject(v)
-		if err == nil {
-			caller = oid.String()
-		}
-	}
-	return zap.String("caller", caller)
-}
-
-// CallerFromContext извлекает auth.Principal из контекста и устанавливает его в качестве "вызывающего" в формате ObjectID.
-func CallerFromContext(ctx context.Context) zapcore.Field {
-	return Caller(auth.GetPrincipal(ctx))
-}
-
-func Attr(attr any) zapcore.Field {
-	return zap.Any("attr", attr)
-}
-
-func Tags(tags ...string) zapcore.Field {
-	return zap.Strings("tags", tags)
-}