Browse Source

add zap

pull/1/head
loveckiy.ivan 11 months ago
parent
commit
5d08625c5d
  1. 62
      README.md
  2. 70
      field.go
  3. 58
      field_storage.go
  4. 42
      field_storage_test.go
  5. 122
      fields.go
  6. 8
      go.mod
  7. 17
      go.sum
  8. 113
      kafka.go
  9. 25
      level_observer.go
  10. 364
      logger.go
  11. 333
      logger_main.go
  12. 152
      logger_test.go
  13. 401
      middleware.go
  14. 49
      options.go
  15. 766
      string_casting_json_encoder.go
  16. 108
      string_casting_json_encoder_test.go
  17. 242
      types/json.go
  18. 55
      types/json_test.go
  19. 18
      types/map_string.go
  20. 11
      types/tools.go
  21. 54
      types/types.go
  22. 38
      types/types_test.go
  23. 88
      types/url.go
  24. 77
      types/url_test.go
  25. 127
      types/where.go
  26. 38
      types/where_test.go
  27. 38
      wrapper.go

62
README.md

@ -1,2 +1,62 @@
# logger
# Logger
Рекомендуемый logger для всех проектов.
По сути, это просто обертка над `go.uber.org/zap` с небольшим дополнительным функционалом.
## Краткое руководство по использованию
Рекомендуется также ознакомиться с документацией по `go.uber.org/zap`.
### Инициализация
В самом начале main-функции требуется инициализация:
```go
logger.SetupDefaultLogger("my-project-name")
```
### Запись в лог
Примеры:
```go
// Запишет информационное сообщение с дополнительным полем любого типа,
// пытаясь его представить в текстовом виде
logger.Logger(ctx).Info("Some info message", zap.Any("key", someVar))
// Запишет предупреждение с дополнительным JSON-полем,
// в котором автоматически замаскирует "секретные" данные
logger.Logger(ctx).Warn("Some info message", types.JSON("data", someJsonStr))
// Запишет ошибку c URL в дополнительном поле,
// в котором автоматически замаскирует "секретные" данные
logger.Logger(ctx).Error("Some error message", types.URL("url", someUrl))
// Запишет фатальную ошибку и вызовет выход из программы с кодом 1
logger.Logger(ctx).Fatal("Some error message", zap.Error("err", err))
```
### Сохранение Request-ID
Рекомендуется настроить автоматическое добавление в логи поля `request-id` - ID текущего запроса.
Это можно сделать несколькими способами.
Рекомендуемый способ - использовать middleware:
* для HTTP - `HTTPMiddleware`, `HTTPMiddlewareWithParams` (логирование запросов с заданными параметрами)
* для gRPC - `GRPCUnaryServerInterceptor`
Но можно и вручную, вызвав функцию `SetRequestIDCtx`
Также есть функция для получения используемого Request-ID - `GetFieldCtx`
(в случае отсутствия сохраненного в контексте request-id вернет пустую строку)
### Добавление своих полей в лог
Можно настроить автоматическое добавление своих полей в лог.
* `SetFieldCtx` - добавление своего поля в лог (через контекст)
* `GetFieldCtx` - получение ранее сохраненного поля лога (если в контексте нет такого поля, вернется пустая строка)
* `WithFieldsContext` - добавление лог полей в context. Все поля добавленные в текущий контекст будут записаны в логи.
При записи полей логов в контекст рекомендуется использовать этот метод.
### Дополнительные типы полей
Помимо широкого набора типов полей, предоставляемых `zap`,
доступны дополнительные типы полей (пакет `types`):
* `JSON` - строка в формате JSON, в которой автоматически маскируются "секретные" ключи
(список ключей - см. в исходном коде)
* `URL` - строка с URL, в которой автоматически маскируются "секретные" ключи
(список ключей - см. в исходном коде)

70
field.go

@ -0,0 +1,70 @@
package logger
import "go.uber.org/zap"
type fieldType int
const (
fieldTypeString fieldType = iota
fieldTypeUint64
fieldTypeFloat64
)
// Field - alias типов логов для работы с fieldStorage.
// В дальнейшем надо бы использовать для самого пакета логера, чтобы абстрагироваться от внешнего пакета zap.
type Field struct {
typeOf fieldType
key string
value interface{}
}
// External - каст типов под внешний логер. Если будут добавлены новые типы для обработки, надо расширять кастинг,
// по аналогии с FieldUint64.
func (f *Field) External() zap.Field {
switch f.typeOf {
case fieldTypeString:
{
if v, ok := f.value.(string); ok {
return zap.String(f.key, v)
}
}
case fieldTypeUint64:
{
if v, ok := f.value.(uint64); ok {
return zap.Uint64(f.key, v)
}
}
case fieldTypeFloat64:
{
if v, ok := f.value.(float64); ok {
return zap.Float64(f.key, v)
}
}
}
return zap.String("warn", "Field type is not found")
}
func FieldString(key string, val string) Field {
return Field{
typeOf: fieldTypeString,
key: key,
value: val,
}
}
func FieldUint64(key string, val uint64) Field {
return Field{
typeOf: fieldTypeUint64,
key: key,
value: val,
}
}
func FieldFloat64(key string, val float64) Field {
return Field{
typeOf: fieldTypeFloat64,
key: key,
value: val,
}
}

58
field_storage.go

@ -0,0 +1,58 @@
package logger
import (
"context"
"sync"
"go.uber.org/zap"
)
const storageKey key = "wb.logger.field_storage"
type fieldMap map[string]Field
// fieldStorage структура хранения полей в map.
type fieldStorage struct {
fields fieldMap
sync.RWMutex
}
func (s *fieldStorage) SetField(f Field) {
s.Lock()
s.fields[f.key] = f
s.Unlock()
}
func (s *fieldStorage) External() []zap.Field {
s.RLock()
externalFields := make([]zap.Field, 0, len(s.fields))
for _, v := range s.fields {
externalFields = append(externalFields, v.External())
}
s.RUnlock()
return externalFields
}
func newStorage() *fieldStorage {
mp := make(fieldMap, 5)
return &fieldStorage{fields: mp}
}
func getStorageFromCtx(ctx context.Context) (*fieldStorage, bool) {
if mc, ok := ctx.Value(storageKey).(*fieldStorage); ok && mc != nil {
return mc, false
}
return newStorage(), true
}
func withStorageContext(ctx context.Context, storage *fieldStorage) context.Context {
return context.WithValue(ctx, storageKey, storage)
}
func withStorageFields(ctx context.Context, logger *zap.Logger) *zap.Logger {
storage, _ := getStorageFromCtx(ctx)
return logger.With(storage.External()...)
}

42
field_storage_test.go

@ -0,0 +1,42 @@
package logger
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
)
func TestFieldsStorage_SetFields(t *testing.T) {
testR := require.New(t)
testCases := []struct {
Input []Field
Expect []zap.Field
}{
{
Input: []Field{FieldUint64("MerchantIDKey", 123), FieldUint64("MerchantIDKey", 124)},
Expect: []zap.Field{zap.Uint64("MerchantIDKey", 124)},
},
{
Input: []Field{FieldUint64("MerchantIDKey", 124)},
Expect: []zap.Field{zap.Uint64("MerchantIDKey", 124)},
},
{
Input: []Field{FieldString("RequestKey", "124"), FieldUint64("MerchantIDKey", 123)},
Expect: []zap.Field{zap.Uint64("MerchantIDKey", 123), zap.String("RequestKey", "124")},
},
{
Input: nil,
Expect: []zap.Field{},
},
}
for i := range testCases {
ctx := context.Background()
ctx = WithFieldsContext(ctx, testCases[i].Input...)
storage, _ := getStorageFromCtx(ctx)
res := storage.External()
testR.ElementsMatch(testCases[i].Expect, res)
}
}

122
fields.go

@ -0,0 +1,122 @@
package logger
import (
"context"
"strconv"
"sync"
)
const (
requestIDField string = "request-id"
)
//nolint:gochecknoglobals
var (
logKeys = make(map[key]struct{})
mtx sync.RWMutex
)
type key string
func SetFieldCtx(ctx context.Context, name, val string) context.Context {
nameKey := key("wb.logger." + name)
mtx.RLock()
_, ok := logKeys[nameKey]
mtx.RUnlock()
if !ok {
mtx.Lock()
logKeys[nameKey] = struct{}{}
mtx.Unlock()
}
return context.WithValue(ctx, nameKey, val)
}
// WithFieldsContext получение контекста из полей явного типа,
// при логировании поля будут прописаны в external логгер (zap).
func WithFieldsContext(ctx context.Context, fields ...Field) context.Context {
storage, isNew := getStorageFromCtx(ctx)
for _, field := range fields {
storage.SetField(field)
}
if isNew {
// если в контексте не был записан storage возвращаем с родительским контекстом
return withStorageContext(ctx, storage)
}
// если он уже есть, достаточно вернуть текущий контекст, тк storage ссылочный
return ctx
}
func GetFieldCtx(ctx context.Context, name string) string {
nameKey := key("wb.logger." + name)
requestID, _ := ctx.Value(nameKey).(string)
return requestID
}
// SetRequestIDCtx sets request-id log field via context.
// Tip: use HTTPMiddleware instead.
func SetRequestIDCtx(ctx context.Context, val string) context.Context {
return SetFieldCtx(ctx, requestIDField, val)
}
func GetRequestIDCtx(ctx context.Context) string {
return GetFieldCtx(ctx, requestIDField)
}
// Deprecated: use SetRequestIDCtx instead.
func NewContext(ctx context.Context, requestID string) context.Context {
return SetRequestIDCtx(ctx, requestID)
}
// Deprecated: use GetRequestIDCtx instead.
func GetRequestID(ctx context.Context) string {
return GetRequestIDCtx(ctx)
}
// Deprecated: use AddFieldCtx instead.
func SetInvoiceIDCtx(ctx context.Context, val string) context.Context {
return SetFieldCtx(ctx, "invoice-id", val)
}
// Deprecated: use AddFieldCtx instead.
func SetInternalIDCtx(ctx context.Context, val string) context.Context {
return SetFieldCtx(ctx, "internal-id", val)
}
// Deprecated: use AddFieldCtx instead.
func SetExternalIDCtx(ctx context.Context, val string) context.Context {
return SetFieldCtx(ctx, "external-id", val)
}
// Deprecated: use AddFieldCtx instead.
func SetBankNameCtx(ctx context.Context, val string) context.Context {
return SetFieldCtx(ctx, "bank-name", val)
}
// Deprecated: use AddFieldCtx instead.
func SetTerminalNameCtx(ctx context.Context, val string) context.Context {
return SetFieldCtx(ctx, "terminal-name", val)
}
// Deprecated: use AddFieldCtx instead.
func SetMerchantIDCtx(ctx context.Context, val string) context.Context {
return SetFieldCtx(ctx, "merchant-id", val)
}
// Deprecated: use AddFieldCtx instead.
func SetUserIDCtx(ctx context.Context, val string) context.Context {
return SetFieldCtx(ctx, "user-id", val)
}
// Deprecated: use AddFieldCtx instead.
func SetShardIndexCtx(ctx context.Context, val uint64) context.Context {
return SetFieldCtx(SetFieldCtx(
ctx,
"wb.logger.shard-index", strconv.FormatUint(val, 10)),
"wb.logger.shard-number", strconv.FormatUint(val+1, 10),
)
}

8
go.mod

