loveckiy.ivan
11 months ago
27 changed files with 3193 additions and 283 deletions
@ -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, в которой автоматически маскируются "секретные" ключи |
|||
(список ключей - см. в исходном коде) |
|||
|
@ -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, |
|||
} |
|||
} |
@ -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()...) |
|||
} |
@ -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) |
|||
} |
|||
} |
@ -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), |
|||
) |
|||
} |
@ -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...)) |
|||
} |
@ -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 |
|||
} |
|||
} |
|||
}) |
|||
} |
@ -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, "→", 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, "→", 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, "→", errI) |
|||
} else { |
|||
initType = v |
|||
err = nil |
|||
} |
|||
} |
|||
|
|||
} |
|||
logger, _ := config.Build() |
|||
|
|||
return logger, initType, err |
|||
return logger |
|||
} |
|||
|
@ -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, "→", 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, "→", 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, "→", errI) |
|||
} else { |
|||
initType = v |
|||
err = nil |
|||
} |
|||
} |
|||
|
|||
} |
|||
|
|||
return logger, initType, err |
|||
} |
@ -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") |
|||
} |
@ -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))) |
|||
} |
@ -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 |
|||
} |
|||
} |
@ -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 |
|||
} |
@ -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") |
|||
} |
|||
} |
@ -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] |
|||
} |
@ -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) |
|||
}) |
|||
} |
|||
} |
@ -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} |
|||
} |
@ -0,0 +1,11 @@ |
|||
package types |
|||
|
|||
func ArrayContains(a []string, x string) bool { |
|||
for _, n := range a { |
|||
if x == n { |
|||
return true |
|||
} |
|||
} |
|||
|
|||
return false |
|||
} |
@ -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)) |
|||
} |
@ -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)) |
|||
}) |
|||
} |
|||
} |
@ -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 |
|||
} |
@ -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) |
|||
} |
|||
} |
@ -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) |
|||
} |
@ -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()) |
|||
} |
|||
} |
@ -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…
Reference in new issue