Skip to content
Snippets Groups Projects
Commit e2a1c17d authored by Pavel Antonov's avatar Pavel Antonov :asterisk:
Browse files

Merge branch 'feature/PRXS-951-1961-ZapIntergration' into 'feature/PRXS-951-Log'

Добавлена интеграция zap логгера с сервисом логирования

See merge request perxis/perxis-go!148
parents cdcfee8d d77d6bc1
Branches
Tags
No related merge requests found
package zap
import (
"context"
"sync"
"time"
"git.perx.ru/perxis/perxis-go/log"
"git.perx.ru/perxis/perxis-go/pkg/errors"
)
const (
defaultMaxBufferSize = 1000
defaultMaxSyncQueueSize = 16
defaultFlushInterval = 5 * time.Second
)
var SyncQueueOverflow = errors.New("sync queue overflow")
// BufferedWriteSyncer это WriteSyncer, который отправляет записи в log.Service.
// Когда количество буферизированных записей достигает некоторого предела или проходит определенный фиксированный интервал,
// записи отправляются в очередь для синхронизации с log.Service.
type BufferedWriteSyncer struct {
// FlushInterval устанавливает интервал, через который буферизированные записи будут отправлены на синхронизацию.
//
// Значение по умолчанию для этого параметра равно 5 секунд.
FlushInterval time.Duration
// MaxBufferSize устанавливает максимальное количество записей, которые могут быть буферизованы.
// Когда количество буферизованных записей превысит этот порог, они будут отправлены на синхронизацию в log.Service.
//
// Значение по умолчанию для этого параметра равно 1000.
MaxBufferSize int
// MaxSyncQueueSize устанавливает максимальный размер очереди записей на синхронизацию с log.Service.
//
// Значение по умолчанию для этого параметра равно 16.
MaxSyncQueueSize int
// Service сервис для хранения записей
Service log.Service
wg sync.WaitGroup
mu sync.RWMutex
buffer []*log.Entry
syncQueue chan []*log.Entry
flushStop chan struct{} // flushStop закрывается, когда flushLoop должен быть остановлен
started bool // started указывает, был ли выполнен Start
stopped bool // stopped указывает, был ли выполнен Stop
}
func (ws *BufferedWriteSyncer) start() {
if ws.Service == nil {
panic("service is required")
}
if ws.FlushInterval == 0 {
ws.FlushInterval = defaultFlushInterval
}
if ws.MaxBufferSize == 0 {
ws.MaxBufferSize = defaultMaxBufferSize
}
if ws.MaxSyncQueueSize == 0 {
ws.MaxSyncQueueSize = defaultMaxSyncQueueSize
}
ws.buffer = make([]*log.Entry, 0, ws.MaxBufferSize)
ws.syncQueue = make(chan []*log.Entry, ws.MaxSyncQueueSize)
ws.flushStop = make(chan struct{})
ws.wg.Add(2)
go ws.syncLoop()
go ws.flushLoop()
ws.started = true
}
func (ws *BufferedWriteSyncer) Stop() error {
ws.mu.Lock()
defer ws.mu.Unlock()
if !ws.started || ws.stopped {
return nil
}
ws.stopped = true
close(ws.flushStop) // завершаем flushLoop
err := ws.flush() // очищаем оставшиеся записи
close(ws.syncQueue) // завершаем syncLoop
ws.wg.Wait() // дожидаемся завершения flushLoop и syncLoop
return err
}
// Write отправляет запись в буфер.
// Когда количество буферизованных записей превышает максимальный размер буфера, буферизированные записи будут отправлены на синхронизацию.
func (ws *BufferedWriteSyncer) Write(entry *log.Entry) error {
ws.mu.Lock()
defer ws.mu.Unlock()
if !ws.started {
ws.start()
}
// Проверяем, не достигли ли мы предела размера буфера. Если это так, тогда освобождаем его.
if len(ws.buffer)+1 > ws.MaxBufferSize {
err := ws.flush()
if err != nil {
return err
}
}
ws.buffer = append(ws.buffer, entry)
return nil
}
// Sync освобождает буфер и отправляет буферизированные записи на синхронизацию.
func (ws *BufferedWriteSyncer) Sync() error {
ws.mu.Lock()
defer ws.mu.Unlock()
if ws.started {
return ws.flush()
}
return nil
}
// flush освобождает буфер и отправляет буферизированные записи на синхронизацию.
// Если очередь на синхронизацию переполнена, будет возвращена ошибка SyncQueueOverflow
//
// ВНИМАНИЕ: Не является безопасным для конкурентного вызова.
func (ws *BufferedWriteSyncer) flush() error {
if len(ws.buffer) == 0 {
return nil
}
// Проверяем, не достигли ли мы предела размера очереди. Если это так, возвращаем ошибку.
if len(ws.syncQueue)+1 > ws.MaxSyncQueueSize {
return SyncQueueOverflow
}
ws.syncQueue <- ws.buffer
ws.buffer = make([]*log.Entry, 0, ws.MaxBufferSize)
return nil
}
// flushLoop периодически отправляет буферизированные записи на синхронизацию.
func (ws *BufferedWriteSyncer) flushLoop() {
ticker := time.NewTicker(ws.FlushInterval)
defer func() {
ticker.Stop()
ws.wg.Done()
}()
for {
select {
case <-ticker.C:
_ = ws.Sync()
case <-ws.flushStop:
return
}
}
}
// syncLoop синхронизирует записи с log.Service.
func (ws *BufferedWriteSyncer) syncLoop() {
defer ws.wg.Done()
for entries := range ws.syncQueue {
_ = ws.Service.Log(context.Background(), entries)
}
}
package zap
import (
"sync"
"testing"
"time"
"git.perx.ru/perxis/perxis-go/log"
logmocks "git.perx.ru/perxis/perxis-go/log/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
func TestBufferedWriteSyncer_Write(t *testing.T) {
service := &logmocks.Service{}
service.On("Log", mock.Anything, mock.Anything).
Return(nil).
Run(func(args mock.Arguments) {
entries := args.Get(1).([]*log.Entry)
require.Equal(t, 2, len(entries))
}).
Once()
ws := &BufferedWriteSyncer{Service: service}
err := ws.Write(&log.Entry{Message: "first log message"})
require.NoError(t, err)
err = ws.Write(&log.Entry{Message: "second log message"})
require.NoError(t, err)
err = ws.Stop()
require.NoError(t, err)
service.AssertExpectations(t)
}
func TestBufferedWriteSyncer_Write_Concurrent(t *testing.T) {
service := &logmocks.Service{}
service.On("Log", mock.Anything, mock.Anything).
Return(nil).
Run(func(args mock.Arguments) {
entries := args.Get(1).([]*log.Entry)
require.Equal(t, 100, len(entries))
}).
Once()
ws := &BufferedWriteSyncer{Service: service}
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(wg *sync.WaitGroup) {
defer wg.Done()
err := ws.Write(&log.Entry{Message: "log message"})
require.NoError(t, err)
}(&wg)
}
wg.Wait()
err := ws.Stop()
require.NoError(t, err)
service.AssertExpectations(t)
}
func TestBufferedWriteSyncer_Flush(t *testing.T) {
service := &logmocks.Service{}
service.On("Log", mock.Anything, mock.Anything).
Return(nil).
Run(func(args mock.Arguments) {
entries := args.Get(1).([]*log.Entry)
require.Equal(t, 10, len(entries))
}).
Times(10)
ws := &BufferedWriteSyncer{Service: service}
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
err := ws.Write(&log.Entry{Message: "log message"})
require.NoError(t, err)
}
err := ws.Sync()
require.NoError(t, err)
}
err := ws.Stop()
require.NoError(t, err)
service.AssertExpectations(t)
}
func TestBufferedWriteSyncer_MaxBufferSize(t *testing.T) {
service := &logmocks.Service{}
service.On("Log", mock.Anything, mock.Anything).
Return(nil).
Run(func(args mock.Arguments) {
entries := args.Get(1).([]*log.Entry)
assert.Equal(t, 10, len(entries))
}).
Times(10)
ws := &BufferedWriteSyncer{Service: service, MaxBufferSize: 10}
for i := 0; i < 100; i++ {
err := ws.Write(&log.Entry{Message: "log message"})
require.NoError(t, err)
}
err := ws.Stop()
require.NoError(t, err)
service.AssertExpectations(t)
}
func TestBufferedWriteSyncer_FlushInterval(t *testing.T) {
service := &logmocks.Service{}
service.On("Log", mock.Anything, mock.Anything).
Return(nil).
Run(func(args mock.Arguments) {
entries := args.Get(1).([]*log.Entry)
assert.Equal(t, 10, len(entries))
}).
Once()
ws := &BufferedWriteSyncer{Service: service, FlushInterval: time.Second}
for j := 0; j < 10; j++ {
err := ws.Write(&log.Entry{Message: "log message"})
require.NoError(t, err)
}
time.Sleep(3 * time.Second) // ждем, пока сработает интервал
err := ws.Stop()
require.NoError(t, err)
service.AssertExpectations(t)
}
package zap
import (
oid "git.perx.ru/perxis/perxis-go/id"
"git.perx.ru/perxis/perxis-go/log"
"git.perx.ru/perxis/perxis-go/pkg/id"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// WriteSyncer отвечает за хранение и синхронизацию log.Entry
type WriteSyncer interface {
Write(entry *log.Entry) error
Sync() error
}
// Core кодирует zapcore.Entry в log.Entry и отправляет их в WriteSyncer
type Core struct {
zapcore.LevelEnabler
writeSyncer WriteSyncer
fields []zap.Field
}
func NewCore(writeSyncer WriteSyncer) *Core {
return &Core{
LevelEnabler: zapcore.InfoLevel,
writeSyncer: writeSyncer,
}
}
func (core *Core) With(fields []zapcore.Field) zapcore.Core {
return &Core{
LevelEnabler: core.LevelEnabler,
writeSyncer: core.writeSyncer,
fields: append(core.fields, fields...),
}
}
func (core *Core) Check(entry zapcore.Entry, checkedEntry *zapcore.CheckedEntry) *zapcore.CheckedEntry {
if core.Enabled(entry.Level) {
return checkedEntry.AddCore(entry, core)
}
return checkedEntry
}
func (core *Core) Write(entry zapcore.Entry, fields []zapcore.Field) error {
return core.writeSyncer.Write(core.getEntry(entry, fields))
}
func (core *Core) Sync() error {
return core.writeSyncer.Sync()
}
func (core *Core) getEntry(entry zapcore.Entry, fields []zapcore.Field) *log.Entry {
if len(core.fields) > 0 {
fields = append(fields, core.fields...)
}
enc := zapcore.NewMapObjectEncoder()
for _, field := range fields {
field.AddTo(enc)
}
ent := &log.Entry{
ID: id.GenerateNewID(),
Timestamp: entry.Time,
LogLevel: log.Level(entry.Level),
Message: entry.Message,
}
ent.Category, _ = enc.Fields["category"].(string)
ent.Component, _ = enc.Fields["component"].(string)
ent.Event, _ = enc.Fields["event"].(string)
ent.ObjectID, _ = enc.Fields["object"].(*oid.ObjectId)
ent.CallerID, _ = enc.Fields["caller"].(*oid.ObjectId)
ent.Attr = enc.Fields["attr"]
if tags, ok := enc.Fields["tags"].([]any); ok {
for _, item := range tags {
if tag, ok := item.(string); ok {
ent.Tags = append(ent.Tags, tag)
}
}
}
return ent
}
package zap
import (
"testing"
"git.perx.ru/perxis/perxis-go/id"
"git.perx.ru/perxis/perxis-go/log"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func TestCore_getEntry(t *testing.T) {
core := NewCore(nil)
tests := []struct {
name string
input struct {
entry zapcore.Entry
fields []zapcore.Field
}
want *log.Entry
}{
{
name: "simple",
input: struct {
entry zapcore.Entry
fields []zapcore.Field
}{
entry: zapcore.Entry{Level: zapcore.InfoLevel, Message: "создан элемент коллекции"},
fields: []zapcore.Field{
zap.String("key", "val"), // будет проигнорировано
Category("create"),
Component("Items.Service"),
Event("Items.Create"),
Object("/spaces/WPNN/envs/9VGP/cols/GxNv/items/W0fl"),
Caller("/users/PHVz"),
Attr("any"),
Tags("tag1", "tag2", "tag3"),
},
},
want: &log.Entry{
LogLevel: log.Level(zapcore.InfoLevel),
Message: "создан элемент коллекции",
Category: "create",
Component: "Items.Service",
Event: "Items.Create",
ObjectID: id.MustObjectId("/spaces/WPNN/envs/9VGP/cols/GxNv/items/W0fl"),
CallerID: id.MustObjectId("/users/PHVz"),
Attr: "any",
Tags: []string{"tag1", "tag2", "tag3"},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := core.getEntry(tc.input.entry, tc.input.fields)
got.ID = tc.want.ID // игнорируем ID
got.Timestamp = tc.want.Timestamp // игнорируем Timestamp
require.Equal(t, tc.want, got)
})
}
}
package zap
import (
"context"
"reflect"
"slices"
"testing"
"git.perx.ru/perxis/perxis-go/id"
"git.perx.ru/perxis/perxis-go/log"
logmocks "git.perx.ru/perxis/perxis-go/log/mocks"
"git.perx.ru/perxis/perxis-go/pkg/auth"
"git.perx.ru/perxis/perxis-go/pkg/items"
"git.perx.ru/perxis/perxis-go/pkg/users"
usersmocks "git.perx.ru/perxis/perxis-go/pkg/users/mocks"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func TestExample(t *testing.T) {
item := items.NewItem("WPNN", "9VGP", "GxNv", "W0fl", nil, nil)
user := &users.User{ID: "294de355"}
wantEntries := []*log.Entry{
{
LogLevel: log.Level(zapcore.InfoLevel),
Message: "Successfully created",
Component: "Items",
Event: items.EventCreateItem,
ObjectID: id.MustObjectId(item),
CallerID: id.MustObjectId(user),
Tags: []string{"tag1", "tag2", "tag3"},
},
{
LogLevel: log.Level(zapcore.WarnLevel),
Message: "Successfully updated",
Component: "Items",
Event: items.EventUpdateItem,
ObjectID: id.MustObjectId(item),
CallerID: id.MustObjectId(user),
Attr: map[string]map[string]any{"title": {"old": "old title", "new": "new title"}},
},
}
service := &logmocks.Service{}
service.On("Log", mock.Anything, mock.Anything).
Return(nil).
Run(func(args mock.Arguments) {
entries := args.Get(1).([]*log.Entry)
require.True(t, slices.EqualFunc(wantEntries, entries, func(wantEntry, gotEntry *log.Entry) bool {
require.NotEmpty(t, gotEntry.ID)
require.NotEmpty(t, gotEntry.Timestamp)
gotEntry.ID = wantEntry.ID // игнорируем ID
gotEntry.Timestamp = wantEntry.Timestamp // игнорируем Timestamp
return reflect.DeepEqual(wantEntry, gotEntry)
}))
}).
Once()
usersService := &usersmocks.Users{}
usersService.On("GetByIdentity", mock.Anything, "74d90aaf").Return(user, nil).Once()
factory := auth.PrincipalFactory{Users: usersService}
ws := &BufferedWriteSyncer{Service: service}
logger := zap.New(NewCore(ws))
// Пример отправки логов для сервиса Items
{
logger := logger.With(Component("Items"))
ctx := auth.WithPrincipal(context.Background(), factory.User("74d90aaf"))
// Отправка лога при создании item
logger.Info("Successfully created",
Event(items.EventCreateItem),
Object(item),
CallerFromContext(ctx),
Tags("tag1", "tag2", "tag3"),
)
// Отправка лога при обновлении item
logger.Warn("Successfully updated",
Event(items.EventUpdateItem),
Object(item),
CallerFromContext(ctx),
Attr(map[string]map[string]any{"title": {"old": "old title", "new": "new title"}}),
)
}
err := ws.Stop()
require.NoError(t, err)
service.AssertExpectations(t)
}
...@@ -2,78 +2,72 @@ package zap ...@@ -2,78 +2,72 @@ package zap
import ( import (
"context" "context"
"fmt"
"git.perx.ru/perxis/perxis-go/id" "git.perx.ru/perxis/perxis-go/id"
_ "git.perx.ru/perxis/perxis-go/id/system" // регистрируем обработчики для системных объектов
"git.perx.ru/perxis/perxis-go/pkg/auth" "git.perx.ru/perxis/perxis-go/pkg/auth"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore"
) )
const ( func Category(category string) zap.Field {
unknownObject = "unknown" if category == "" {
unknownCaller = "unknown" return zap.Skip()
) }
func Category(category string) zapcore.Field {
return zap.String("category", category) return zap.String("category", category)
} }
func Component(component string) zapcore.Field { func Component(component string) zap.Field {
if component == "" {
return zap.Skip()
}
return zap.String("component", component) return zap.String("component", component)
} }
func Event(event string) zapcore.Field { func Event(event string) zap.Field {
if event == "" {
return zap.Skip()
}
return zap.String("event", event) return zap.String("event", event)
} }
// Object возвращает поле и устанавливает передаваемый аргумент в качестве идентификатора объекта в формате ObjectID. // Object возвращает поле и устанавливает передаваемый аргумент в качестве идентификатора объекта в формате ObjectId.
// Поддерживаемые типы: string, fmt.Stringer. // Поддерживает типы в формате ObjectId: id.Descriptor, string, map[string]any, системные объекты.
// Если передан аргумент другого типа, будет произведена попытка привести переданное значение к ObjectID. func Object(v any) zap.Field {
func Object(v any) zapcore.Field { oid, err := id.NewObjectId(v)
var object = unknownObject if err != nil {
switch value := v.(type) { return zap.Skip()
case string: }
object = value return zap.Reflect("object", oid)
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. // Caller возвращает поле и устанавливает передаваемый аргумент в качестве "вызывающего" в формате ObjectId.
// Поддерживаемые типы: string, fmt.Stringer. // Поддерживает типы в формате ObjectId: id.Descriptor, string, map[string]any, системные объекты.
// Если передан аргумент другого типа, будет произведена попытка привести переданное значение к ObjectID. func Caller(v any) zap.Field {
func Caller(v any) zapcore.Field { oid, err := id.NewObjectId(v)
var caller = unknownCaller if err != nil {
switch value := v.(type) { return zap.Skip()
case string: }
caller = value return zap.Reflect("caller", oid)
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. // CallerFromContext извлекает auth.Principal из контекста и устанавливает его в качестве "вызывающего" в формате Object.
func CallerFromContext(ctx context.Context) zapcore.Field { func CallerFromContext(ctx context.Context) zap.Field {
if ctx == nil {
return zap.Skip()
}
return Caller(auth.GetPrincipal(ctx)) return Caller(auth.GetPrincipal(ctx))
} }
func Attr(attr any) zapcore.Field { func Attr(attr any) zap.Field {
if attr == nil {
return zap.Skip()
}
return zap.Any("attr", attr) return zap.Any("attr", attr)
} }
func Tags(tags ...string) zapcore.Field { func Tags(tags ...string) zap.Field {
if len(tags) == 0 {
return zap.Skip()
}
return zap.Strings("tags", tags) return zap.Strings("tags", tags)
} }
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: Object(item), want: zap.Reflect("object", oid)},
{name: "object id", field: Object(itemId), want: zap.Reflect("object", oid)},
{name: "string", field: Object(oid.String()), want: zap.Reflect("object", oid)},
{name: "invalid", field: Object(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: Caller(user), want: zap.Reflect("caller", oid)},
{name: "object id", field: Caller(userId), want: zap.Reflect("caller", oid)},
{name: "string", field: Caller(oid.String()), want: zap.Reflect("caller", oid)},
{name: "invalid", field: Caller(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: CallerFromContext(ctx), want: zap.Reflect("caller", oid)},
{name: "invalid", field: CallerFromContext(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))
})
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment