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) -}