@ -4,18 +4,26 @@ go 1.19
require (
git.lowcodeplatform.net/fabric/logbox-client v0.1.3
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.8.0
go.uber.org/zap v1.24.0
)
require (
git.lowcodeplatform.net/fabric/logbox v0.1.1 // indirect
git.lowcodeplatform.net/fabric/packages v0.0.0-20230129123752-a3dc6393a856 // indirect
github.com/Jille/grpc-multi-resolver v1.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.7.0 // indirect
golang.org/x/net v0.4.0 // indirect
golang.org/x/sys v0.3.0 // indirect
golang.org/x/text v0.5.0 // indirect
google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6 // indirect
google.golang.org/grpc v1.52.3 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

17
go.sum

@ -15,13 +15,27 @@ github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec=
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -41,3 +55,6 @@ google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

113
kafka.go

@ -0,0 +1,113 @@
package logger
import (
"context"
"fmt"
"net"
"strconv"
"github.com/pkg/errors"
"github.com/segmentio/kafka-go"
"go.uber.org/zap"
)
type KafkaConfig struct {
// format: "localhost:9092,localhost:9093,localhost:9094"
Addr []string
Topic string
NumPartitions int
ReplicationFactor int
}
func (kc *KafkaConfig) createTopic() error {
conn, err := kafka.Dial("tcp", kc.Addr[0])
if err != nil {
return errors.Wrap(err, "can't connect to Kafka")
}
defer conn.Close()
controller, err := conn.Controller()
if err != nil {
return errors.Wrap(err, "cannot get controller")
}
var controllerConn *kafka.Conn
controllerConn, err = kafka.Dial("tcp", net.JoinHostPort(controller.Host, strconv.Itoa(controller.Port)))
if err != nil {
return errors.Wrap(err, "can't connect to Kafka controller")
}
defer controllerConn.Close()
replicationFactor := 1
if kc.ReplicationFactor > 1 {
replicationFactor = kc.ReplicationFactor
}
numPartitions := 1
if kc.NumPartitions > 1 {
replicationFactor = kc.NumPartitions
}
topicConfigs := []kafka.TopicConfig{
{
Topic: kc.Topic,
NumPartitions: numPartitions,
ReplicationFactor: replicationFactor,
},
}
err = controllerConn.CreateTopics(topicConfigs...)
if err != nil {
return errors.Wrap(err, "can't create topics")
}
return nil
}
func (kc *KafkaConfig) writer(errLogger *zap.Logger) *kafka.Writer {
return &kafka.Writer{
Addr: kafka.TCP(kc.Addr...),
Topic: kc.Topic,
Balancer: &kafka.LeastBytes{},
Async: true,
ErrorLogger: &errorLogger{
errLogger,
},
}
}
type writerSyncer struct {
kwr *kafka.Writer
errorLogger *zap.Logger
topic string
}
func (ws *writerSyncer) Write(p []byte) (int, error) {
val := make([]byte, len(p))
copy(val, p)
m := kafka.Message{
Value: val,
}
err := ws.kwr.WriteMessages(context.Background(), m)
if err != nil {
ws.errorLogger.Error("Error writing log", zap.ByteString("log", p), zap.Error(err))
}
return len(p), nil
}
func (ws *writerSyncer) Sync() error {
return ws.kwr.Close()
}
type errorLogger struct {
*zap.Logger
}
func (l *errorLogger) Printf(msg string, args ...interface{}) {
l.Error(fmt.Sprintf(msg, args...))
}

25
level_observer.go

@ -0,0 +1,25 @@
package logger
import (
"context"
"time"
"go.uber.org/zap/zapcore"
)
type LevelProvider = func(ctx context.Context, curLevel zapcore.Level) zapcore.Level
func SetLevelObserver(ctx context.Context, interval time.Duration, provider LevelProvider) {
go onceLevelObserver.Do(func() {
t := time.NewTicker(interval)
defer t.Stop()
for {
select {
case <-t.C:
level.SetLevel(provider(ctx, level.Level()))
case <-ctx.Done():
return
}
}
})
}

364
logger.go

@ -1,333 +1,133 @@
// обертка для логирования, которая дополняем аттрибутами логируемого процесса logrus
// дополняем значениями, идентифицирующими запущенный сервис UID,Name,Service
package logger
import (
"context"
"fmt"
"io"
"os"
"strings"
"sync"
"time"
"git.lowcodeplatform.net/fabric/lib"
"github.com/sirupsen/logrus"
"github.com/pkg/errors"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var logrusB = logrus.New()
const sep = string(os.PathSeparator)
var (
defaultLogger *Engine
level zap.AtomicLevel
onceLevelObserver sync.Once
)
// LogLine структура строк лог-файла. нужна для анмаршалинга
type LogLine struct {
Config string `json:"config"`
Level string `json:"level"`
Msg interface{} `json:"msg"`
Name string `json:"name"`
Srv string `json:"srv"`
Time string `json:"time"`
Uid string `json:"uid"`
type Engine struct {
*zap.Logger
}
type log struct {
// куда логируем? stdout/;*os.File на файл, в который будем писать логи
Output io.Writer `json:"output"`
//Debug:
// сообщения отладки, профилирования.
// В production системе обычно сообщения этого уровня включаются при первоначальном
// запуске системы или для поиска узких мест (bottleneck-ов).
//goland:noinspection GoUnusedExportedFunction
func SetupDefaultLogger(namespace string, options ...ConfigOption) {
logger := initLogger(options...)
defaultLogger = New(logger.Named(namespace))
}
//Info: - логировать процесс выполнения
// обычные сообщения, информирующие о действиях системы.
// Реагировать на такие сообщения вообще не надо, но они могут помочь, например,
// при поиске багов, расследовании интересных ситуаций итд.
func SetupDefaultKafkaLogger(namespace string, cfg KafkaConfig) error {
if len(cfg.Addr) == 0 {
return errors.New("kafka address must be specified")
}
//Warning: - логировать странные операции
// записывая такое сообщение, система пытается привлечь внимание обслуживающего персонала.
// Произошло что-то странное. Возможно, это новый тип ситуации, ещё не известный системе.
// Следует разобраться в том, что произошло, что это означает, и отнести ситуацию либо к
// инфо-сообщению, либо к ошибке. Соответственно, придётся доработать код обработки таких ситуаций.
if err := cfg.createTopic(); err != nil {
return errors.Wrapf(err, "cannot create topic: %s", cfg.Topic)
}
//Error: - логировать ошибки
// ошибка в работе системы, требующая вмешательства. Что-то не сохранилось, что-то отвалилось.
// Необходимо принимать меры довольно быстро! Ошибки этого уровня и выше требуют немедленной записи в лог,
// чтобы ускорить реакцию на них. Нужно понимать, что ошибка пользователя – это не ошибка системы.
// Если пользователь ввёл в поле -1, где это не предполагалось – не надо писать об этом в лог ошибок.
errorLogger := initLogger(WithStringCasting())
//Panic: - логировать критические ошибки
// это особый класс ошибок. Такие ошибки приводят к неработоспособности системы в целом, или
// неработоспособности одной из подсистем. Чаще всего случаются фатальные ошибки из-за неверной конфигурации
// или отказов оборудования. Требуют срочной, немедленной реакции. Возможно, следует предусмотреть уведомление о таких ошибках по SMS.
// указываем уровни логирования Error/Warning/Debug/Info/Panic
ws := &writerSyncer{
kwr: cfg.writer(errorLogger),
topic: cfg.Topic,
errorLogger: errorLogger,
}
//Trace: - логировать обработки запросов
enc := newStringCastingEncoder(zap.NewProductionEncoderConfig())
core := zapcore.NewCore(enc, ws, zap.NewAtomicLevelAt(zap.InfoLevel))
// можно указывать через | разные уровени логирования, например Error|Warning
// можно указать All - логирование всех уровней
Levels string `json:"levels"`
// uid процесса (сервиса), который логируется (случайная величина)
UID string `json:"uid"`
// имя процесса (сервиса), который логируется
Name string `json:"name"`
// название сервиса (app/gui...)
Service string `json:"service"`
// директория сохранения логов
Dir string `json:"dir"`
// uid-конфигурации с которой был запущен процесс
Config string `json:"config"`
// интервал между проверками актуального файла логирования (для текущего дня)
IntervalReload time.Duration `json:"delay_reload"`
// интервал проверками на наличие файлов на удаление
IntervalClearFiles time.Duration `json:"interval_clear_files"`
// период хранения файлов лет-месяцев-дней (например: 0-1-0 - хранить 1 месяц)
PeriodSaveFiles string `json:"period_save_files"`
errOut, _, err := zap.Open("stderr")
if err != nil {
return err
}
// путь к сервису отправки логов в хранилище (Logbox)
LogboxURL string
// интервал отправки (в промежутках сохраняем в буфер)
LogboxSendInterval time.Duration
opts := []zap.Option{zap.ErrorOutput(errOut), zap.AddCaller()}
File *os.File
logger := zap.New(core, opts...)
defaultLogger = New(logger.Named(namespace))
mux *sync.Mutex
return nil
}
// ConfigLogger общий конфигуратор логирования
type ConfigLogger struct {
Level, Uid, Name, Srv, Config string
func Logger(ctx context.Context) *Engine {
if defaultLogger == nil {
panic("logger has not been initialized, call SetupDefaultLogger() before use")
}
File ConfigFileLogger
Vfs ConfigVfsLogger
Logbox ConfigLogboxLogger
Priority []string
return defaultLogger.WithContext(ctx)
}
type Log interface {
Trace(args ...interface{})
Debug(args ...interface{})
Info(args ...interface{})
Warning(args ...interface{})
Error(err error, args ...interface{})
Panic(err error, args ...interface{})
Exit(err error, args ...interface{})
Close()
func (l *Engine) SetLevel(lvl zapcore.Level) {
level.SetLevel(lvl)
}
func (l *log) Trace(args ...interface{}) {
if strings.Contains(l.Levels, "Trace") {
logrusB.SetOutput(l.Output)
logrusB.SetFormatter(&logrus.JSONFormatter{})
logrusB.SetLevel(logrus.TraceLevel)
logrusB.WithFields(logrus.Fields{
"name": l.Name,
"uid": l.UID,
"srv": l.Service,
"config": l.Config,
}).Trace(args...)
if strings.Contains(l.Levels, "Stdout") {
fmt.Printf("Trace: %+v\n", args)
}
func New(logger *zap.Logger) *Engine {
return &Engine{
Logger: logger,
}
}
func (l *log) Debug(args ...interface{}) {
if strings.Contains(l.Levels, "Debug") {
logrusB.SetOutput(l.Output)
logrusB.SetFormatter(&logrus.JSONFormatter{})
// Only log the warning severity or above.
logrusB.SetLevel(logrus.DebugLevel)
logrusB.WithFields(logrus.Fields{
"name": l.Name,
"uid": l.UID,
"srv": l.Service,
"config": l.Config,
}).Debug(args...)
if strings.Contains(l.Levels, "Stdout") {
fmt.Printf("Debug: %+v\n", args)
}
func (l *Engine) WithContext(ctx context.Context) *Engine {
if ctx == nil {
return l
}
}
func (l *log) Info(args ...interface{}) {
if strings.Contains(l.Levels, "Info") {
logrusB.SetOutput(l.Output)
logrusB.SetFormatter(&logrus.JSONFormatter{})
logger := l.Logger
logrusB.SetLevel(logrus.InfoLevel)
mtx.RLock()
for field := range logKeys {
fieldName := string(field)
fieldNameParts := strings.Split(fieldName, ".")
fieldName = fieldNameParts[len(fieldNameParts)-1]
logrusB.WithFields(logrus.Fields{
"name": l.Name,
"uid": l.UID,
"srv": l.Service,
"config": l.Config,
}).Info(args...)
if strings.Contains(l.Levels, "Stdout") {
fmt.Printf("Info: %+v\n", args)
value := ctx.Value(field)
if value == nil {
continue
}
}
}
func (l *log) Warning(args ...interface{}) {
if strings.Contains(l.Levels, "Warning") {
logrusB.SetOutput(l.Output)
logrusB.SetFormatter(&logrus.JSONFormatter{})
logrusB.SetLevel(logrus.WarnLevel)
logrusB.WithFields(logrus.Fields{
"name": l.Name,
"uid": l.UID,
"srv": l.Service,
"config": l.Config,
}).Warn(args...)
if strings.Contains(l.Levels, "Stdout") {
fmt.Printf("Warn: %+v\n", args)
if valueStr, ok := value.(string); ok && valueStr != "" {
logger = logger.With(zap.String(fieldName, valueStr))
}
}
}
mtx.RUnlock()
func (l *log) Error(err error, args ...interface{}) {
if err != nil {
if args != nil {
args = append(args, "; error:", err)
} else {
args = append(args, "error:", err)
}
}
if strings.Contains(l.Levels, "Error") {
logrusB.SetOutput(l.Output)
logrusB.SetFormatter(&logrus.JSONFormatter{})
logrusB.SetLevel(logrus.ErrorLevel)
// оборачиваем полями из fieldStorage
logger = withStorageFields(ctx, logger)
logrusB.WithFields(logrus.Fields{
"name": l.Name,
"uid": l.UID,
"srv": l.Service,
"config": l.Config,
}).Error(args...)
if strings.Contains(l.Levels, "Stdout") {
fmt.Printf("Error: %+v\n", args)
}
}
return &Engine{Logger: logger}
}
func (l *log) Panic(err error, args ...interface{}) {
if err != nil {
if args != nil {
args = append(args, "; error:", err)
} else {
args = append(args, "error:", err)
}
}
if strings.Contains(l.Levels, "Panic") {
if strings.Contains(l.Levels, "Stdout") {
fmt.Printf("Panic: %+v\n", args)
}
logrusB.SetOutput(l.Output)
logrusB.SetFormatter(&logrus.JSONFormatter{})
logrusB.SetLevel(logrus.PanicLevel)
logrusB.WithFields(logrus.Fields{
"name": l.Name,
"uid": l.UID,
"srv": l.Service,
"config": l.Config,
}).Panic(args...)
}
func (l *Engine) AddHook(f func(zapcore.Entry)) {
l.Logger = l.Logger.WithOptions(zap.Hooks(func(entry zapcore.Entry) error {
f(entry)
return nil
}))
}
// Exit внутренняя ф-ция логирования и прекращения работы программы
func (l *log) Exit(err error, args ...interface{}) {
if err != nil {
if args != nil {
args = append(args, "; error:", err)
} else {
args = append(args, "error:", err)
}
func initLogger(options ...ConfigOption) *zap.Logger {
level = zap.NewAtomicLevelAt(zap.InfoLevel)
config := zap.NewProductionConfig()
config.Level = level
config.Sampling = &zap.SamplingConfig{
Initial: 1000,
Thereafter: 10,
}
if strings.Contains(l.Levels, "Fatal") {
if strings.Contains(l.Levels, "Stdout") {
fmt.Printf("Exit: %+v\n", args)
}
logrusB.SetOutput(l.Output)
logrusB.SetFormatter(&logrus.JSONFormatter{})
logrusB.SetLevel(logrus.FatalLevel)
logrusB.WithFields(logrus.Fields{
"name": l.Name,
"uid": l.UID,
"srv": l.Service,
"config": l.Config,
}).Fatal(args...)
for _, opt := range options {
config = opt(config)
}
}
func (l *log) Close() {
l.File.Close()
}
func NewLogger(ctx context.Context, cfg ConfigLogger) (logger Log, initType string, err error) {
var errI error
err = fmt.Errorf("logger init")
for _, v := range cfg.Priority {
if v == "file" && err != nil {
// если путь указан относительно / значит задан абсолютный путь, иначе в директории
if cfg.File.Dir[:1] != sep {
rootDir, _ := lib.RootDir()
cfg.File.Dir = rootDir + sep + "logs" + sep + cfg.File.Dir
}
// инициализировать лог и его ротацию
logger, errI = NewFileLogger(ctx, cfg)
if errI != nil {
err = fmt.Errorf("%s %s failed init files-logger, (err: %s)", err, "&#8594;", errI)
fmt.Println(err, cfg)
} else {
initType = v
err = nil
}
}
if v == "vfs" && err != nil {
// инициализировать лог и его ротацию
vs := strings.Split(cfg.Vfs.Dir, sep) // берем только последнее значение в пути для vfs-логера
vs = vs[len(vs)-1:]
if len(vs) != 0 {
cfg.Vfs.Dir = "logs"
}
// инициализировать лог и его ротацию
logger, errI = NewVfsLogger(ctx, cfg)
fmt.Println(logger, errI)
if errI != nil {
err = fmt.Errorf("%s %s failed init files-vfs, (err: %s)", err, "&#8594;", errI)
fmt.Println(err, cfg)
} else {
initType = v
err = nil
}
}
if v == "logbox" && err != nil {
// инициализировать лог и его ротацию
logger, errI = NewLogboxLogger(ctx, cfg)
if errI != nil {
err = fmt.Errorf("%s %s failed init files-logbox, (err: %s)", err, "&#8594;", errI)
} else {
initType = v
err = nil
}
}
}
logger, _ := config.Build()
return logger, initType, err
return logger
}

333
logger_main.go

@ -0,0 +1,333 @@
// обертка для логирования, которая дополняем аттрибутами логируемого процесса logrus
// дополняем значениями, идентифицирующими запущенный сервис UID,Name,Service
package logger
import (
"context"
"fmt"
"io"
"os"
"strings"
"sync"
"time"
"git.lowcodeplatform.net/fabric/lib"
"github.com/sirupsen/logrus"
)
var logrusB = logrus.New()
const sep = string(os.PathSeparator)
// LogLine структура строк лог-файла. нужна для анмаршалинга
type LogLine struct {
Config string `json:"config"`
Level string `json:"level"`
Msg interface{} `json:"msg"`
Name string `json:"name"`
Srv string `json:"srv"`
Time string `json:"time"`
Uid string `json:"uid"`
}
type log struct {
// куда логируем? stdout/;*os.File на файл, в который будем писать логи
Output io.Writer `json:"output"`
//Debug:
// сообщения отладки, профилирования.
// В production системе обычно сообщения этого уровня включаются при первоначальном
// запуске системы или для поиска узких мест (bottleneck-ов).
//Info: - логировать процесс выполнения
// обычные сообщения, информирующие о действиях системы.
// Реагировать на такие сообщения вообще не надо, но они могут помочь, например,
// при поиске багов, расследовании интересных ситуаций итд.
//Warning: - логировать странные операции
// записывая такое сообщение, система пытается привлечь внимание обслуживающего персонала.
// Произошло что-то странное. Возможно, это новый тип ситуации, ещё не известный системе.
// Следует разобраться в том, что произошло, что это означает, и отнести ситуацию либо к
// инфо-сообщению, либо к ошибке. Соответственно, придётся доработать код обработки таких ситуаций.
//Error: - логировать ошибки
// ошибка в работе системы, требующая вмешательства. Что-то не сохранилось, что-то отвалилось.
// Необходимо принимать меры довольно быстро! Ошибки этого уровня и выше требуют немедленной записи в лог,
// чтобы ускорить реакцию на них. Нужно понимать, что ошибка пользователя – это не ошибка системы.
// Если пользователь ввёл в поле -1, где это не предполагалось – не надо писать об этом в лог ошибок.
//Panic: - логировать критические ошибки
// это особый класс ошибок. Такие ошибки приводят к неработоспособности системы в целом, или
// неработоспособности одной из подсистем. Чаще всего случаются фатальные ошибки из-за неверной конфигурации
// или отказов оборудования. Требуют срочной, немедленной реакции. Возможно, следует предусмотреть уведомление о таких ошибках по SMS.
// указываем уровни логирования Error/Warning/Debug/Info/Panic
//Trace: - логировать обработки запросов
// можно указывать через | разные уровени логирования, например Error|Warning
// можно указать All - логирование всех уровней
Levels string `json:"levels"`
// uid процесса (сервиса), который логируется (случайная величина)
UID string `json:"uid"`
// имя процесса (сервиса), который логируется
Name string `json:"name"`
// название сервиса (app/gui...)
Service string `json:"service"`
// директория сохранения логов
Dir string `json:"dir"`
// uid-конфигурации с которой был запущен процесс
Config string `json:"config"`
// интервал между проверками актуального файла логирования (для текущего дня)
IntervalReload time.Duration `json:"delay_reload"`
// интервал проверками на наличие файлов на удаление
IntervalClearFiles time.Duration `json:"interval_clear_files"`
// период хранения файлов лет-месяцев-дней (например: 0-1-0 - хранить 1 месяц)
PeriodSaveFiles string `json:"period_save_files"`
// путь к сервису отправки логов в хранилище (Logbox)
LogboxURL string
// интервал отправки (в промежутках сохраняем в буфер)
LogboxSendInterval time.Duration
File *os.File
mux *sync.Mutex
}
// ConfigLogger общий конфигуратор логирования
type ConfigLogger struct {
Level, Uid, Name, Srv, Config string
File ConfigFileLogger
Vfs ConfigVfsLogger
Logbox ConfigLogboxLogger
Priority []string
}
type Log interface {
Trace(args ...interface{})
Debug(args ...interface{})
Info(args ...interface{})
Warning(args ...interface{})
Error(err error, args ...interface{})
Panic(err error, args ...interface{})
Exit(err error, args ...interface{})
Close()
}
func (l *log) Trace(args ...interface{}) {
if strings.Contains(l.Levels, "Trace") {
logrusB.SetOutput(l.Output)
logrusB.SetFormatter(&logrus.JSONFormatter{})
logrusB.SetLevel(logrus.TraceLevel)
logrusB.WithFields(logrus.Fields{
"name": l.Name,
"uid": l.UID,
"srv": l.Service,
"config": l.Config,
}).Trace(args...)
if strings.Contains(l.Levels, "Stdout") {
fmt.Printf("Trace: %+v\n", args)
}
}
}
func (l *log) Debug(args ...interface{}) {
if strings.Contains(l.Levels, "Debug") {
logrusB.SetOutput(l.Output)
logrusB.SetFormatter(&logrus.JSONFormatter{})
// Only log the warning severity or above.
logrusB.SetLevel(logrus.DebugLevel)
logrusB.WithFields(logrus.Fields{
"name": l.Name,
"uid": l.UID,
"srv": l.Service,
"config": l.Config,
}).Debug(args...)
if strings.Contains(l.Levels, "Stdout") {
fmt.Printf("Debug: %+v\n", args)
}
}
}
func (l *log) Info(args ...interface{}) {
if strings.Contains(l.Levels, "Info") {
logrusB.SetOutput(l.Output)
logrusB.SetFormatter(&logrus.JSONFormatter{})
logrusB.SetLevel(logrus.InfoLevel)
logrusB.WithFields(logrus.Fields{
"name": l.Name,
"uid": l.UID,
"srv": l.Service,
"config": l.Config,
}).Info(args...)
if strings.Contains(l.Levels, "Stdout") {
fmt.Printf("Info: %+v\n", args)
}
}
}
func (l *log) Warning(args ...interface{}) {
if strings.Contains(l.Levels, "Warning") {
logrusB.SetOutput(l.Output)
logrusB.SetFormatter(&logrus.JSONFormatter{})
logrusB.SetLevel(logrus.WarnLevel)
logrusB.WithFields(logrus.Fields{
"name": l.Name,
"uid": l.UID,
"srv": l.Service,
"config": l.Config,
}).Warn(args...)
if strings.Contains(l.Levels, "Stdout") {
fmt.Printf("Warn: %+v\n", args)
}
}
}
func (l *log) Error(err error, args ...interface{}) {
if err != nil {
if args != nil {
args = append(args, "; error:", err)
} else {
args = append(args, "error:", err)
}
}
if strings.Contains(l.Levels, "Error") {
logrusB.SetOutput(l.Output)
logrusB.SetFormatter(&logrus.JSONFormatter{})
logrusB.SetLevel(logrus.ErrorLevel)
logrusB.WithFields(logrus.Fields{
"name": l.Name,
"uid": l.UID,
"srv": l.Service,
"config": l.Config,
}).Error(args...)
if strings.Contains(l.Levels, "Stdout") {
fmt.Printf("Error: %+v\n", args)
}
}
}
func (l *log) Panic(err error, args ...interface{}) {
if err != nil {
if args != nil {
args = append(args, "; error:", err)
} else {
args = append(args, "error:", err)
}
}
if strings.Contains(l.Levels, "Panic") {
if strings.Contains(l.Levels, "Stdout") {
fmt.Printf("Panic: %+v\n", args)
}
logrusB.SetOutput(l.Output)
logrusB.SetFormatter(&logrus.JSONFormatter{})
logrusB.SetLevel(logrus.PanicLevel)
logrusB.WithFields(logrus.Fields{
"name": l.Name,
"uid": l.UID,
"srv": l.Service,
"config": l.Config,
}).Panic(args...)
}
}
// Exit внутренняя ф-ция логирования и прекращения работы программы
func (l *log) Exit(err error, args ...interface{}) {
if err != nil {
if args != nil {
args = append(args, "; error:", err)
} else {
args = append(args, "error:", err)
}
}
if strings.Contains(l.Levels, "Fatal") {
if strings.Contains(l.Levels, "Stdout") {
fmt.Printf("Exit: %+v\n", args)
}
logrusB.SetOutput(l.Output)
logrusB.SetFormatter(&logrus.JSONFormatter{})
logrusB.SetLevel(logrus.FatalLevel)
logrusB.WithFields(logrus.Fields{
"name": l.Name,
"uid": l.UID,
"srv": l.Service,
"config": l.Config,
}).Fatal(args...)
}
}
func (l *log) Close() {
l.File.Close()
}
func NewLogger(ctx context.Context, cfg ConfigLogger) (logger Log, initType string, err error) {
var errI error
err = fmt.Errorf("logger init")
for _, v := range cfg.Priority {
if v == "file" && err != nil {
// если путь указан относительно / значит задан абсолютный путь, иначе в директории
if cfg.File.Dir[:1] != sep {
rootDir, _ := lib.RootDir()
cfg.File.Dir = rootDir + sep + "logs" + sep + cfg.File.Dir
}
// инициализировать лог и его ротацию
logger, errI = NewFileLogger(ctx, cfg)
if errI != nil {
err = fmt.Errorf("%s %s failed init files-logger, (err: %s)", err, "&#8594;", errI)
fmt.Println(err, cfg)
} else {
initType = v
err = nil
}
}
if v == "vfs" && err != nil {
// инициализировать лог и его ротацию
vs := strings.Split(cfg.Vfs.Dir, sep) // берем только последнее значение в пути для vfs-логера
vs = vs[len(vs)-1:]
if len(vs) != 0 {
cfg.Vfs.Dir = "logs"
}
// инициализировать лог и его ротацию
logger, errI = NewVfsLogger(ctx, cfg)
fmt.Println(logger, errI)
if errI != nil {
err = fmt.Errorf("%s %s failed init files-vfs, (err: %s)", err, "&#8594;", errI)
fmt.Println(err, cfg)
} else {
initType = v
err = nil
}
}
if v == "logbox" && err != nil {
// инициализировать лог и его ротацию
logger, errI = NewLogboxLogger(ctx, cfg)
if errI != nil {
err = fmt.Errorf("%s %s failed init files-logbox, (err: %s)", err, "&#8594;", errI)
} else {
initType = v
err = nil
}
}
}
return logger, initType, err
}

152
logger_test.go

@ -0,0 +1,152 @@
package logger
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"go.uber.org/zap/zaptest/observer"
)
func TestSetLevelObserver(t *testing.T) {
t.Run("Run3Times", func(t *testing.T) {
level = zap.NewAtomicLevelAt(zap.InfoLevel)
ctx, cancel := context.WithCancel(context.Background())
fnCallsCount := 0
levelObserver := func(ctx context.Context, curLevel zapcore.Level) zapcore.Level {
if fnCallsCount == 3 {
return curLevel
}
switch fnCallsCount {
case 0:
assert.Equal(t, zap.InfoLevel, curLevel)
case 1:
assert.Equal(t, zap.WarnLevel, curLevel)
case 2:
assert.Equal(t, zap.ErrorLevel, curLevel)
}
fnCallsCount++
if fnCallsCount == 3 {
cancel()
}
return curLevel + 1
}
SetLevelObserver(ctx, 1*time.Nanosecond, levelObserver)
<-ctx.Done()
assert.Equal(t, 3, fnCallsCount)
})
t.Run("NotRun", func(t *testing.T) {
level = zap.NewAtomicLevelAt(zap.InfoLevel)
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond)
defer cancel()
fn2 := func(ctx context.Context, curLevel zapcore.Level) zapcore.Level {
assert.Fail(t, "function was call")
return curLevel
}
// TODO: функция зависит от вызова в предыдущем тесте
SetLevelObserver(ctx, 1*time.Nanosecond, fn2)
<-ctx.Done()
})
}
func TestZapLogger(t *testing.T) {
t.Run("without context fields", func(t *testing.T) {
observedZapCore, observedLogs := observer.New(zap.InfoLevel)
observedLogger := zap.New(observedZapCore)
logger := New(observedLogger.Named("wb.logger"))
// init without ctx fields
testLogCaseWithoutCtxFields(logger)
// check
require.Equal(t, 2, observedLogs.Len())
allLogs := observedLogs.All()
assert.Equal(t, "init test logs without ctx fields", allLogs[0].Message)
assert.Equal(t, "log with fields", allLogs[1].Message)
assert.ElementsMatch(t,
[]zap.Field{
zap.String("first", "123"),
zap.Uint("second", 12),
}, allLogs[1].Context)
})
t.Run("with context fields", func(t *testing.T) {
observedZapCore, observedLogs := observer.New(zap.InfoLevel)
observedLogger := zap.New(observedZapCore)
logger := New(observedLogger.Named("wb.logger"))
// init with ctx fields
testLogCaseWithCtxFields(logger)
// check
require.Equal(t, 2, observedLogs.Len())
allLogs := observedLogs.All()
assert.Equal(t, "init test logs with ctx fields", allLogs[0].Message)
assert.Equal(t, "log with fields", allLogs[1].Message)
assert.ElementsMatch(t,
[]zap.Field{
zap.String("first", "123"),
zap.String("ctxKey1", "ctxVal1"),
zap.Uint("second", 12),
}, allLogs[1].Context)
})
t.Run("with context fields with field storage", func(t *testing.T) {
observedZapCore, observedLogs := observer.New(zap.InfoLevel)
observedLogger := zap.New(observedZapCore)
logger := New(observedLogger.Named("wb.logger"))
// init with ctx fields
testLogCaseWithCtxFieldsStorage(logger)
// check
require.Equal(t, 2, observedLogs.Len())
allLogs := observedLogs.All()
assert.Equal(t, "init test logs with ctx fields", allLogs[0].Message)
assert.Equal(t, "log with fields", allLogs[1].Message)
assert.ElementsMatch(t,
[]zap.Field{
zap.String("first", "123"),
zap.String("ctxKey1", "ctxVal1"),
zap.Uint("second", 12),
zap.String("ctx2", "ctx2"),
zap.Uint64("ctx3", 123),
}, allLogs[1].Context)
})
}
func testLogCaseWithoutCtxFields(logger *Engine) {
logger.Info("init test logs without ctx fields")
logger.With(
zap.String("first", "123"),
zap.Uint("second", 12),
).Info("log with fields")
}
func testLogCaseWithCtxFields(logger *Engine) {
ctx := context.Background()
ctx = SetFieldCtx(ctx, "ctxKey1", "ctxVal1")
logger = logger.WithContext(ctx)
logger.Info("init test logs with ctx fields")
logger.With(
zap.String("first", "123"),
zap.Uint("second", 12),
).Info("log with fields")
}
func testLogCaseWithCtxFieldsStorage(logger *Engine) {
ctx := context.Background()
ctx = SetFieldCtx(ctx, "ctxKey1", "ctxVal1")
ctx = WithFieldsContext(ctx, FieldString("ctx2", "ctx2"), FieldUint64("ctx3", 123))
logger = logger.WithContext(ctx)
logger.Info("init test logs with ctx fields")
logger.With(
zap.String("first", "123"),
zap.Uint("second", 12),
).Info("log with fields")
}

401
middleware.go

@ -0,0 +1,401 @@
package logger
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"runtime"
"runtime/debug"
"strings"
"time"
"git.lowcodeplatform.net/packages/logger/types"
"github.com/google/uuid"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
const (
headerXRequestID = "X-Request-ID"
headerXRequestUnit = "X-Request-Unit"
headerXRequestService = "X-Request-Service"
)
const (
requestIDKey = "request-id"
requestUserAgentKey = "user-agent"
requestUnitKey = "unit-key"
requestServiceKey = "service-key"
requestBodyKey = "request-body"
responseBodyKey = "response-body"
methodKey = "method"
remoteAddrKey = "remote-addr"
userAgentKey = "user-agent"
durationKey = "duration"
urlKey = "url"
statusCodeKey = "status-code"
headerExpiresAtKey = "header_expires_at"
requestLengthKey = "request-length"
)
type statusCodeWriter struct {
http.ResponseWriter
statusCode int
}
func (w *statusCodeWriter) WriteHeader(statusCode int) {
w.statusCode = statusCode
w.ResponseWriter.WriteHeader(statusCode)
}
func (w *statusCodeWriter) Write(b []byte) (int, error) {
if w.statusCode == 0 {
w.statusCode = 200
}
return w.ResponseWriter.Write(b)
}
func (w *statusCodeWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
hj, ok := w.ResponseWriter.(http.Hijacker)
if !ok || hj == nil {
return nil, nil, fmt.Errorf("http.Hijacker interface is not implemented in given response writer")
}
return hj.Hijack()
}
func HTTPMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestID := r.Header.Get(headerXRequestID)
if requestID == "" {
requestID = uuid.New().String()
r.Header.Add(headerXRequestID, requestID) // keep compatibility with old logger
}
w.Header().Set(headerXRequestID, requestID)
ctx := SetRequestIDCtx(r.Context(), requestID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// HTTPMiddleWareParams параметры логирования, при пустых параметрах HTTPMiddlewareWithParams
// будет отрабатывать, как HTTPMiddleware.
type HTTPMiddleWareParams struct {
NeedToLogIncomingPostRequest bool
NeedToLogResponse bool
NeedToLogPanic bool
}
type Middleware interface {
HTTPMiddlewareWithParams(next http.Handler) http.Handler
}
type middleware struct {
params *HTTPMiddleWareParams
}
func NewHTTPMiddleware(params *HTTPMiddleWareParams) Middleware {
return &middleware{params: params}
}
// HTTPMiddlewareWithParams http middleware для записи логов запросов,
// параметры логирования определяются HTTPMiddleWareParams.
func (m *middleware) HTTPMiddlewareWithParams(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if m.params.NeedToLogPanic {
defer func() {
if rr := recover(); rr != nil {
stack := debug.Stack()
pc, file, line, _ := runtime.Caller(4)
Logger(r.Context()).Warn("panic happened",
zap.String("recover-error", fmt.Sprint(rr)),
zap.String("file", file),
zap.Int("line", line),
zap.String("function", runtime.FuncForPC(pc).Name()),
zap.ByteString("stack", stack),
zap.String("stacktrace", string(debug.Stack())),
zap.Int(statusCodeKey, 500),
types.URL("url", r.RequestURI),
zap.String("method", r.Method),
zap.String("remote-addr", r.RemoteAddr),
zap.String("user-agent", r.Header.Get("User-Agent")),
zap.String("header_expires_at", r.Header.Get("X-Expires-At")))
panic(r)
}
}()
}
var timeStart time.Time
if m.params.NeedToLogResponse {
timeStart = time.Now()
}
requestID := r.Header.Get(headerXRequestID)
if requestID == "" {
requestID = uuid.New().String()
r.Header.Add(headerXRequestID, requestID) // keep compatibility with old logger
}
w.Header().Set(headerXRequestID, requestID)
ctx := SetRequestIDCtx(r.Context(), requestID)
if m.params.NeedToLogIncomingPostRequest && isPostWithContent(r) {
Logger(r.Context()).Info("incoming POST request",
types.URL("url", r.RequestURI),
zap.String(requestUnitKey, r.Header.Get(headerXRequestUnit)),
zap.String(requestServiceKey, r.Header.Get(headerXRequestService)),
zap.String(requestBodyKey, getRequestBodyCopy(r)),
zap.Int64("request-length", r.ContentLength),
zap.String("method", r.Method),
zap.String("remote-addr", r.RemoteAddr),
zap.String(requestUserAgentKey, r.Header.Get("User-Agent")),
zap.String("header_expires_at", r.Header.Get("X-Expires-At")))
}
sw := &statusCodeWriter{ResponseWriter: w}
*r = *r.WithContext(ctx)
next.ServeHTTP(sw, r)
if m.params.NeedToLogResponse {
Logger(r.Context()).Info("request completed",
zap.Float64("timing", time.Since(timeStart).Seconds()),
zap.Int(statusCodeKey, sw.statusCode),
types.URL("url", r.RequestURI),
zap.String(requestUnitKey, r.Header.Get(headerXRequestUnit)),
zap.String(requestServiceKey, r.Header.Get(headerXRequestService)),
zap.String(requestBodyKey, getRequestBodyCopy(r)),
zap.String("method", r.Method),
zap.String("remote-addr", r.RemoteAddr),
zap.String("user-agent", r.Header.Get("User-Agent")),
zap.String("header_expires_at", r.Header.Get("X-Expires-At")))
}
})
}
type loggerFormat struct {
logRequestFunc func(req *http.Request)
logResponseFunc func(context.Context, *http.Response, time.Time)
}
func HTTPClientLogging(hc *http.Client, options ...Option) {
lf := &loggerFormat{
logRequestFunc: defaultLogRequest,
logResponseFunc: defaultLogResponse,
}
for _, option := range options {
lf = option(lf)
}
if hc.Transport == nil {
hc.Transport = http.DefaultTransport
}
hc.Transport = &loggerTransport{
transport: hc.Transport,
logRequestFunc: lf.logRequestFunc,
logResponseFunc: lf.logResponseFunc,
}
}
// loggerTransport implements http.RoundTripper.
// When set as Transport of http.Client, it executes HTTP requests with logging.
// No field is mandatory.
type loggerTransport struct {
transport http.RoundTripper
logRequestFunc func(req *http.Request)
logResponseFunc func(context.Context, *http.Response, time.Time)
}
// Used if loggerTransport.logRequestFunc is not set.
var defaultLogRequest = func(r *http.Request) {
Logger(r.Context()).Info("external request",
zap.String(methodKey, r.Method),
types.URL(urlKey, r.URL.String()),
types.JSON(requestBodyKey, getRequestBodyCopy(r)),
zap.Int64(requestLengthKey, r.ContentLength))
}
// Used if loggerTransport.logResponseFunc is not set.
var defaultLogResponse = func(ctx context.Context, r *http.Response, begin time.Time) {
Logger(ctx).Info("external response",
zap.String(methodKey, r.Request.Method),
zap.Int(statusCodeKey, r.StatusCode),
types.URL(urlKey, r.Request.URL.String()),
types.JSON(responseBodyKey, getResponseBodyCopy(r)),
zap.Float64(durationKey, time.Since(begin).Seconds()),
)
}
// ExternalFormRequestLogger hides sensitive data from request.
// hides from url.
// hides from form values.
func ExternalFormRequestLogger() func(r *http.Request) {
return func(r *http.Request) {
Logger(r.Context()).Info("external request",
zap.String(methodKey, r.Method),
types.URL(urlKey, r.URL.String()),
types.URL(requestBodyKey, getRequestBodyCopy(r)),
zap.Int64(requestLengthKey, r.ContentLength))
}
}
// RoundTrip is the core part of this module and implements http.RoundTripper.
// Executes HTTP request with request/response logging.
func (lt *loggerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
lt.logRequest(req)
begin := time.Now()
resp, err := lt.transport.RoundTrip(req)
if err != nil {
return nil, err
}
lt.logResponse(req.Context(), resp, begin)
return resp, err
}
func (lt *loggerTransport) logRequest(req *http.Request) {
lt.logRequestFunc(req)
}
func (lt *loggerTransport) logResponse(ctx context.Context, resp *http.Response, begin time.Time) {
lt.logResponseFunc(ctx, resp, begin)
}
func GRPCUnaryServerInterceptor(
ctx context.Context,
req interface{},
_ *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
var requestID string
if headers, ok := metadata.FromIncomingContext(ctx); ok {
if header, ok := headers["x-request-id"]; ok && len(header) > 0 {
requestID = header[0]
}
}
if requestID == "" {
requestID = uuid.New().String()
}
ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs("x-request-id", requestID))
ctx = SetRequestIDCtx(ctx, requestID)
return handler(ctx, req)
}
func GRPCUnaryClientInterceptor(
ctx context.Context,
method string,
req interface{},
reply interface{},
cc *grpc.ClientConn,
invoker grpc.UnaryInvoker,
opts ...grpc.CallOption,
) error {
var requestID string
requestID = GetRequestIDCtx(ctx)
if requestID == "" {
requestID = uuid.New().String()
}
ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs("x-request-id", requestID))
err := invoker(ctx, method, req, reply, cc, opts...)
return err
}
func getRequestBodyCopy(r *http.Request) string {
if r.Body == nil {
return ""
}
var reqBody []byte
reqBody, _ = io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewBuffer(reqBody))
return string(reqBody)
}
func isPostWithContent(r *http.Request) bool {
return r.Method == http.MethodPost && r.ContentLength != 0
}
func getResponseBodyCopy(r *http.Response) string {
if r.Body == nil {
return ""
}
var reqBody []byte
reqBody, _ = io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewBuffer(reqBody))
return string(reqBody)
}
func GRPCLoggingInterceptor(ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler,
) (
interface{}, error,
) {
begin := time.Now()
logMethodBefore(ctx, info.FullMethod, req)
res, err := handler(ctx, req)
logMethodAfter(ctx, info.FullMethod, begin, res, err)
return res, err
}
func logMethodAfter(ctx context.Context, path string, begin time.Time, resp interface{}, err error) {
parts := strings.Split(path, "/")
methodName := parts[len(parts)-1]
respRaw, respErr := json.Marshal(resp)
if respErr != nil {
Logger(ctx).Warn("logMethodAfter err:", zap.Error(respErr))
}
fields := []zap.Field{
zap.String("method", methodName),
types.JSON("response", string(respRaw)),
zap.Float64("duration", time.Since(begin).Seconds()),
}
if err != nil {
fields = append(fields, zap.String("error", err.Error()))
}
Logger(ctx).Info("finished", fields...)
}
func logMethodBefore(ctx context.Context, path string, req interface{}) {
parts := strings.Split(path, "/")
methodName := parts[len(parts)-1]
reqRaw, err := json.Marshal(req)
if err != nil {
Logger(ctx).Warn("logMethodBefore err:", zap.Error(err))
}
Logger(ctx).Info("enter", zap.String("method", methodName), types.JSON("request", string(reqRaw)))
}

49
options.go

@ -0,0 +1,49 @@
package logger
import (
"context"
"net/http"
"time"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
type ConfigOption func(cfg zap.Config) zap.Config
func WithStringCasting() ConfigOption {
const encodingName = "json_string_casting"
return func(cfg zap.Config) zap.Config {
cfg.Encoding = encodingName
_ = zap.RegisterEncoder(encodingName, func(config zapcore.EncoderConfig) (zapcore.Encoder, error) {
encoder := newStringCastingEncoder(config)
return encoder, nil
})
return cfg
}
}
func WithOutputPaths(paths []string) ConfigOption {
return func(cfg zap.Config) zap.Config {
cfg.OutputPaths = paths
return cfg
}
}
type Option func(lf *loggerFormat) *loggerFormat
func WithLogRequestFunc(l func(req *http.Request)) Option {
return func(lf *loggerFormat) *loggerFormat {
lf.logRequestFunc = l
return lf
}
}
func WithLogResponseFunc(l func(context.Context, *http.Response, time.Time)) Option {
return func(lf *loggerFormat) *loggerFormat {
lf.logResponseFunc = l
return lf
}
}

766
string_casting_json_encoder.go

@ -0,0 +1,766 @@
package logger
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"math"
"sync"
"time"
"unicode/utf8"
"go.uber.org/zap"
"go.uber.org/zap/buffer"
"go.uber.org/zap/zapcore"
)
const _hex = "0123456789abcdef"
var (
_ zapcore.Encoder = (*stringCastingJSONEncoder)(nil)
_jsonPool = sync.Pool{New: func() interface{} {
return &stringCastingJSONEncoder{}
}}
nullLiteralBytes = []byte("null")
)
func putJSONEncoder(enc *stringCastingJSONEncoder) {
if enc.reflectBuf != nil {
enc.reflectBuf.Free()
}
enc.EncoderConfig = nil
enc.buf = nil
enc.spaced = false
enc.openNamespaces = 0
enc.reflectBuf = nil
enc.reflectEnc = nil
_jsonPool.Put(enc)
}
func addFields(enc zapcore.ObjectEncoder, fields []zap.Field) {
for i := range fields {
fields[i].AddTo(enc)
}
}
func fullNameEncoder(loggerName string, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString(loggerName)
}
func getJSONEncoder() *stringCastingJSONEncoder {
encoder, _ := _jsonPool.Get().(*stringCastingJSONEncoder)
return encoder
}
func defaultReflectedEncoder(w io.Writer) zapcore.ReflectedEncoder {
enc := json.NewEncoder(w)
// For consistency with our custom JSON encoder.
enc.SetEscapeHTML(false)
return enc
}
type stringCastingJSONEncoder struct {
*zapcore.EncoderConfig
buf *buffer.Buffer
bufPool buffer.Pool
spaced bool
openNamespaces int
escape bool
reflectBuf *buffer.Buffer
reflectEnc zapcore.ReflectedEncoder
}
func (enc *stringCastingJSONEncoder) AddArray(key string, arr zapcore.ArrayMarshaler) error {
enc.addKey(key)
return enc.AppendArray(arr)
}
func (enc *stringCastingJSONEncoder) AddObject(key string, obj zapcore.ObjectMarshaler) error {
enc.addKey(key)
return enc.AppendObject(obj)
}
func (enc *stringCastingJSONEncoder) AddBinary(key string, val []byte) {
enc.AddString(key, base64.StdEncoding.EncodeToString(val))
}
func (enc *stringCastingJSONEncoder) AddByteString(key string, val []byte) {
enc.addKey(key)
enc.AppendByteString(val)
}
func (enc *stringCastingJSONEncoder) AddBool(key string, val bool) {
enc.addKey(key)
enc.AppendBool(val)
}
func (enc *stringCastingJSONEncoder) AddComplex128(key string, val complex128) {
enc.addKey(key)
enc.AppendComplex128(val)
}
func (enc *stringCastingJSONEncoder) AddComplex64(key string, val complex64) {
enc.addKey(key)
enc.AppendComplex64(val)
}
func (enc *stringCastingJSONEncoder) AddDuration(key string, val time.Duration) {
enc.addKey(key)
enc.AppendDuration(val)
}
func (enc *stringCastingJSONEncoder) AddFloat64(key string, val float64) {
enc.addKey(key)
enc.AppendFloat64(val)
}
func (enc *stringCastingJSONEncoder) AddFloat32(key string, val float32) {
enc.addKey(key)
enc.AppendFloat32(val)
}
func (enc *stringCastingJSONEncoder) AddInt(k string, v int) { enc.AddInt64(k, int64(v)) }
func (enc *stringCastingJSONEncoder) AddInt32(k string, v int32) { enc.AddInt64(k, int64(v)) }
func (enc *stringCastingJSONEncoder) AddInt16(k string, v int16) { enc.AddInt64(k, int64(v)) }
func (enc *stringCastingJSONEncoder) AddInt8(k string, v int8) { enc.AddInt64(k, int64(v)) }
func (enc *stringCastingJSONEncoder) AddUint(k string, v uint) { enc.AddUint64(k, uint64(v)) }
func (enc *stringCastingJSONEncoder) AddUint32(k string, v uint32) { enc.AddUint64(k, uint64(v)) }
func (enc *stringCastingJSONEncoder) AddUint16(k string, v uint16) { enc.AddUint64(k, uint64(v)) }
func (enc *stringCastingJSONEncoder) AddUint8(k string, v uint8) { enc.AddUint64(k, uint64(v)) }
func (enc *stringCastingJSONEncoder) AddUintptr(k string, v uintptr) { enc.AddUint64(k, uint64(v)) }
func (enc *stringCastingJSONEncoder) AddInt64(key string, val int64) {
enc.addKey(key)
enc.AppendInt64(val)
}
func (enc *stringCastingJSONEncoder) AddString(key, val string) {
enc.addKey(key)
enc.AppendString(val)
}
func (enc *stringCastingJSONEncoder) AddTime(key string, val time.Time) {
enc.addKey(key)
enc.AppendTime(val)
}
func (enc *stringCastingJSONEncoder) AddUint64(key string, val uint64) {
enc.addKey(key)
enc.AppendUint64(val)
}
func (enc *stringCastingJSONEncoder) AddReflected(key string, obj interface{}) error {
valueBytes, err := enc.encodeReflected(obj)
if err != nil {
return err
}
enc.addKey(key)
if enc.escape {
_, err = enc.buf.Write([]byte(`\"`))
} else {
_, err = enc.buf.Write([]byte(`"`))
}
if err != nil {
return err
}
_, err = enc.buf.Write(valueBytes)
if err != nil {
return err
}
if enc.escape {
_, err = enc.buf.Write([]byte(`\"`))
} else {
_, err = enc.buf.Write([]byte(`"`))
}
return err
}
func (enc *stringCastingJSONEncoder) OpenNamespace(key string) {
enc.addKey(key)
enc.buf.AppendByte('{')
enc.openNamespaces++
}
func (enc *stringCastingJSONEncoder) Clone() zapcore.Encoder {
clone := enc.clone()
_, _ = clone.buf.Write(enc.buf.Bytes())
return clone
}
func (enc *stringCastingJSONEncoder) clone() *stringCastingJSONEncoder {
clone := getJSONEncoder()
clone.EncoderConfig = enc.EncoderConfig
clone.spaced = enc.spaced
clone.openNamespaces = enc.openNamespaces
clone.escape = enc.escape
clone.bufPool = buffer.NewPool() // строку не удалять. Магия. На этом все держится)
clone.buf = enc.bufPool.Get()
return clone
}
func (enc *stringCastingJSONEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
final := enc.clone()
final.buf.AppendByte('{')
if final.LevelKey != "" && final.EncodeLevel != nil {
final.addKey(final.LevelKey)
cur := final.buf.Len()
final.EncodeLevel(ent.Level, final)
if cur == final.buf.Len() {
// User-supplied EncodeLevel was a no-op. Fall back to strings to keep
// output JSON valid.
final.AppendString(ent.Level.String())
}
}
if final.TimeKey != "" {
final.AddTime(final.TimeKey, ent.Time)
}
if ent.LoggerName != "" && final.NameKey != "" {
final.addKey(final.NameKey)
cur := final.buf.Len()
nameEncoder := final.EncodeName
// if no name encoder provided, fall back to fullNameEncoder for backwards
// compatibility
if nameEncoder == nil {
nameEncoder = fullNameEncoder
}
nameEncoder(ent.LoggerName, final)
if cur == final.buf.Len() {
// User-supplied EncodeName was a no-op. Fall back to strings to
// keep output JSON valid.
final.AppendString(ent.LoggerName)
}
}
if ent.Caller.Defined {
if final.CallerKey != "" {
final.addKey(final.CallerKey)
cur := final.buf.Len()
final.EncodeCaller(ent.Caller, final)
if cur == final.buf.Len() {
// User-supplied EncodeCaller was a no-op. Fall back to strings to
// keep output JSON valid.
final.AppendString(ent.Caller.String())
}
}
if final.FunctionKey != "" {
final.addKey(final.FunctionKey)
final.AppendString(ent.Caller.Function)
}
}
if final.MessageKey != "" {
final.addKey(enc.MessageKey)
final.AppendString(ent.Message)
}
if enc.buf.Len() > 0 {
final.addElementSeparator()
_, _ = final.buf.Write(enc.buf.Bytes())
}
addFields(final, fields)
final.closeOpenNamespaces()
if ent.Stack != "" && final.StacktraceKey != "" {
final.AddString(final.StacktraceKey, ent.Stack)
}
final.buf.AppendByte('}')
final.buf.AppendString(final.LineEnding)
ret := final.buf
putJSONEncoder(final)
return ret, nil
}
func (enc *stringCastingJSONEncoder) AppendArray(arr zapcore.ArrayMarshaler) error {
needResetEscape := false
enc.addElementSeparator()
if enc.escape {
enc.buf.AppendByte('\\')
} else {
needResetEscape = true
}
enc.escape = true
enc.buf.AppendByte('"')
enc.buf.AppendByte('[')
err := arr.MarshalLogArray(enc)
enc.buf.AppendByte(']')
if enc.escape && !needResetEscape {
enc.buf.AppendByte('\\')
}
enc.buf.AppendByte('"')
if needResetEscape {
enc.escape = false
}
return err
}
func (enc *stringCastingJSONEncoder) AppendObject(obj zapcore.ObjectMarshaler) error {
needResetEscape := false
// Close ONLY new openNamespaces that are created during
// AppendObject().
old := enc.openNamespaces
enc.openNamespaces = 0
enc.addElementSeparator()
if enc.escape {
enc.buf.AppendByte('\\')
} else {
needResetEscape = true
}
enc.escape = true
enc.buf.AppendByte('"')
enc.buf.AppendByte('{')
err := obj.MarshalLogObject(enc)
enc.buf.AppendByte('}')
if enc.escape && !needResetEscape {
enc.buf.AppendByte('\\')
}
enc.buf.AppendByte('"')
if needResetEscape {
enc.escape = false
}
enc.closeOpenNamespaces()
enc.openNamespaces = old
return err
}
func (enc *stringCastingJSONEncoder) AppendBool(val bool) {
enc.addElementSeparator()
if enc.escape {
enc.buf.AppendByte('\\')
}
enc.buf.AppendByte('"')
enc.buf.AppendBool(val)
if enc.escape {
enc.buf.AppendByte('\\')
}
enc.buf.AppendByte('"')
}
func (enc *stringCastingJSONEncoder) AppendByteString(val []byte) {
enc.addElementSeparator()
if enc.escape {
enc.buf.AppendByte('\\')
}
enc.buf.AppendByte('"')
enc.safeAddByteString(val)
if enc.escape {
enc.buf.AppendByte('\\')
}
enc.buf.AppendByte('"')
}
func (enc *stringCastingJSONEncoder) safeAddByteString(s []byte) {
for i := 0; i < len(s); {
if enc.tryAddRuneSelf(s[i]) {
i++
continue
}
r, size := utf8.DecodeRune(s[i:])
if enc.tryAddRuneError(r, size) {
i++
continue
}
_, _ = enc.buf.Write(s[i : i+size])
i += size
}
}
func (enc *stringCastingJSONEncoder) tryAddRuneError(r rune, size int) bool {
if r == utf8.RuneError && size == 1 {
enc.buf.AppendString(`\ufffd`)
return true
}
return false
}
func (enc *stringCastingJSONEncoder) appendComplex(val complex128, precision int) {
enc.addElementSeparator()
// Cast to a platform-independent, fixed-size type.
r, i := float64(real(val)), float64(imag(val)) // nolint
if enc.escape {
enc.buf.AppendByte('\\')
}
enc.buf.AppendByte('"')
// Because we're always in a quoted string, we can use strconv without
// special-casing NaN and +/-Inf.
enc.buf.AppendFloat(r, precision)
// If imaginary part is less than 0, minus (-) sign is added by default
// by AppendFloat.
if i >= 0 {
enc.buf.AppendByte('+')
}
enc.buf.AppendFloat(i, precision)
enc.buf.AppendByte('i')
if enc.escape {
enc.buf.AppendByte('\\')
}
enc.buf.AppendByte('"')
}
func (enc *stringCastingJSONEncoder) AppendDuration(val time.Duration) {
cur := enc.buf.Len()
if cur == enc.buf.Len() {
// User-supplied EncodeDuration is a no-op. Fall back to nanoseconds to keep
// JSON valid.
enc.AppendString(fmt.Sprintf("%d", int64(val)))
}
}
func (enc *stringCastingJSONEncoder) AppendInt64(val int64) {
enc.addElementSeparator()
if enc.escape {
enc.buf.AppendByte('\\')
}
enc.buf.AppendByte('"')
enc.buf.AppendInt(val)
if enc.escape {
enc.buf.AppendByte('\\')
}
enc.buf.AppendByte('"')
}
func (enc *stringCastingJSONEncoder) encodeReflected(obj interface{}) ([]byte, error) {
if obj == nil {
return nullLiteralBytes, nil
}
enc.resetReflectBuf()
if err := enc.reflectEnc.Encode(obj); err != nil {
return nil, err
}
enc.reflectBuf.TrimNewline()
return enc.reflectBuf.Bytes(), nil
}
func (enc *stringCastingJSONEncoder) resetReflectBuf() {
if enc.reflectBuf == nil {
enc.reflectBuf = enc.bufPool.Get()
enc.reflectEnc = enc.NewReflectedEncoder(enc.reflectBuf)
} else {
enc.reflectBuf.Reset()
}
}
func (enc *stringCastingJSONEncoder) AppendReflected(val interface{}) error {
valueBytes, err := enc.encodeReflected(val)
if err != nil {
return err
}
enc.addElementSeparator()
_, err = enc.buf.Write(valueBytes)
return err
}
func (enc *stringCastingJSONEncoder) AppendString(val string) {
enc.addElementSeparator()
if enc.escape {
enc.buf.AppendByte('\\')
}
enc.buf.AppendByte('"')
enc.safeAddString(val)
if enc.escape {
enc.buf.AppendByte('\\')
}
enc.buf.AppendByte('"')
}
func (enc *stringCastingJSONEncoder) AppendTimeLayout(time time.Time, layout string) {
enc.addElementSeparator()
if enc.escape {
enc.buf.AppendByte('\\')
}
enc.buf.AppendByte('"')
enc.buf.AppendTime(time, layout)
if enc.escape {
enc.buf.AppendByte('\\')
}
enc.buf.AppendByte('"')
}
func (enc *stringCastingJSONEncoder) AppendTime(val time.Time) {
cur := enc.buf.Len()
if e := enc.EncodeTime; e != nil {
e(val, enc)
}
if cur == enc.buf.Len() {
if enc.escape {
enc.buf.AppendByte('\\')
}
enc.buf.AppendByte('"')
// User-supplied EncodeTime is a no-op. Fall back to nanos since epoch to keep
// output JSON valid.
enc.buf.AppendString(fmt.Sprintf("%d", val.UnixNano()))
if enc.escape {
enc.buf.AppendByte('\\')
}
enc.buf.AppendByte('"')
}
}
func (enc *stringCastingJSONEncoder) AppendUint64(val uint64) {
enc.addElementSeparator()
if enc.escape {
enc.buf.AppendByte('\\')
}
enc.buf.AppendByte('"')
enc.buf.AppendUint(val)
if enc.escape {
enc.buf.AppendByte('\\')
}
enc.buf.AppendByte('"')
}
func (enc *stringCastingJSONEncoder) AppendComplex64(v complex64) {
enc.appendComplex(complex128(v), 32)
}
func (enc *stringCastingJSONEncoder) AppendComplex128(v complex128) {
enc.appendComplex(complex128(v), 64)
}
func (enc *stringCastingJSONEncoder) AppendFloat64(v float64) { enc.appendFloat(v, 64) }
func (enc *stringCastingJSONEncoder) AppendFloat32(v float32) { enc.appendFloat(float64(v), 32) }
func (enc *stringCastingJSONEncoder) AppendInt(v int) { enc.AppendInt64(int64(v)) }
func (enc *stringCastingJSONEncoder) AppendInt32(v int32) { enc.AppendInt64(int64(v)) }
func (enc *stringCastingJSONEncoder) AppendInt16(v int16) { enc.AppendInt64(int64(v)) }
func (enc *stringCastingJSONEncoder) AppendInt8(v int8) { enc.AppendInt64(int64(v)) }
func (enc *stringCastingJSONEncoder) AppendUint(v uint) { enc.AppendUint64(uint64(v)) }
func (enc *stringCastingJSONEncoder) AppendUint32(v uint32) { enc.AppendUint64(uint64(v)) }
func (enc *stringCastingJSONEncoder) AppendUint16(v uint16) { enc.AppendUint64(uint64(v)) }
func (enc *stringCastingJSONEncoder) AppendUint8(v uint8) { enc.AppendUint64(uint64(v)) }
func (enc *stringCastingJSONEncoder) AppendUintptr(v uintptr) { enc.AppendUint64(uint64(v)) }
func (enc *stringCastingJSONEncoder) tryAddRuneSelf(b byte) bool {
if b >= utf8.RuneSelf {
return false
}
if 0x20 <= b && b != '\\' && b != '"' {
enc.buf.AppendByte(b)
return true
}
switch b {
case '\\', '"':
enc.buf.AppendByte('\\')
enc.buf.AppendByte(b)
case '\n':
enc.buf.AppendByte('\\')
enc.buf.AppendByte('n')
case '\r':
enc.buf.AppendByte('\\')
enc.buf.AppendByte('r')
case '\t':
enc.buf.AppendByte('\\')
enc.buf.AppendByte('t')
default:
// Encode bytes < 0x20, except for the escape sequences above.
enc.buf.AppendString(`\u00`)
enc.buf.AppendByte(_hex[b>>4])
enc.buf.AppendByte(_hex[b&0xF])
}
return true
}
func (enc *stringCastingJSONEncoder) appendFloat(val float64, bitSize int) {
enc.addElementSeparator()
switch {
case math.IsNaN(val):
enc.buf.AppendString(`"NaN"`)
case math.IsInf(val, 1):
enc.buf.AppendString(`"+Inf"`)
case math.IsInf(val, -1):
enc.buf.AppendString(`"-Inf"`)
default:
if enc.escape {
enc.buf.AppendByte('\\')
}
enc.buf.AppendByte('"')
enc.buf.AppendFloat(val, bitSize)
if enc.escape {
enc.buf.AppendByte('\\')
}
enc.buf.AppendByte('"')
}
}
func (enc *stringCastingJSONEncoder) safeAddString(s string) {
for i := 0; i < len(s); {
if enc.tryAddRuneSelf(s[i]) {
i++
continue
}
r, size := utf8.DecodeRuneInString(s[i:])
if enc.tryAddRuneError(r, size) {
i++
continue
}
enc.buf.AppendString(s[i : i+size])
i += size
}
}
func (enc *stringCastingJSONEncoder) addKey(key string) {
enc.addElementSeparator()
if enc.escape {
enc.buf.AppendByte('\\')
}
enc.buf.AppendByte('"')
enc.safeAddString(key)
if enc.escape {
enc.buf.AppendByte('\\')
}
enc.buf.AppendByte('"')
enc.buf.AppendByte(':')
if enc.spaced {
enc.buf.AppendByte(' ')
}
}
func (enc *stringCastingJSONEncoder) addElementSeparator() {
last := enc.buf.Len() - 1
if last < 0 {
return
}
switch enc.buf.Bytes()[last] {
case '{', '[', ':', ',', ' ':
return
default:
enc.buf.AppendByte(',')
if enc.spaced {
enc.buf.AppendByte(' ')
}
}
}
func (enc *stringCastingJSONEncoder) closeOpenNamespaces() {
for i := 0; i < enc.openNamespaces; i++ {
enc.buf.AppendByte('}')
}
enc.openNamespaces = 0
}
func newStringCastingEncoder(cfg zapcore.EncoderConfig) *stringCastingJSONEncoder {
if cfg.NewReflectedEncoder == nil {
cfg.NewReflectedEncoder = defaultReflectedEncoder
}
bp := buffer.NewPool()
enc := &stringCastingJSONEncoder{
bufPool: bp,
buf: bp.Get(),
spaced: true,
}
enc.EncoderConfig = &cfg
return enc
}

108
string_casting_json_encoder_test.go

@ -0,0 +1,108 @@
package logger
import (
"context"
"encoding/json"
"io"
"os"
"testing"
"time"
"git.lowcodeplatform.net/packages/logger/types"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
type TestStringCastingObjMarsh struct {
Name string
Obj *TestStringCastingObjMarsh
}
func (o TestStringCastingObjMarsh) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("name", o.Name)
if o.Obj != nil {
_ = encoder.AddObject("obj", o.Obj)
}
return nil
}
type TestStringCastingJSONEncoderObjMarshArr []*TestStringCastingObjMarsh
func (f TestStringCastingJSONEncoderObjMarshArr) MarshalLogArray(enc zapcore.ArrayEncoder) error {
for _, o := range f {
_ = enc.AppendObject(o)
}
return nil
}
func TestStringCastingJSONEncoder(t *testing.T) {
logFile, err := os.CreateTemp(os.TempDir(), "test-string-casting")
if err != nil {
t.Error(err)
}
ctx := context.Background()
ctx = SetRequestIDCtx(ctx, "test-string-casting")
obj := &TestStringCastingObjMarsh{Name: "Sam", Obj: &TestStringCastingObjMarsh{Name: "John"}}
SetupDefaultLogger(
"test-namespace",
WithStringCasting(),
WithOutputPaths([]string{logFile.Name()}),
)
Logger(ctx).Info("test message",
zap.Int("int", 4),
zap.Int8("int8", 4),
zap.Int16("int16", 4),
zap.Int32("int32", 4),
zap.Int64("int64", 4),
zap.Uint("uint", 4),
zap.Uint8("uint8", 4),
zap.Uint16("uint16", 4),
zap.Uint32("uint32", 4),
zap.Uint64("uint64", 4),
zap.Float32("float32", 4.2),
zap.Float64("float64", 4.2),
zap.String("string", "string"),
zap.Bool("bool_true", true),
zap.Bool("bool_false", false),
zap.Any("anyInt", 1),
zap.Any("anyObj", obj),
zap.Object("object", obj),
zap.Times("times", []time.Time{time.Now(), time.Now()}),
zap.Array("array", TestStringCastingJSONEncoderObjMarshArr{obj, obj}),
zap.Time("time", time.Now()),
zap.ByteString("byte_string", []byte("abc")),
zap.Duration("duration", time.Second*10),
types.JSON("json", `{"key": "value"}`),
types.URL("url", `https://some.url/path?password=123`),
types.StringMap("string_map", map[string]string{"first": "first_val", "second": "second_val"}),
)
data, err := io.ReadAll(logFile)
if err != nil {
t.Error(err)
}
if !json.Valid(data) {
t.Error("result is not valid json")
}
}

242
types/json.go

@ -0,0 +1,242 @@
package types
import (
"encoding/json"
"fmt"
"io"
"strings"
"github.com/pkg/errors"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// JSON предоставляет тип данных для логирование JSON-строк через zap-логгер с учетом
// маскирования полей с приватными данными.
func JSON(key string, val string) zap.Field {
hashedVal, err := MaskSensitiveJSONFields(val, nil, nil, nil)
if err != nil {
hashedVal = fmt.Sprintf(`{"error": "%s", "val": "%s"}`,
strings.ReplaceAll(val, `"`, `\"`),
strings.ReplaceAll(err.Error(), `"`, `\"`),
)
}
return zap.Field{Key: key, Type: zapcore.StringType, String: hashedVal, Integer: 0, Interface: nil}
}
const (
unknown = iota
object
objectEnd
fieldName
fieldValue
array
arrayValue
arrayEnd
)
// MaskSensitiveJSONFields маскирует приватные данные в JSON-строках.
func MaskSensitiveJSONFields(jsonString string, excludeKeys, hideKeys, excludeValues []string) (string, error) {
decoder := json.NewDecoder(strings.NewReader(jsonString))
decoder.UseNumber()
strBuilder := new(strings.Builder)
var (
token json.Token
stateStack intStack
lastFieldName string
err error
)
state := unknown
beforeNextValue := func() error {
switch state {
case fieldValue:
if err := strBuilder.WriteByte(','); err != nil {
return errors.Wrap(err, "logger: json mask encoder")
}
state = fieldName
case fieldName:
if err := strBuilder.WriteByte(':'); err != nil {
return errors.Wrap(err, "logger: json mask encoder")
}
state = fieldValue
case arrayValue:
if err := strBuilder.WriteByte(','); err != nil {
return errors.Wrap(err, "logger: json mask encoder")
}
case array:
state = arrayValue
case object:
state = fieldName
}
return nil
}
pushState := func() error {
switch state {
case object, fieldValue:
stateStack = stateStack.push(fieldValue)
case array, arrayValue:
stateStack = stateStack.push(arrayValue)
case unknown:
stateStack = stateStack.push(unknown)
default:
return fmt.Errorf("logger: json mask encoder: invalid state (json is invalid?): %d", state)
}
return nil
}
setState := func(newState int) error {
switch newState {
case object:
if err := beforeNextValue(); err != nil {
return err
}
if err := strBuilder.WriteByte('{'); err != nil {
return errors.Wrap(err, "logger: json mask encoder")
}
if err := pushState(); err != nil {
return err
}
state = newState
case objectEnd:
if err := strBuilder.WriteByte('}'); err != nil {
return errors.Wrap(err, "logger: json mask encoder")
}
stateStack, state = stateStack.pop()
case array:
if err := beforeNextValue(); err != nil {
return err
}
if err := strBuilder.WriteByte('['); err != nil {
return errors.Wrap(err, "logger: json mask encoder")
}
if err := pushState(); err != nil {
return err
}
state = newState
case arrayEnd:
if err := strBuilder.WriteByte(']'); err != nil {
return errors.Wrap(err, "logger: json mask encoder")
}
stateStack, state = stateStack.pop()
}
return nil
}
excludeKeys = append(excludeKeys, defaultExcludeKeys...)
hideKeys = append(hideKeys, defaultHideKeys...)
for {
token, err = decoder.Token()
if err != nil {
break
}
if v, ok := token.(json.Delim); ok {
switch v.String() {
case "{":
if err := setState(object); err != nil {
return "", err
}
case "}":
if err := setState(objectEnd); err != nil {
return "", err
}
case "[":
if err := setState(array); err != nil {
return "", err
}
case "]":
if err := setState(arrayEnd); err != nil {
return "", err
}
}
} else {
if err := beforeNextValue(); err != nil {
return "", err
}
var jsonBytes []byte
switch dataType := token.(type) {
case string:
currentFieldName := ""
if state == fieldName {
lastFieldName = dataType
} else if state == fieldValue {
currentFieldName = lastFieldName
}
jsonBytes, err = json.Marshal(hashSensitiveValue(currentFieldName, dataType, excludeKeys, hideKeys, excludeValues))
default:
jsonBytes, err = json.Marshal(dataType)
}
if err != nil {
return "", errors.Wrap(err, "logger: json mask encoder")
}
if _, err = strBuilder.Write(jsonBytes); err != nil {
return "", errors.Wrap(err, "logger: json mask encoder")
}
}
}
if !errors.Is(err, io.EOF) {
return jsonString, nil
}
return strBuilder.String(), nil
}
func hashSensitiveValue(fieldName, src string, excludeKeys, hideKeys, excludeValues []string) string {
fieldName = strings.ToLower(fieldName)
for _, hideKey := range hideKeys {
if strings.Contains(fieldName, hideKey) {
return Hide(src)
}
}
for _, excludeKey := range excludeKeys {
if strings.Contains(fieldName, excludeKey) {
return Mask(src)
}
}
for _, excludeValue := range excludeValues {
if strings.HasPrefix(src, excludeValue) {
return Mask(src)
}
}
return src
}
type intStack []int
func (s intStack) push(v int) intStack {
return append(s, v)
}
func (s intStack) pop() (intStack, int) {
if len(s) == 0 {
return s, 0
}
l := len(s)
return s[:l-1], s[l-1]
}

55
types/json_test.go

@ -0,0 +1,55 @@
package types
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestMaskSensitiveJSONFields(t *testing.T) {
for _, testCase := range []struct {
name string
input string
expected string
}{
{
name: "empty string",
},
{
name: "null",
input: "null",
expected: "null",
},
{
name: "empty object",
input: "{}",
expected: "{}",
},
{
name: "wrong json",
input: "{",
expected: "{",
},
{
name: "wrong json 2",
input: `{"sd":abc или <html>Internal server Error</html>`,
expected: `{"sd":abc или <html>Internal server Error</html>`,
},
{
name: "with password",
input: `{"password": "foo", "bar": "baz"}`,
expected: `{"password":"---","bar":"baz"}`,
},
{
name: "with card",
input: `{"card": "1234123412341234", "bar": "baz"}`,
expected: `{"card":"1234----1234","bar":"baz"}`,
},
} {
t.Run(testCase.name, func(t *testing.T) {
actual, err := MaskSensitiveJSONFields(testCase.input, nil, nil, nil)
require.NoError(t, err)
require.Equal(t, testCase.expected, actual)
})
}
}

18
types/map_string.go

@ -0,0 +1,18 @@
package types
import (
"fmt"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func StringMap(key string, val map[string]string) zap.Field {
res := ""
for dataKey, dataValue := range val {
res += fmt.Sprintf("%s:%s;", dataKey, dataValue)
}
return zap.Field{Key: key, Type: zapcore.StringType, String: res, Integer: 0, Interface: nil}
}

11
types/tools.go

@ -0,0 +1,11 @@
package types
func ArrayContains(a []string, x string) bool {
for _, n := range a {
if x == n {
return true
}
}
return false
}

54
types/types.go

@ -0,0 +1,54 @@
package types
import (
"fmt"
"strings"
)
const (
longLenPan = 16
shortLenPan = 12
)
var defaultExcludeKeys = []string{
"email",
"card",
"first_name",
"firstname",
"last_name",
"lastname",
"cvc",
"cvc2",
"csc",
"csc2",
"pan",
"$pan",
"cardnumber",
"$cvc",
"$cvc2",
"pg_card_pan",
"pg_card_cvc",
"hpan",
}
var defaultHideKeys = []string{
"password",
"secret",
"client_secret",
"access_token",
}
func Mask(value string) string {
switch {
case len(value) > longLenPan:
return value[:6] + fmt.Sprintf("---(%d)---", len(value)-longLenPan) + value[len(value)-4:]
case len(value) > shortLenPan:
return value[:4] + strings.Repeat("-", len(value)-shortLenPan) + value[len(value)-4:]
default:
return Hide(value)
}
}
func Hide(value string) string {
return strings.Repeat("-", len(value))
}

38
types/types_test.go

@ -0,0 +1,38 @@
package types
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestMask(t *testing.T) {
for _, testCase := range []struct {
name string
source string
expected string
}{
{
name: "empty",
},
{
name: "more than 16",
source: "12345678901234567",
expected: "123456---(1)---4567",
},
{
name: "more than 12",
source: "1234567890123456",
expected: "1234----3456",
},
{
name: "less than 12",
source: "123456789012",
expected: "------------",
},
} {
t.Run(testCase.name, func(t *testing.T) {
require.Equal(t, testCase.expected, Mask(testCase.source))
})
}
}

88
types/url.go

@ -0,0 +1,88 @@
package types
import (
"fmt"
"net/url"
"strings"
"github.com/pkg/errors"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func URL(key string, val string) zap.Field {
hashedVal, err := MaskSensitiveURLFields(val, nil, nil)
if err != nil {
hashedVal = fmt.Sprintf(`{"error": "%s", "val": "%s"}`,
strings.ReplaceAll(val, `"`, `\"`),
strings.ReplaceAll(err.Error(), `"`, `\"`),
)
}
return zap.Field{Key: key, Type: zapcore.StringType, String: hashedVal, Integer: 0, Interface: nil}
}
func MaskSensitiveURLFields(urlString string, excludeKeys, hideKeys []string) (string, error) {
var (
query url.Values
parsed *url.URL
err error
)
isParamsOnly := !strings.Contains(urlString, "?") && strings.Contains(urlString, "=")
if isParamsOnly {
query, err = url.ParseQuery(urlString)
if err != nil {
return "", errors.Wrapf(err, "wrong formatted url: %s", urlString)
}
} else {
parsed, err = url.Parse(urlString)
if err != nil {
return "", errors.Wrapf(err, "wrong formatted url: %s", urlString)
}
query = parsed.Query()
}
excludeKeys = append(excludeKeys, defaultExcludeKeys...)
hideKeys = append(hideKeys, defaultHideKeys...)
for key, values := range query {
if ArrayContains(hideKeys, strings.ToLower(key)) {
query.Set(key, Hide(strings.Join(values, ",")))
} else if ArrayContains(excludeKeys, strings.ToLower(key)) {
query.Set(key, Mask(strings.Join(values, ",")))
}
}
if isParamsOnly {
return url.PathUnescape(query.Encode())
}
parsed.RawQuery = query.Encode()
return MaskFQDN(parsed.String())
}
func MaskFQDN(rawURL string) (string, error) {
const placeholder = "_password_"
maskedURL := ""
data, err := url.Parse(rawURL)
if err == nil && rawURL != "" {
_, ok := data.User.Password()
if ok {
user := data.User.Username()
data.User = url.UserPassword(user, placeholder)
} else {
data.User = nil
}
data.RawQuery = data.Query().Encode()
maskedURL = strings.Replace(data.String(), placeholder, "***", 1)
}
return maskedURL, err
}

77
types/url_test.go

@ -0,0 +1,77 @@
package types
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestMaskSensitiveURLFields(t *testing.T) {
for _, testCase := range []struct {
name string
input string
expected string
}{
{
name: "empty string",
},
{
name: "single host",
input: "localhost",
expected: "localhost",
},
{
name: "query",
input: "foo=bar",
expected: "foo=bar",
},
{
name: "host and query",
input: "localhost?foo=bar",
expected: "localhost?foo=bar",
},
{
name: "host and hidden query",
input: "localhost?password=bar",
expected: "localhost?password=---",
},
{
name: "host and card",
input: "localhost?card=1234123412341234",
expected: "localhost?card=1234----1234",
},
{
name: "postgres user and pass",
input: "postgres://user:password@host:5678/path?card=1234123412341234&password=bar",
expected: "postgres://user:***@host:5678/path?card=1234----1234&password=---",
},
} {
t.Run(testCase.name, func(t *testing.T) {
actual, err := MaskSensitiveURLFields(testCase.input, nil, nil)
require.NoError(t, err)
require.Equal(t, testCase.expected, actual)
})
}
}
var caseList = []struct {
Origin string
Masked string
}{
{
Origin: "postgres://user:password@host:5678/path?param1=value1#ancher",
Masked: "postgres://user:***@host:5678/path?param1=value1#ancher",
},
{
Origin: "",
Masked: "",
},
}
func TestMaskFQDN(t *testing.T) {
for _, c := range caseList {
actual, err := MaskFQDN(c.Origin)
require.NoError(t, err)
require.Equal(t, actual, c.Masked)
}
}

127
types/where.go

@ -0,0 +1,127 @@
package types
import (
"fmt"
"net/url"
"os"
"path/filepath"
"runtime"
"strings"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
type where struct {
FuncName string
Path string
Line int
Host string
Project string
}
func (w where) method() string {
return w.FuncName
}
func (w where) project() string {
return filepath.Join(w.Host, w.Project)
}
func (w where) path() string {
return fmt.Sprintf("%s:%d", w.Path, w.Line)
}
func (w where) MarshalLogObject(enc zapcore.ObjectEncoder) error {
data := map[string]string{
"method": w.method(),
"project": w.project(),
"path": w.path(),
}
for name, value := range data {
if value != "" {
enc.AddString(name, value)
}
}
return nil
}
var (
srcMark = filepath.Join("go", "src")
modMark = filepath.Join("go", "pkg", "mod")
goPathMark = filepath.Join("gopath", "src")
)
const separator = string(os.PathSeparator)
func WhereWithDeep(deep int) zap.Field {
field := zap.Skip()
var (
w where
pc uintptr
ok bool
)
pc, w.Path, w.Line, ok = runtime.Caller(deep)
if !ok {
return field
}
w.Path = strings.ReplaceAll(w.Path, separator, "/")
var ix int
if ix = strings.Index(w.Path, srcMark); ix > -1 {
w.Path = w.Path[ix+len(srcMark)+1:]
} else if ix = strings.Index(w.Path, modMark); ix > -1 {
w.Path = w.Path[ix+len(modMark)+1:]
} else if ix = strings.Index(w.Path, goPathMark); ix > -1 {
w.Path = w.Path[ix+len(goPathMark)+1:]
}
funcDetails := runtime.FuncForPC(pc)
if funcDetails == nil {
return field
}
funcFQN := funcDetails.Name()
funcParts := strings.Split(funcFQN, separator)
if len(funcParts) < 3 {
return field
}
w.FuncName = filepath.Join(funcParts[len(funcParts)-3:]...)
if !strings.HasPrefix(w.Path, "/") {
urlData, err := url.Parse("//" + w.Path)
if err != nil {
return field
}
if len(urlData.Path) == 0 {
return field
}
w.Path = urlData.Path[1:]
pathParts := strings.Split(w.Path, separator)
if len(pathParts) < 2 {
return field
}
w.Host = urlData.Host
w.Project = filepath.Join(pathParts[0:2]...)
}
return zapcore.Field{
Type: zapcore.InlineMarshalerType,
Interface: w,
}
}
func Where() zap.Field {
return WhereWithDeep(2)
}

38
types/where_test.go

@ -0,0 +1,38 @@
package types
import (
"strings"
"testing"
"go.uber.org/zap/zapcore"
)
func TestWhere(t *testing.T) {
field := Where()
if field.Type == zapcore.SkipType {
t.Fatal("unexpected zapcore field type: got zapcore.SkipType")
}
w, ok := field.Interface.(where)
if !ok {
t.Fatal("unexpected zapcore field type: can not convert to where structure")
}
const expectedMethod = "packages/logger/types.TestWhere"
if w.method() != expectedMethod {
t.Errorf("got unexpected path of the caller method:\n\texptected: %s\n\tgot: %s",
expectedMethod, w.method())
}
const expectedProject = "git.lowcodeplatform.net/packages"
if w.project() != expectedProject && w.project() != "" {
t.Errorf("got unexpected name of the caller project:\n\texptected: %s or empty value\n\tgot: %s",
expectedProject, w.project())
}
const expectedPath = "wbpay-go/packages/logger/types/where_test.go:11"
if !strings.HasSuffix(w.path(), expectedPath) {
t.Errorf("got unexpected path of the caller:\n\texptected: %s\n\tgot: %s",
expectedPath, w.path())
}
}

38
wrapper.go

@ -0,0 +1,38 @@
package logger
import (
"context"
"git.lowcodeplatform.net/packages/logger/types"
"go.uber.org/zap"
)
const callStackFramesDeep = 3
func prepareFields(fields ...zap.Field) []zap.Field {
return append([]zap.Field{types.WhereWithDeep(callStackFramesDeep)}, fields...)
}
func Debug(ctx context.Context, msg string, fields ...zap.Field) {
Logger(ctx).Debug(msg, prepareFields(fields...)...)
}
func Info(ctx context.Context, msg string, fields ...zap.Field) {
Logger(ctx).Info(msg, prepareFields(fields...)...)
}
func Warn(ctx context.Context, msg string, fields ...zap.Field) {
Logger(ctx).Warn(msg, prepareFields(fields...)...)
}
func Error(ctx context.Context, msg string, fields ...zap.Field) {
Logger(ctx).Error(msg, prepareFields(fields...)...)
}
func Panic(ctx context.Context, msg string, fields ...zap.Field) {
Logger(ctx).Panic(msg, prepareFields(fields...)...)
}
func Fatal(ctx context.Context, msg string, fields ...zap.Field) {
Logger(ctx).Fatal(msg, prepareFields(fields...)...)
}
Loading…
Cancel
Save